From 120517bcef6e9ecff6176654f54a569cad0fdcbb Mon Sep 17 00:00:00 2001 From: mohitb35 <44917347+mohitb35@users.noreply.github.com> Date: Tue, 14 Jan 2025 10:18:49 +0700 Subject: [PATCH 01/11] fix: update prisma schema to use PostgreSQL --- prisma/schema.prisma | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 43fa32f5f5..4ca9eb5ea8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -3,7 +3,7 @@ generator client { } datasource db { - provider = "mysql" + provider = "postgresql" url = env("DB_CONN_URL") } @@ -14,7 +14,7 @@ model contribution { treeCount Decimal? @map("tree_count") @db.Decimal(14, 2) bouquetDonationId Int? @map("bouquet_donation_id") quantity Float? - deletedAt DateTime? @map("deleted_at") @db.DateTime(0) + deletedAt DateTime? @map("deleted_at") @db.Timestamp(0) contributionType String @map("contribution_type") @db.VarChar(20) isVerified Int? @map("is_verified") paymentStatus String? @map("payment_status") @db.VarChar(20) @@ -23,14 +23,14 @@ model contribution { profile profile? @relation(fields: [profile_id], references: [id]) bouquet contribution? @relation("bouquet", fields: [bouquetDonationId], references: [id]) bouquetContributions contribution[] @relation("bouquet") - plantDate DateTime @map("plant_date") @db.DateTime(0) + plantDate DateTime @map("plant_date") @db.Timestamp(0) profile_id Int? gift gift[] giftTo Json? @map("gift_data") giftMethod String? @map("gift_method") @db.VarChar(20) tenant tenant? @relation(fields: [tenantId], references: [id]) // Add a one-to-one relation to the tenant model tenantId Int? @map("tenant_id") - created DateTime @unique @db.DateTime(0) + created DateTime @unique @db.Timestamp(0) } model tenant { @@ -45,12 +45,12 @@ model project { guid String @db.VarChar(32) name String? @db.VarChar(255) allowDonations Boolean @map("accept_donations") - description String? @db.LongText + description String? @db.Text country String? @map("country") @db.VarChar(2) unit String? @map("unit_type") @db.VarChar(255) image String? @db.VarChar(255) purpose String @db.VarChar(64) - location String? @db.LongText + location String? @db.Text geoLongitude Float? @map("geo_longitude") geoLatitude Float? @map("geo_latitude") contribution contribution[] @@ -63,7 +63,7 @@ model profile { id Int @id @default(autoincrement()) guid String @db.VarChar(32) name String? @db.VarChar(255) - deleted_at DateTime? @map("deleted_at") @db.DateTime(0) + deleted_at DateTime? @map("deleted_at") @db.Timestamp(0) contribution contribution[] plantProject project[] gift gift[] @@ -77,10 +77,10 @@ model gift { metadata Json? contribution contribution? @relation(fields: [donationId], references: [id], onDelete: Restrict, onUpdate: Restrict, map: "FK_A47C990D4DC1279C") recipient profile? @relation(fields: [recipientId], references: [id]) // Relation field to profile - created DateTime @db.DateTime(0) + created DateTime @db.Timestamp(0) value Int? purpose String @db.VarChar(32) type String @db.VarChar(50) - plantDate DateTime? @map("payment_date") @db.DateTime(0) - redemptionDate DateTime? @map("redemption_date") @db.DateTime(0) + plantDate DateTime? @map("payment_date") @db.Timestamp(0) + redemptionDate DateTime? @map("redemption_date") @db.Timestamp(0) } From 0b264156121cae0a572e6014bbf4ff7326ae2b58 Mon Sep 17 00:00:00 2001 From: mohitb35 <44917347+mohitb35@users.noreply.github.com> Date: Tue, 14 Jan 2025 10:55:11 +0700 Subject: [PATCH 02/11] fix: update projectList procedure for postgresql --- src/server/procedures/myForest/projectList.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/server/procedures/myForest/projectList.ts b/src/server/procedures/myForest/projectList.ts index 89c76d6451..d35e99e4b8 100644 --- a/src/server/procedures/myForest/projectList.ts +++ b/src/server/procedures/myForest/projectList.ts @@ -18,18 +18,22 @@ export const projectListsProcedure = procedure.query(async () => { p.name, p.slug, p.classification, - COALESCE(metadata ->> '$.ecosystem', metadata ->> '$.ecosystems') as ecosystem, + COALESCE(metadata ->> 'ecosystem', metadata ->> 'ecosystems') as ecosystem, p.purpose, - p.unit_type AS unitType, + p.unit_type AS "unitType", p.country, p.geometry, p.image, CASE - WHEN p.accept_donations = 1 AND p.prohibit_donations = 0 AND p.is_active = 1 AND p.is_published = 1 AND p.is_verified = 1 + WHEN p.accept_donations IS TRUE + AND p.prohibit_donations IS FALSE + AND p.is_active IS TRUE + AND p.is_published IS TRUE + AND p.is_verified IS TRUE THEN TRUE ELSE FALSE - END AS allowDonations, - prof.name AS tpoName + END AS "allowDonations", + prof.name AS "tpoName" FROM project p INNER JOIN profile prof ON p.tpo_id = prof.id From 63dcf6f9d5d05a7c1f37c92e075832575aee9f7b Mon Sep 17 00:00:00 2001 From: mohitb35 <44917347+mohitb35@users.noreply.github.com> Date: Wed, 15 Jan 2025 11:28:28 +0700 Subject: [PATCH 03/11] fix: adapt contributions procedure for postgresql --- .../procedures/myForest/contributions.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/server/procedures/myForest/contributions.ts b/src/server/procedures/myForest/contributions.ts index e725cfebda..287976870e 100644 --- a/src/server/procedures/myForest/contributions.ts +++ b/src/server/procedures/myForest/contributions.ts @@ -73,18 +73,18 @@ async function fetchContributions( const contributions = await prisma.$queryRaw` SELECT c.guid, - COALESCE(c.quantity, c.tree_count) as units, - c.unit_type as unitType, - c.plant_date as plantDate, - c.contribution_type as contributionType, - c.plant_project_id as projectId, + COALESCE(c.quantity, c.tree_count) as "units", + c.unit_type as "unitType", + c.plant_date as "plantDate", + c.contribution_type as "contributionType", + c.plant_project_id as "projectId", c.amount, c.currency, c.country, c.geometry, - c.gift_method as giftMethod, - c.gift_data->>'$.recipientName' as giftRecipient, - c.gift_data->>'$.type' as giftType + c.gift_method as "giftMethod", + c.gift_data->>'recipientName' as "giftRecipient", + c.gift_data->>'type' as "giftType" FROM contribution c WHERE @@ -104,12 +104,12 @@ async function fetchContributions( async function fetchGifts(profileIds: number[]): Promise { const gifts = await prisma.$queryRaw` SELECT - round((g.value)/100, 2) as quantity, - g.metadata->>'$.giver.name' as giftGiver, - g.metadata->>'$.project.id' as projectGuid, - g.metadata->>'$.project.name' as projectName, - g.metadata->>'$.project.country' as country, - COALESCE(g.payment_date, g.redemption_date) as plantDate + ROUND(CAST(g.value AS NUMERIC)/100, 2) as "quantity", + g.metadata->'giver'->>'name' as "giftGiver", + g.metadata->'project'->>'id' as "projectGuid", + g.metadata->'project'->>'name' as "projectName", + g.metadata->'project'->>'country' as "country", + COALESCE(g.payment_date, g.redemption_date) as "plantDate" FROM gift g WHERE From b62378d2689c3c5a3607057548f763531527ab01 Mon Sep 17 00:00:00 2001 From: mohitb35 <44917347+mohitb35@users.noreply.github.com> Date: Wed, 15 Jan 2025 11:42:58 +0700 Subject: [PATCH 04/11] fix: adapt leaderboard procedure for postgresql --- src/server/procedures/myForest/leaderboard.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/server/procedures/myForest/leaderboard.ts b/src/server/procedures/myForest/leaderboard.ts index 4ce12922fc..264d416f08 100644 --- a/src/server/procedures/myForest/leaderboard.ts +++ b/src/server/procedures/myForest/leaderboard.ts @@ -19,8 +19,8 @@ async function fetchMostRecentGifts(profileIds: number[]) { }[] >` SELECT - round((value)/100, 2) as quantity, - COALESCE(NULLIF(metadata->>'$.giver.name', ''), 'anonymous') as giverName, + ROUND(CAST(value AS NUMERIC)/100, 2) as "quantity", + COALESCE(NULLIF(metadata->'giver'->>'name', ''), 'anonymous') as "giverName", purpose FROM gift WHERE @@ -41,18 +41,18 @@ async function fetchTopGifters(profileIds: number[]) { }[] >` SELECT - sum(round((value)/100, 2)) as totalQuantity, - metadata->>'$.giver.name' as giverName, + SUM(ROUND(CAST(value AS NUMERIC)/100, 2)) as "totalQuantity", + metadata->'giver'->>'name' as "giverName", purpose FROM gift WHERE recipient_id IN (${Prisma.join(profileIds)}) AND deleted_at IS NULL AND value <> 0 - AND metadata->>'$.giver.name' IS NOT NULL - AND metadata->>'$.giver.name' <> '' - GROUP BY giverName, purpose - ORDER BY totalQuantity DESC + AND metadata->'giver'->>'name' IS NOT NULL + AND metadata->'giver'->>'name' <> '' + GROUP BY "giverName", "purpose" + ORDER BY "totalQuantity" DESC LIMIT 10; `; } From f2646a58ad5c1d8fa37e6c973be8c333060a2ff9 Mon Sep 17 00:00:00 2001 From: mohitb35 <44917347+mohitb35@users.noreply.github.com> Date: Wed, 15 Jan 2025 13:19:04 +0700 Subject: [PATCH 05/11] fix: adapt fetchProfileGroupData for postgresql --- src/server/utils/fetchProfileGroupData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/utils/fetchProfileGroupData.ts b/src/server/utils/fetchProfileGroupData.ts index 0f8df99fc1..6760a46aeb 100644 --- a/src/server/utils/fetchProfileGroupData.ts +++ b/src/server/utils/fetchProfileGroupData.ts @@ -4,7 +4,7 @@ import prisma from '../../../prisma/client'; export async function fetchProfileGroupData(profileId: number) { const data = await prisma.$queryRaw` - SELECT pg.profile_id as profileId + SELECT pg.profile_id as "profileId" FROM profile_group AS pg WHERE pg.lft BETWEEN ( SELECT root.lft From 874d55beef35ca133380a9423a224fcfd6599020 Mon Sep 17 00:00:00 2001 From: mohitb35 <44917347+mohitb35@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:23:45 +0700 Subject: [PATCH 06/11] chore: install serverless-postgres --- package-lock.json | 146 ++++++++++++++++++++++++++++++++++++++++++---- package.json | 1 + 2 files changed, 137 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 819bebea97..4c070c542a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -96,6 +96,7 @@ "read-more-react": "^1.0.10", "sass": "^1.62.1", "serverless-mysql": "^1.5.4", + "serverless-postgres": "^2.1.1", "slick-carousel": "^1.8.1", "styled-jsx": "^5.0.0", "supercluster": "^8.0.1", @@ -5316,7 +5317,7 @@ "version": "4.16.2", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.16.2.tgz", "integrity": "sha512-vx1nxVvN4QeT/cepQce68deh/Turxy5Mr+4L4zClFuK1GlxN3+ivxfuv+ej/gvidWn1cE1uAhW7ALLNlYbRUAw==", - "devOptional": true, + "dev": true, "hasInstallScript": true }, "node_modules/@prisma/engines-version": { @@ -14198,7 +14199,7 @@ "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", - "devOptional": true + "dev": true }, "node_modules/debug": { "version": "4.3.7", @@ -19784,13 +19785,6 @@ "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/jquery": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", - "license": "MIT", - "peer": true - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -25225,6 +25219,87 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, + "node_modules/pg": { + "version": "8.13.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz", + "integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==", + "dependencies": { + "pg-connection-string": "^2.7.0", + "pg-pool": "^3.7.0", + "pg-protocol": "^1.7.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz", + "integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz", + "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -25563,6 +25638,41 @@ "node": ">=6.14.4" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/potpack": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", @@ -25894,7 +26004,7 @@ "version": "4.16.2", "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.16.2.tgz", "integrity": "sha512-SYCsBvDf0/7XSJyf2cHTLjLeTLVXYfqp7pG5eEVafFLeT0u/hLFz/9W196nDRGUOo1JfPatAEb+uEnTQImQC1g==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "dependencies": { "@prisma/engines": "4.16.2" @@ -28007,6 +28117,14 @@ "@types/mysql": "^2.15.6" } }, + "node_modules/serverless-postgres": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/serverless-postgres/-/serverless-postgres-2.1.1.tgz", + "integrity": "sha512-ctkK/XL0iVXBXWUZrKp6vLibxgpUmy7EnmXowSzMazO4bTwNC11WPOsfRTa42RdthtvQWgtT14CyVFzyQ+pUdQ==", + "dependencies": { + "pg": "^8.11.5" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -28449,6 +28567,14 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", diff --git a/package.json b/package.json index 5f4ed72f2c..86466eec9d 100644 --- a/package.json +++ b/package.json @@ -130,6 +130,7 @@ "read-more-react": "^1.0.10", "sass": "^1.62.1", "serverless-mysql": "^1.5.4", + "serverless-postgres": "^2.1.1", "slick-carousel": "^1.8.1", "styled-jsx": "^5.0.0", "supercluster": "^8.0.1", From ae45f8dc1cd6b4b0892df6d96b09d5c260215721 Mon Sep 17 00:00:00 2001 From: mohitb35 <44917347+mohitb35@users.noreply.github.com> Date: Wed, 15 Jan 2025 17:24:48 +0700 Subject: [PATCH 07/11] feat: migrate db connection from MySQL to PostgreSQL - replace `serverless-mysql` with `serverless-postgres` - set up `query` helper function with connection lifecycle management - add TypeScript interfaces for database responses - configure debug mode for local development This changes how database connections are handled: - explicit connect() before queries - clean() after queries for proper resource cleanup --- src/utils/connectDB.ts | 79 +++++++++++++++++++++++++++++++++++------- 1 file changed, 66 insertions(+), 13 deletions(-) diff --git a/src/utils/connectDB.ts b/src/utils/connectDB.ts index 5325299699..5a7155137d 100644 --- a/src/utils/connectDB.ts +++ b/src/utils/connectDB.ts @@ -1,20 +1,73 @@ -import mysql from 'serverless-mysql'; +import postgres from 'serverless-postgres'; import { ConnectionString } from 'connection-string'; -const { user, password, path, hostname: host, port } = new ConnectionString( - process.env.DB_CONN_URL -); +// Types for database configuration +interface DatabaseConfig { + host?: string; + port?: number; + database?: string; + user?: string; + password?: string; + debug?: boolean; +} + +interface Field { + name: string; + tableID: number; + columnID: number; + dataTypeID: number; + dataTypeSize: number; + dataTypeModifier: number; + format: string; +} + +// Type for query response from serverless-postgres +interface QueryResult { + command: 'SELECT' | 'UPDATE' | 'INSERT' | 'DELETE'; + rowCount: number; + rows: T[]; + fields: Field[]; +} + +const { + user, + password, + path, + hostname: host, + port, +} = new ConnectionString(process.env.DB_CONN_URL); const database = path?.[0]; -const db = mysql({ - config: { - host, - port, - database, - user, - password, - }, -}); +const config: DatabaseConfig = { + host, + port: port, + database, + user, + password, + debug: process.env.NODE_ENV === 'development', +}; + +const db = new postgres(config); + +/** + * Executes a SQL query with proper connection handling + * @param queryText - The SQL query text with $1, $2, etc. for parameters + * @param values - Array of parameter values [$1, $2 etc.] + * @returns Promise resolving to an array of query results + * @throws Error if query fails + */ +export async function query( + queryText: string, + values: unknown[] = [] +): Promise { + try { + await db.connect(); + const result: QueryResult = await db.query(queryText, values); + return result.rows; + } finally { + await db.clean(); // This is better than quit() for serverless environments + } +} export default db; From feb95d2a413f8129ec4b597f73a4a2dd628e259c Mon Sep 17 00:00:00 2001 From: mohitb35 <44917347+mohitb35@users.noreply.github.com> Date: Thu, 16 Jan 2025 16:42:21 +0700 Subject: [PATCH 08/11] feat: adapts data explorer APIs for postgres --- pages/api/data-explorer/export.ts | 67 +++---- .../map/distinct-species/[projectId].ts | 33 ++-- .../map/plant-location/[plantLocationId].ts | 149 +++++++-------- .../data-explorer/map/plant-location/index.ts | 58 +++--- .../data-explorer/map/sites/[projectId].ts | 24 ++- pages/api/data-explorer/species-planted.ts | 60 +++--- .../data-explorer/total-species-planted.ts | 47 ++--- .../api/data-explorer/total-trees-planted.ts | 31 +-- pages/api/data-explorer/trees-planted.ts | 176 ++++++++++-------- src/features/common/types/dataExplorer.d.ts | 14 +- 10 files changed, 341 insertions(+), 318 deletions(-) diff --git a/pages/api/data-explorer/export.ts b/pages/api/data-explorer/export.ts index 9ec7c4e988..66bf485d76 100644 --- a/pages/api/data-explorer/export.ts +++ b/pages/api/data-explorer/export.ts @@ -1,7 +1,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import type { IExportData } from '../../../src/features/common/types/dataExplorer'; -import db from '../../../src/utils/connectDB'; +import { query } from '../../../src/utils/connectDB'; import nc from 'next-connect'; import { rateLimiter, @@ -16,46 +16,47 @@ handler.use(speedLimiter); handler.post(async (req, response) => { const { projectId, startDate, endDate } = req.body; try { - const query = ` + const queryText = ` SELECT - iv.hid, - iv.intervention_start_date, - COALESCE(ss.name, ps.other_species, iv.other_species, 'Unknown') AS species, - CASE WHEN iv.type='single-tree-registration' THEN 1 ELSE ps.tree_count END AS tree_count, - iv.geometry, - iv.type, - iv.trees_allocated, - iv.trees_planted, - iv.metadata, - iv.description, - iv.plant_project_id, - iv.sample_tree_count, - iv.capture_status, - iv.created - FROM intervention iv - LEFT JOIN planted_species ps ON ps.intervention_id = iv.id - LEFT JOIN scientific_species ss ON COALESCE(iv.scientific_species_id, ps.scientific_species_id) = ss.id - JOIN project pp ON iv.plant_project_id = pp.id - WHERE - pp.guid=? AND - iv.type IN ('multi-tree-registration','single-tree-registration') AND - iv.deleted_at IS NULL AND - iv.intervention_start_date BETWEEN ? AND ? - `; - - const res = await db.query(query, [ + iv.hid, + iv.intervention_start_date, + COALESCE(ss.name, ps.other_species, iv.other_species, 'Unknown') AS species, + CASE WHEN iv.type='single-tree-registration' THEN 1 ELSE ps.tree_count END AS tree_count, + iv.geometry, + iv.type, + iv.trees_allocated, + iv.trees_planted, + iv.metadata, + iv.description, + iv.plant_project_id, + iv.sample_tree_count, + iv.capture_status, + iv.created + FROM intervention iv + LEFT JOIN planted_species ps ON ps.intervention_id = iv.id + LEFT JOIN scientific_species ss ON COALESCE(iv.scientific_species_id, ps.scientific_species_id) = ss.id + JOIN project pp ON iv.plant_project_id = pp.id + WHERE + pp.guid = $1 AND + iv.type IN ('multi-tree-registration','single-tree-registration') AND + iv.deleted_at IS NULL AND + iv.intervention_start_date BETWEEN $2 AND $3 + `; + + // Set the end date to the end of the day + const endDateTime = new Date(endDate); + endDateTime.setHours(23, 59, 59, 999); + + const res = await query(queryText, [ projectId, startDate, - `${endDate} 23:59:59.999`, + endDateTime, ]); - await db.end(); - response.status(200).json({ data: res }); } catch (err) { console.error('Error fetching export data:', err); - } finally { - await db.quit(); + response.status(500).json({ error: 'Failed to fetch export data' }); } }); diff --git a/pages/api/data-explorer/map/distinct-species/[projectId].ts b/pages/api/data-explorer/map/distinct-species/[projectId].ts index df03d043f6..3623dc6d0f 100644 --- a/pages/api/data-explorer/map/distinct-species/[projectId].ts +++ b/pages/api/data-explorer/map/distinct-species/[projectId].ts @@ -4,7 +4,7 @@ import type { UncleanDistinctSpecies, } from '../../../../../src/features/common/types/dataExplorer'; -import db from '../../../../../src/utils/connectDB'; +import { query } from '../../../../../src/utils/connectDB'; import nc from 'next-connect'; import { rateLimiter, @@ -43,20 +43,20 @@ handler.get(async (req, response) => { let distinctSpecies: DistinctSpecies; try { - const query = ` - SELECT - DISTINCT COALESCE(ss.name, ps.other_species, iv.other_species, 'Unknown') AS name - FROM intervention iv - LEFT JOIN planted_species ps ON iv.id = ps.intervention_id - LEFT JOIN scientific_species ss ON COALESCE(iv.scientific_species_id, ps.scientific_species_id) = ss.id - JOIN project pp ON iv.plant_project_id = pp.id - WHERE - pp.guid = ? - AND iv.deleted_at IS NULL - AND iv.type IN ('single-tree-registration', 'multi-tree-registration') - `; - - const res = await db.query(query, [projectId]); + const queryText = ` + SELECT + DISTINCT COALESCE(ss.name, ps.other_species, iv.other_species, 'Unknown') AS "name" + FROM intervention iv + LEFT JOIN planted_species ps ON iv.id = ps.intervention_id + LEFT JOIN scientific_species ss ON COALESCE(iv.scientific_species_id, ps.scientific_species_id) = ss.id + JOIN project pp ON iv.plant_project_id = pp.id + WHERE + pp.guid = $1 + AND iv.deleted_at IS NULL + AND iv.type IN ('single-tree-registration', 'multi-tree-registration') + `; + + const res = await query(queryText, [projectId]); distinctSpecies = res.map((species) => species.name); @@ -69,8 +69,7 @@ handler.get(async (req, response) => { response.status(200).json({ data: distinctSpecies }); } catch (err) { console.error(`Error fetching distinct species for ${projectId}`, err); - } finally { - db.quit(); + response.status(500).json({ error: 'Failed to fetch distinct species' }); } }); diff --git a/pages/api/data-explorer/map/plant-location/[plantLocationId].ts b/pages/api/data-explorer/map/plant-location/[plantLocationId].ts index 1fb08a842a..5eef638061 100644 --- a/pages/api/data-explorer/map/plant-location/[plantLocationId].ts +++ b/pages/api/data-explorer/map/plant-location/[plantLocationId].ts @@ -5,93 +5,88 @@ import type { } from '../../../../../src/features/common/types/dataExplorer'; import nc from 'next-connect'; -import db from '../../../../../src/utils/connectDB'; +import { query } from '../../../../../src/utils/connectDB'; const handler = nc(); handler.get(async (req, response) => { const { plantLocationId } = req.query; - const query = ` - SELECT - JSON_OBJECT( - 'properties', ( - SELECT JSON_OBJECT( - 'type', iv.type, - 'hid', iv.hid - ) - FROM intervention iv - WHERE iv.guid = ? - ), - 'plantedSpecies', ( - SELECT JSON_ARRAYAGG( - JSON_OBJECT( - 'scientificName', COALESCE(ss.name, ps.other_species, iv.other_species), - 'treeCount', COALESCE(ps.tree_count, iv.trees_planted, 0) - ) - ) - FROM intervention iv - LEFT JOIN planted_species ps ON iv.id = ps.intervention_id - LEFT JOIN scientific_species ss ON COALESCE(iv.scientific_species_id, ps.scientific_species_id) = ss.id - WHERE iv.guid = ? - GROUP BY iv.id - ), - 'totalPlantedTrees', ( - SELECT SUM(COALESCE(ps.tree_count, iv.trees_planted, 0)) - FROM intervention iv - LEFT JOIN planted_species ps ON iv.id = ps.intervention_id - LEFT JOIN scientific_species ss ON COALESCE(iv.scientific_species_id, ps.scientific_species_id) = ss.id - WHERE iv.guid = ? - GROUP BY iv.id - ), - 'samplePlantLocations', ( - SELECT JSON_ARRAYAGG( - JSON_OBJECT( - 'measurements', JSON_OBJECT( - 'height', JSON_UNQUOTE(JSON_EXTRACT(siv.measurements, '$.height')), - 'width', JSON_UNQUOTE(JSON_EXTRACT(siv.measurements, '$.width')) - ), - 'tag', siv.tag, - 'guid', siv.guid, - 'species', ( - SELECT ss.name - FROM intervention pl_inner - JOIN scientific_species ss ON pl_inner.scientific_species_id = ss.id - WHERE pl_inner.guid = siv.guid - LIMIT 1 - ), - 'geometry', JSON_OBJECT( - 'type', JSON_UNQUOTE(JSON_EXTRACT(siv.geometry, '$.type')), - 'coordinates', JSON_EXTRACT(siv.geometry, '$.coordinates') - ) - ) - ) - FROM intervention iv - INNER JOIN intervention siv ON iv.id = siv.parent_id - LEFT JOIN scientific_species ss ON iv.scientific_species_id = ss.id - WHERE iv.guid = ? - GROUP BY iv.parent_id - ), - 'totalSamplePlantLocations', ( - SELECT COUNT(*) - FROM intervention iv - INNER JOIN intervention siv ON iv.id = siv.parent_id - WHERE iv.guid = ? - GROUP BY iv.parent_id - ) - ) - AS result; - `; + const queryText = ` + SELECT + JSON_BUILD_OBJECT( + 'properties', ( + SELECT JSON_BUILD_OBJECT( + 'type', iv.type, + 'hid', iv.hid + ) + FROM intervention iv + WHERE iv.guid = $1 + ), + 'plantedSpecies', ( + SELECT COALESCE(JSON_AGG( + JSON_BUILD_OBJECT( + 'scientificName', COALESCE(ss.name, ps.other_species, iv.other_species), + 'treeCount', COALESCE(ps.tree_count, iv.trees_planted, 0) + ) + ), '[]'::json) + FROM intervention iv + LEFT JOIN planted_species ps ON iv.id = ps.intervention_id + LEFT JOIN scientific_species ss ON COALESCE(iv.scientific_species_id, ps.scientific_species_id) = ss.id + WHERE iv.guid = $1 + GROUP BY iv.id + ), + 'totalPlantedTrees', ( + SELECT SUM(COALESCE(ps.tree_count, iv.trees_planted, 0)) + FROM intervention iv + LEFT JOIN planted_species ps ON iv.id = ps.intervention_id + LEFT JOIN scientific_species ss ON COALESCE(iv.scientific_species_id, ps.scientific_species_id) = ss.id + WHERE iv.guid = $1 + GROUP BY iv.id + ), + 'samplePlantLocations', ( + SELECT COALESCE(JSON_AGG( + JSON_BUILD_OBJECT( + 'measurements', JSON_BUILD_OBJECT( + 'height', (siv.measurements->>'height')::text, + 'width', (siv.measurements->>'width')::text + ), + 'tag', siv.tag, + 'guid', siv.guid, + 'species', ( + SELECT ss.name + FROM intervention pl_inner + JOIN scientific_species ss ON pl_inner.scientific_species_id = ss.id + WHERE pl_inner.guid = siv.guid + LIMIT 1 + ), + 'geometry', JSON_BUILD_OBJECT( + 'type', (siv.geometry->>'type')::text, + 'coordinates', siv.geometry->'coordinates' + ) + ) + ), '[]'::json) + FROM intervention iv + INNER JOIN intervention siv ON iv.id = siv.parent_id + LEFT JOIN scientific_species ss ON iv.scientific_species_id = ss.id + WHERE iv.guid = $1 + GROUP BY iv.parent_id + ), + 'totalSamplePlantLocations', ( + SELECT COUNT(*) + FROM intervention iv + INNER JOIN intervention siv ON iv.id = siv.parent_id + WHERE iv.guid = $1 + GROUP BY iv.parent_id + ) + ) as result + `; - const res = await db.query(query, [ - plantLocationId, - plantLocationId, - plantLocationId, - plantLocationId, + const res = await query(queryText, [ plantLocationId, ]); - const plantLocationDetails: PlantLocationDetails = JSON.parse(res[0].result); + const plantLocationDetails: PlantLocationDetails = res[0]?.result || null; response.status(200).json({ res: plantLocationDetails, diff --git a/pages/api/data-explorer/map/plant-location/index.ts b/pages/api/data-explorer/map/plant-location/index.ts index dc7899c5c7..bdf96c29ff 100644 --- a/pages/api/data-explorer/map/plant-location/index.ts +++ b/pages/api/data-explorer/map/plant-location/index.ts @@ -1,15 +1,15 @@ import type { NextApiRequest, NextApiResponse } from 'next'; -import type { Geometry } from '@turf/turf'; +import type { Point, Polygon } from 'geojson'; import type { SinglePlantLocationApiResponse } from '../../../../../src/features/common/types/dataExplorer'; +import { query } from '../../../../../src/utils/connectDB'; import nc from 'next-connect'; -import db from '../../../../../src/utils/connectDB'; import { QueryType } from '../../../../../src/features/user/TreeMapper/Analytics/constants'; const handler = nc(); interface UncleanPlantLocations { - geometry: string; + geometry: Point | Polygon; guid: string; treeCount: number; name: string; @@ -20,49 +20,57 @@ handler.post(async (req, response) => { req.body; try { - let query = ` + let queryText = ` SELECT - iv.guid, - iv.trees_planted as treeCount, - iv.geometry, - COALESCE(ss.name, ps.other_species, iv.other_species, 'Unknown') AS name - FROM intervention iv - LEFT JOIN planted_species ps ON iv.id = ps.intervention_id - LEFT JOIN scientific_species ss ON COALESCE(iv.scientific_species_id, ps.scientific_species_id) = ss.id - JOIN project pp ON iv.plant_project_id = pp.id - WHERE - pp.guid = ? AND - iv.deleted_at IS NULL AND - iv.type in ('multi-tree-registration', 'single-tree-registration') + iv.guid, + CAST(iv.trees_planted AS INTEGER) as "treeCount", + iv.geometry, + COALESCE(ss.name, ps.other_species, iv.other_species, 'Unknown') AS "name" + FROM intervention iv + LEFT JOIN planted_species ps ON iv.id = ps.intervention_id + LEFT JOIN scientific_species ss ON COALESCE(iv.scientific_species_id, ps.scientific_species_id) = ss.id + JOIN project pp ON iv.plant_project_id = pp.id + WHERE + pp.guid = $1 AND + iv.deleted_at IS NULL AND + iv.type in ('multi-tree-registration', 'single-tree-registration') `; const values = [projectId]; + let paramIndex = 2; if (queryType !== QueryType.DATE) { - query += ' AND DATE(iv.intervention_start_date) BETWEEN ? AND ?'; + queryText += ` AND DATE(iv.intervention_start_date) BETWEEN $${paramIndex} AND $${ + paramIndex + 1 + }`; values.push(fromDate, toDate); + paramIndex += 2; } if (queryType) { if (queryType === QueryType.DATE) { // Filter by date - query += ' AND DATE(iv.intervention_start_date) = ?'; + queryText += ` AND DATE(iv.intervention_start_date) = $${paramIndex}`; values.push(searchQuery); + paramIndex++; } else if (queryType === QueryType.HID) { // Filter by HID - query += ' AND iv.hid = ?'; + queryText += ` AND iv.hid = $${paramIndex}`; values.push(searchQuery); + paramIndex++; } } if (species !== 'All') { // Filter by species name - query += - ' AND (ss.name = ? OR ps.other_species = ? OR iv.other_species = ?)'; + queryText += ` AND (ss.name = $${paramIndex} OR ps.other_species = $${ + paramIndex + 1 + } OR iv.other_species = $${paramIndex + 2})`; values.push(species, species, species); + paramIndex += 3; } - const qRes = await db.query(query, values); + const qRes = await query(queryText, values); const plantLocations: SinglePlantLocationApiResponse[] = qRes.map( (plantLocation) => ({ @@ -71,12 +79,10 @@ handler.post(async (req, response) => { guid: plantLocation.guid, treeCount: plantLocation.treeCount, }, - geometry: JSON.parse(plantLocation.geometry) as Geometry, + geometry: plantLocation.geometry, }) ); - await db.end(); - response.setHeader( 'Cache-Control', 's-maxage=7200, stale-while-revalidate' @@ -86,8 +92,6 @@ handler.post(async (req, response) => { } catch (err) { console.error('Error fetching plant location data:', err); response.status(500).json({ error: 'Internal Server Error' }); - } finally { - db.quit(); } }); diff --git a/pages/api/data-explorer/map/sites/[projectId].ts b/pages/api/data-explorer/map/sites/[projectId].ts index c56f6376b6..4a43cc13fd 100644 --- a/pages/api/data-explorer/map/sites/[projectId].ts +++ b/pages/api/data-explorer/map/sites/[projectId].ts @@ -4,7 +4,7 @@ import type { UncleanSite, } from '../../../../../src/features/common/types/dataExplorer'; -import db from '../../../../../src/utils/connectDB'; +import { query } from '../../../../../src/utils/connectDB'; import nc from 'next-connect'; import { rateLimiter, @@ -41,21 +41,22 @@ handler.get(async (req, response) => { } try { - const query = ` + const queryText = ` SELECT - s.name, s.geometry - FROM plant_project_site s - INNER JOIN project p ON s.plant_project_id = p.id - WHERE - p.guid = ?`; + s.name, s.geometry + FROM plant_project_site s + INNER JOIN project p ON s.plant_project_id = p.id + WHERE + p.guid = $1 + `; - const res = await db.query(query, [projectId]); + const res = await query(queryText, [projectId]); const sites: FeatureCollection['features'] = []; for (const site of res) { sites.push({ - geometry: JSON.parse(site.geometry), + geometry: site.geometry, properties: { name: site.name, }, @@ -63,8 +64,6 @@ handler.get(async (req, response) => { }); } - await db.end(); - const featureCollection = { type: 'FeatureCollection', features: sites, @@ -77,8 +76,7 @@ handler.get(async (req, response) => { response.status(200).json({ data: featureCollection }); } catch (err) { console.error(`Error fetching sites for ${projectId}:`, err); - } finally { - db.quit(); + response.status(500).json({ error: 'Failed to fetch sites' }); } }); diff --git a/pages/api/data-explorer/species-planted.ts b/pages/api/data-explorer/species-planted.ts index 2fede8cf75..138aa76082 100644 --- a/pages/api/data-explorer/species-planted.ts +++ b/pages/api/data-explorer/species-planted.ts @@ -1,7 +1,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import type { ISpeciesPlanted } from '../../../src/features/common/types/dataExplorer'; -import db from '../../../src/utils/connectDB'; +import { query } from '../../../src/utils/connectDB'; import nc from 'next-connect'; import { rateLimiter, @@ -41,33 +41,38 @@ handler.post(async (req, response) => { } try { - const query = ` - SELECT - ps.other_species, - COALESCE(iv.scientific_species_id, - ps.scientific_species_id) AS scientific_species_id, - COALESCE(ss.name, - ps.other_species, - iv.other_species, 'Unknown') AS name, - SUM(COALESCE(ps.tree_count, iv.trees_planted, 0)) AS total_tree_count - FROM intervention iv - LEFT JOIN planted_species ps ON iv.id = ps.intervention_id - LEFT JOIN scientific_species ss ON COALESCE(iv.scientific_species_id, ps.scientific_species_id) = ss.id - JOIN project pp ON iv.plant_project_id = pp.id - WHERE - iv.deleted_at IS NULL - AND iv.type IN ('single-tree-registration', 'multi-tree-registration') - AND pp.guid = ? - AND iv.intervention_start_date BETWEEN ? AND ? - GROUP BY - COALESCE(iv.scientific_species_id, ps.scientific_species_id), - COALESCE(ss.name, ps.other_species, iv.other_species, 'Unknown') - ORDER BY total_tree_count DESC`; + const queryText = ` + SELECT + ps.other_species, + COALESCE(iv.scientific_species_id, + ps.scientific_species_id) AS scientific_species_id, + COALESCE(ss.name, + ps.other_species, + iv.other_species, 'Unknown') AS name, + SUM(COALESCE(ps.tree_count, iv.trees_planted, 0))::integer AS total_tree_count + FROM intervention iv + LEFT JOIN planted_species ps ON iv.id = ps.intervention_id + LEFT JOIN scientific_species ss ON COALESCE(iv.scientific_species_id, ps.scientific_species_id) = ss.id + JOIN project pp ON iv.plant_project_id = pp.id + WHERE + iv.deleted_at IS NULL + AND iv.type IN ('single-tree-registration', 'multi-tree-registration') + AND pp.guid = $1 + AND iv.intervention_start_date BETWEEN $2 AND $3 + GROUP BY + ps.other_species, + COALESCE(iv.scientific_species_id, ps.scientific_species_id), + COALESCE(ss.name, ps.other_species, iv.other_species, 'Unknown') + ORDER BY total_tree_count DESC`; - const res = await db.query(query, [ + // Ensure endDate includes time + const endDateTime = new Date(endDate); + endDateTime.setHours(23, 59, 59, 999); + + const res = await query(queryText, [ projectId, startDate, - `${endDate} 23:59:59.999`, + endDateTime, ]); await redisClient.set(CACHE_KEY, JSON.stringify(res), { @@ -77,8 +82,9 @@ handler.post(async (req, response) => { response.status(200).json({ data: res }); } catch (err) { console.error('Error fetching species planted:', err); - } finally { - await db.quit(); + response + .status(500) + .json({ error: 'Failed to fetch species planted data' }); } }); diff --git a/pages/api/data-explorer/total-species-planted.ts b/pages/api/data-explorer/total-species-planted.ts index 367b6c1a1b..5093586dbf 100644 --- a/pages/api/data-explorer/total-species-planted.ts +++ b/pages/api/data-explorer/total-species-planted.ts @@ -1,7 +1,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import type { TotalSpeciesPlanted } from '../../../src/features/common/types/dataExplorer'; -import db from '../../../src/utils/connectDB'; +import { query } from '../../../src/utils/connectDB'; import nc from 'next-connect'; import { rateLimiter, @@ -42,28 +42,32 @@ handler.post(async (req, response) => { } try { - const query = ` + const queryText = ` SELECT - COUNT(DISTINCT COALESCE( - ss.name, - NULLIF(ps.other_species, 'Unknown'), - NULLIF(iv.other_species, 'Unknown') - )) AS totalSpeciesPlanted - FROM intervention iv - LEFT JOIN planted_species ps ON iv.id = ps.intervention_id - LEFT JOIN scientific_species ss ON COALESCE(ps.scientific_species_id, iv.scientific_species_id) = ss.id - JOIN project pp ON iv.plant_project_id = pp.id - WHERE - iv.deleted_at IS NULL - AND iv.type IN ('single-tree-registration', 'multi-tree-registration') - AND pp.guid = ? - AND iv.intervention_start_date BETWEEN ? AND ? - `; + COUNT(DISTINCT COALESCE( + ss.name, + NULLIF(ps.other_species, 'Unknown'), + NULLIF(iv.other_species, 'Unknown') + ))::integer AS "totalSpeciesPlanted" + FROM intervention iv + LEFT JOIN planted_species ps ON iv.id = ps.intervention_id + LEFT JOIN scientific_species ss ON COALESCE(ps.scientific_species_id, iv.scientific_species_id) = ss.id + JOIN project pp ON iv.plant_project_id = pp.id + WHERE + iv.deleted_at IS NULL + AND iv.type IN ('single-tree-registration', 'multi-tree-registration') + AND pp.guid = $1 + AND iv.intervention_start_date BETWEEN $2 AND $3 + `; - const res = await db.query(query, [ + // Ensure endDate includes time + const endDateTime = new Date(endDate); + endDateTime.setHours(23, 59, 59, 999); + + const res = await query(queryText, [ projectId, startDate, - `${endDate} 23:59:59.999`, + endDateTime, ]); await redisClient.set(CACHE_KEY, JSON.stringify(res[0]), { @@ -73,8 +77,9 @@ handler.post(async (req, response) => { response.status(200).json({ data: res[0] }); } catch (err) { console.error('Error fetching total species planted:', err); - } finally { - await db.quit(); + response + .status(500) + .json({ error: 'Failed to fetch total species planted' }); } }); diff --git a/pages/api/data-explorer/total-trees-planted.ts b/pages/api/data-explorer/total-trees-planted.ts index 329be5ceb0..ee05ea28f6 100644 --- a/pages/api/data-explorer/total-trees-planted.ts +++ b/pages/api/data-explorer/total-trees-planted.ts @@ -1,7 +1,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import type { TotalTreesPlanted } from '../../../src/features/common/types/dataExplorer'; -import db from '../../../src/utils/connectDB'; +import { query } from '../../../src/utils/connectDB'; import nc from 'next-connect'; import { rateLimiter, @@ -42,21 +42,25 @@ handler.post(async (req, response) => { } try { - const query = ` + const queryText = ` SELECT - COALESCE(SUM(iv.trees_planted), 0) AS totalTreesPlanted - FROM intervention iv - JOIN project pp ON iv.plant_project_id = pp.id - WHERE - iv.deleted_at IS NULL - AND iv.type IN ('single-tree-registration', 'multi-tree-registration') - AND pp.guid = ? - AND iv.intervention_start_date BETWEEN ? AND ?`; + COALESCE(SUM(iv.trees_planted), 0)::integer AS "totalTreesPlanted" + FROM intervention iv + JOIN project pp ON iv.plant_project_id = pp.id + WHERE + iv.deleted_at IS NULL + AND iv.type IN ('single-tree-registration', 'multi-tree-registration') + AND pp.guid = $1 + AND iv.intervention_start_date BETWEEN $2 AND $3`; - const res = await db.query(query, [ + // Ensure endDate includes time + const endDateTime = new Date(endDate); + endDateTime.setHours(23, 59, 59, 999); + + const res = await query(queryText, [ projectId, startDate, - `${endDate} 23:59:59.999`, + endDateTime, ]); await redisClient.set(CACHE_KEY, JSON.stringify(res[0]), { @@ -66,8 +70,7 @@ handler.post(async (req, response) => { response.status(200).json({ data: res[0] }); } catch (err) { console.error('Error fetching total trees planted:', err); - } finally { - await db.quit(); + response.status(500).json({ error: 'Failed to fetch total trees planted' }); } }); diff --git a/pages/api/data-explorer/trees-planted.ts b/pages/api/data-explorer/trees-planted.ts index c65fbad69d..80c71709b6 100644 --- a/pages/api/data-explorer/trees-planted.ts +++ b/pages/api/data-explorer/trees-planted.ts @@ -6,7 +6,7 @@ import type { IYearlyFrame, } from '../../../src/features/common/types/dataExplorer'; -import db from '../../../src/utils/connectDB'; +import { query } from '../../../src/utils/connectDB'; import { TIME_FRAME } from '../../../src/features/user/TreeMapper/Analytics/components/TreePlanted/TimeFrameSelector'; import nc from 'next-connect'; import { @@ -49,103 +49,116 @@ handler.post(async (req, response) => { return; } - let query: string; + let queryText: string; switch (timeFrame) { case TIME_FRAME.DAYS: - query = ` - SELECT - iv.intervention_start_date AS plantedDate, - SUM(iv.trees_planted) AS treesPlanted - FROM intervention iv - JOIN project pp ON iv.plant_project_id = pp.id - WHERE - iv.deleted_at IS NULL AND - iv.type IN ('single-tree-registration', 'multi-tree-registration') AND - pp.guid = ? AND - iv.intervention_start_date BETWEEN ? AND ? - GROUP BY iv.intervention_start_date - ORDER BY iv.intervention_start_date - `; + queryText = ` + SELECT + iv.intervention_start_date AS "plantedDate", + SUM(iv.trees_planted) AS "treesPlanted" + FROM intervention iv + JOIN project pp ON iv.plant_project_id = pp.id + WHERE + iv.deleted_at IS NULL AND + iv.type IN ('single-tree-registration', 'multi-tree-registration') AND + pp.guid = $1 AND + iv.intervention_start_date BETWEEN $2 AND $3 + GROUP BY iv.intervention_start_date + ORDER BY iv.intervention_start_date + `; break; case TIME_FRAME.WEEKS: - query = ` - SELECT - DATE_SUB(iv.intervention_start_date, INTERVAL WEEKDAY(iv.intervention_start_date) DAY) AS weekStartDate, - DATE_ADD(DATE_SUB(iv.intervention_start_date, INTERVAL WEEKDAY(iv.intervention_start_date) DAY), INTERVAL 6 DAY) AS weekEndDate, - WEEK(iv.intervention_start_date, 1) AS weekNum, - LEFT(MONTHNAME(iv.intervention_start_date), 3) AS month, - YEAR(iv.intervention_start_date) AS year, - SUM(iv.trees_planted) AS treesPlanted - FROM intervention iv - JOIN project pp ON iv.plant_project_id = pp.id - WHERE - iv.deleted_at IS NULL AND - iv.type IN ('single-tree-registration', 'multi-tree-registration') AND - pp.guid = ? AND - iv.intervention_start_date BETWEEN ? AND ? - GROUP BY weekNum, weekStartDate, weekEndDate, month, year - ORDER BY iv.intervention_start_date - `; + queryText = ` + WITH week_ranges AS ( + SELECT + DATE_TRUNC('week', iv.intervention_start_date)::timestamptz AS week_start, + EXTRACT(WEEK FROM iv.intervention_start_date)::integer AS week_num, + SUM(iv.trees_planted) AS trees + FROM intervention iv + JOIN project pp ON iv.plant_project_id = pp.id + WHERE + iv.deleted_at IS NULL AND + iv.type IN ('single-tree-registration', 'multi-tree-registration') AND + pp.guid = $1 AND + iv.intervention_start_date BETWEEN $2 AND $3 + GROUP BY + week_start, + week_num + ) + SELECT + week_start AS "weekStartDate", + (week_start + INTERVAL '6 days')::timestamptz AS "weekEndDate", + week_num AS "weekNum", + TO_CHAR(week_start, 'Mon') AS month, + EXTRACT(YEAR FROM week_start)::integer AS year, + trees::integer AS "treesPlanted" + FROM week_ranges + ORDER BY week_start + `; break; case TIME_FRAME.MONTHS: - query = ` - SELECT - LEFT(MONTHNAME(iv.intervention_start_date), 3) AS month, - YEAR(iv.intervention_start_date) AS year, - SUM(iv.trees_planted) AS treesPlanted - FROM intervention iv - JOIN project pp ON iv.plant_project_id = pp.id - WHERE - iv.deleted_at IS NULL AND - iv.type IN ('single-tree-registration', 'multi-tree-registration') AND - pp.guid = ? AND - iv.intervention_start_date BETWEEN ? AND ? - GROUP BY month, year - ORDER BY iv.intervention_start_date - `; + queryText = ` + SELECT + TO_CHAR(iv.intervention_start_date, 'Mon') AS month, + EXTRACT(YEAR FROM iv.intervention_start_date) AS year, + SUM(iv.trees_planted) AS "treesPlanted" + FROM intervention iv + JOIN project pp ON iv.plant_project_id = pp.id + WHERE + iv.deleted_at IS NULL AND + iv.type IN ('single-tree-registration', 'multi-tree-registration') AND + pp.guid = $1 AND + iv.intervention_start_date BETWEEN $2 AND $3 + GROUP BY month, year, DATE_TRUNC('month', iv.intervention_start_date) + ORDER BY DATE_TRUNC('month', iv.intervention_start_date) + `; break; case TIME_FRAME.YEARS: - query = ` - SELECT - YEAR(iv.intervention_start_date) AS year, - SUM(iv.trees_planted) AS treesPlanted - FROM intervention iv - JOIN project pp ON iv.plant_project_id = pp.id - WHERE - iv.deleted_at IS NULL AND - iv.type IN ('single-tree-registration', 'multi-tree-registration') AND - pp.guid = ? AND - iv.intervention_start_date BETWEEN ? AND ? - GROUP BY year - ORDER BY iv.intervention_start_date - `; + queryText = ` + SELECT + EXTRACT(YEAR FROM iv.intervention_start_date) AS year, + SUM(iv.trees_planted) AS "treesPlanted" + FROM intervention iv + JOIN project pp ON iv.plant_project_id = pp.id + WHERE + iv.deleted_at IS NULL AND + iv.type IN ('single-tree-registration', 'multi-tree-registration') AND + pp.guid = $1 AND + iv.intervention_start_date BETWEEN $2 AND $3 + GROUP BY year + ORDER BY year + `; break; default: - query = ` - SELECT - YEAR(iv.intervention_start_date) AS year, - SUM(iv.trees_planted) AS treesPlanted - FROM intervention iv - JOIN project pp ON iv.plant_project_id = pp.id - WHERE - iv.deleted_at IS NULL AND - iv.type IN ('single-tree-registration', 'multi-tree-registration') AND - pp.guid = ? AND - iv.intervention_start_date BETWEEN ? AND ? - GROUP BY year - ORDER BY iv.intervention_start_date - `; + queryText = ` + SELECT + EXTRACT(YEAR FROM iv.intervention_start_date) AS year, + SUM(iv.trees_planted) AS "treesPlanted" + FROM intervention iv + JOIN project pp ON iv.plant_project_id = pp.id + WHERE + iv.deleted_at IS NULL AND + iv.type IN ('single-tree-registration', 'multi-tree-registration') AND + pp.guid = $1 AND + iv.intervention_start_date BETWEEN $2 AND $3 + GROUP BY year + ORDER BY year + `; } try { - const res = await db.query< - IDailyFrame[] | IWeeklyFrame[] | IMonthlyFrame[] | IYearlyFrame[] - >(query, [projectId, startDate, `${endDate} 23:59:59.999`]); + // Ensure endDate includes time up to the last millisecond of the day + const endDateTime = new Date(endDate); + endDateTime.setHours(23, 59, 59, 999); + + const res = await query< + IDailyFrame | IWeeklyFrame | IMonthlyFrame | IYearlyFrame + >(queryText, [projectId, startDate, endDateTime]); await redisClient.set(CACHE_KEY, JSON.stringify(res), { ex: TWO_HOURS, @@ -154,8 +167,7 @@ handler.post(async (req, response) => { response.status(200).json({ data: res }); } catch (err) { console.error('Error fetching trees planted:', err); - } finally { - await db.quit(); + response.status(500).json({ error: 'Failed to fetch trees planted data' }); } }); diff --git a/src/features/common/types/dataExplorer.d.ts b/src/features/common/types/dataExplorer.d.ts index b6e70401a3..de42b39076 100644 --- a/src/features/common/types/dataExplorer.d.ts +++ b/src/features/common/types/dataExplorer.d.ts @@ -1,4 +1,4 @@ -import type { Geometry } from '@turf/turf'; +import type { Point, Polygon } from 'geojson'; export interface IDailyFrame { plantedDate: string; @@ -58,7 +58,7 @@ export interface TotalSpeciesPlanted { } export interface Feature { - geometry: Geometry; + geometry: Point | Polygon; properties: { name: string; }; @@ -72,7 +72,7 @@ export type FeatureCollection = { export interface UncleanSite { name: string; - geometry: string; + geometry: Point | Polygon; } export interface UncleanDistinctSpecies { @@ -87,7 +87,7 @@ export interface UncleanPlantLocations { } export interface PlantLocation { - geometry: Geometry; + geometry: Point | Polygon; properties: { guid: string; treeCount: number; @@ -111,7 +111,7 @@ export interface Measurements { export interface SamplePlantLocation { tag: string | null; guid: string; - geometry: Geometry; + geometry: Point; measurements: Measurements; } @@ -121,7 +121,7 @@ export interface PlantedSpecies { } export interface PlantLocationDetailsQueryRes { - result: string; + result: PlantLocationDetails; } export interface PlantLocationProperties { @@ -167,7 +167,7 @@ export interface PlantLocationDetailsApiResponse { } export interface SinglePlantLocationApiResponse { - geometry: Geometry; + geometry: Point | Polygon; properties: { guid: string; treeCount: number; From 69270c9950f911b9e02fcd9e8916d1d5ea87ea73 Mon Sep 17 00:00:00 2001 From: mohitb35 <44917347+mohitb35@users.noreply.github.com> Date: Thu, 16 Jan 2025 19:09:43 +0700 Subject: [PATCH 09/11] feat: rotates x-axis labels for more than 15 data items --- .../TreeMapper/Analytics/components/TreePlanted/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/features/user/TreeMapper/Analytics/components/TreePlanted/index.tsx b/src/features/user/TreeMapper/Analytics/components/TreePlanted/index.tsx index ad7784b988..54357f7532 100644 --- a/src/features/user/TreeMapper/Analytics/components/TreePlanted/index.tsx +++ b/src/features/user/TreeMapper/Analytics/components/TreePlanted/index.tsx @@ -508,6 +508,10 @@ export const TreePlanted = () => { xaxis: { ...options.xaxis, categories: categories, + labels: { + ...options.xaxis?.labels, + rotateAlways: data.length > 15, + }, }, tooltip: { custom: function ({ series: s, dataPointIndex }) { From 118ffc518db6be6afe7af10adea1e9f46344f982 Mon Sep 17 00:00:00 2001 From: mohitb35 <44917347+mohitb35@users.noreply.github.com> Date: Fri, 17 Jan 2025 21:11:19 +0700 Subject: [PATCH 10/11] feat: add SSL configuration and maxConnections to database connection --- src/utils/connectDB.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/utils/connectDB.ts b/src/utils/connectDB.ts index 5a7155137d..b4261234e1 100644 --- a/src/utils/connectDB.ts +++ b/src/utils/connectDB.ts @@ -9,6 +9,8 @@ interface DatabaseConfig { user?: string; password?: string; debug?: boolean; + ssl: { rejectUnauthorized: boolean }; + maxConnections?: number; } interface Field { @@ -46,6 +48,8 @@ const config: DatabaseConfig = { user, password, debug: process.env.NODE_ENV === 'development', + ssl: { rejectUnauthorized: false }, + maxConnections: 20, }; const db = new postgres(config); From e4a22c133054bcabec1986892c45b843c2222d90 Mon Sep 17 00:00:00 2001 From: mohitb35 <44917347+mohitb35@users.noreply.github.com> Date: Fri, 17 Jan 2025 21:12:21 +0700 Subject: [PATCH 11/11] chore: uninstall serverless-mysql dependency --- package-lock.json | 83 ----------------------------------------------- package.json | 1 - 2 files changed, 84 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4c070c542a..f7c9f008eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -95,7 +95,6 @@ "react-slick": "^0.30.2", "read-more-react": "^1.0.10", "sass": "^1.62.1", - "serverless-mysql": "^1.5.4", "serverless-postgres": "^2.1.1", "slick-carousel": "^1.8.1", "styled-jsx": "^5.0.0", @@ -10117,15 +10116,6 @@ "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", "dev": true }, - "node_modules/@types/mysql": { - "version": "2.15.26", - "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz", - "integrity": "sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/@types/negotiator/-/negotiator-0.6.3.tgz", @@ -12244,14 +12234,6 @@ "node": "*" } }, - "node_modules/bignumber.js": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", - "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==", - "engines": { - "node": "*" - } - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -21256,52 +21238,6 @@ "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==" }, - "node_modules/mysql": { - "version": "2.18.1", - "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", - "integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==", - "dependencies": { - "bignumber.js": "9.0.0", - "readable-stream": "2.3.7", - "safe-buffer": "5.1.2", - "sqlstring": "2.3.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mysql/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/mysql/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/mysql/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/mysql/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/nanoid": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", @@ -28106,17 +28042,6 @@ "node": ">= 0.8.0" } }, - "node_modules/serverless-mysql": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/serverless-mysql/-/serverless-mysql-1.5.5.tgz", - "integrity": "sha512-QwaCtswn3GKCnqyVA0whwDFMIw91iKTeTvf6F++HoGiunfyvfJ2MdU8d3MKMQdKGNOXIvmUlLq/JVjxuPQxkrw==", - "dependencies": { - "mysql": "^2.18.1" - }, - "optionalDependencies": { - "@types/mysql": "^2.15.6" - } - }, "node_modules/serverless-postgres": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/serverless-postgres/-/serverless-postgres-2.1.1.tgz", @@ -28581,14 +28506,6 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, - "node_modules/sqlstring": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", - "integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/ssf": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", diff --git a/package.json b/package.json index 86466eec9d..8b65de33bb 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,6 @@ "react-slick": "^0.30.2", "read-more-react": "^1.0.10", "sass": "^1.62.1", - "serverless-mysql": "^1.5.4", "serverless-postgres": "^2.1.1", "slick-carousel": "^1.8.1", "styled-jsx": "^5.0.0",