diff --git a/apps/backend/db/.env.dev b/apps/backend/db/.env.dev index 08c799b..06e6c82 100644 --- a/apps/backend/db/.env.dev +++ b/apps/backend/db/.env.dev @@ -1 +1 @@ -DATABASE_URL=postgresql://branch_dev:password@localhost:5433/branch_db +DATABASE_URL=postgresql://branch_dev:password@localhost:5432/branch_db diff --git a/apps/backend/lambdas/projects/handler.ts b/apps/backend/lambdas/projects/handler.ts index 0c24d41..6b99782 100644 --- a/apps/backend/lambdas/projects/handler.ts +++ b/apps/backend/lambdas/projects/handler.ts @@ -25,6 +25,44 @@ export const handler = async (event: any): Promise => { const projects = await db.selectFrom("branch.projects").selectAll().execute(); return json(200, projects); } + + // GET /projects/{id}/donors + const parts = normalizedPath.split('/'); + if (parts.length === 3 && parts[2] === 'donors' && method === 'GET') { + const id = parts[1]; + + + if (!id) return json(400, { message: 'id is required' }); + if (isNaN(Number(id))) { + return json(400, { message: 'Project id must be a valid number' }); + } + const queryString = event.rawQueryString || event.queryStringParameters; + + if (queryString && (typeof queryString === 'string' ? queryString.length > 0 : Object.keys(queryString).length > 0)) { + return json(400, { message: 'Bad Request: Query parameters are not allowed' }); + } + + const project = await db + .selectFrom("branch.projects as p") + .where("p.project_id", "=", Number(id)) + .selectAll() + .executeTakeFirst(); + + if (!project) { + return json(404, { message: 'Project not found' }); + } + + const donors = await db.selectFrom("branch.projects as p").where("p.project_id", "=", Number(id)).innerJoin( + "branch.project_donations as bpd", + "bpd.project_id", + "p.project_id" + ).innerJoin( + "branch.donors as bd", + "bd.donor_id", + "bpd.donor_id" + ).selectAll().execute(); + return json(200, { donors }); + } // POST /projects if ((normalizedPath === '' || normalizedPath === '/' || normalizedPath === '/projects') && method === 'POST') { diff --git a/apps/backend/lambdas/projects/openapi.yaml b/apps/backend/lambdas/projects/openapi.yaml index 7b45fbb..42bc87b 100644 --- a/apps/backend/lambdas/projects/openapi.yaml +++ b/apps/backend/lambdas/projects/openapi.yaml @@ -20,6 +20,11 @@ paths: type: boolean /projects: + get: + summary: GET /projects + responses: + '200': + description: OK post: summary: POST /projects requestBody: @@ -28,7 +33,7 @@ paths: application/json: schema: type: object - required: + required: - name properties: name: @@ -38,3 +43,40 @@ paths: responses: '200': description: OK + + /projects/{id}: + get: + summary: GET /projects/{id} + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + '200': + description: OK + put: + summary: PUT /projects/{id} + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + '200': + description: OK + + /projects/{id}/donors: + get: + summary: GET /projects/{id}/donors + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + '200': + description: OK \ No newline at end of file diff --git a/apps/backend/lambdas/projects/package-lock.json b/apps/backend/lambdas/projects/package-lock.json index 84e231e..e57c5aa 100644 --- a/apps/backend/lambdas/projects/package-lock.json +++ b/apps/backend/lambdas/projects/package-lock.json @@ -499,7 +499,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.9" @@ -1070,7 +1070,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", @@ -1146,28 +1146,28 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tybys/wasm-util": { @@ -1565,7 +1565,7 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -1578,7 +1578,7 @@ "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "acorn": "^8.11.0" @@ -1646,7 +1646,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/argparse": { @@ -2116,7 +2116,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/cross-spawn": { @@ -2196,7 +2196,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -2775,7 +2775,7 @@ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "license": "MIT", "engines": { - "node": ">=0.8.19" + "node": ">=6.0.0" } }, "node_modules/inflight": { @@ -2872,9 +2872,9 @@ } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3407,9 +3407,9 @@ } }, "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3679,9 +3679,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3694,7 +3694,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/makeerror": { @@ -4817,7 +4817,7 @@ "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -4889,7 +4889,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -4987,7 +4987,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/v8-to-istanbul": { @@ -5268,7 +5268,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" diff --git a/apps/backend/lambdas/projects/test/example.test.ts b/apps/backend/lambdas/projects/test/example.test.ts new file mode 100644 index 0000000..094d7f4 --- /dev/null +++ b/apps/backend/lambdas/projects/test/example.test.ts @@ -0,0 +1,61 @@ + +test("health test 🌞", async () => { + let res = await fetch("http://localhost:3000/projects/health") + expect(res.status).toBe(200); +}); + + +test("get projects no donors test 🌞", async () => { + const res = await fetch("http://localhost:3000/projects/4/donors"); + expect(res.status).toBe(200); + let body = await res.json(); + console.log(body); + expect(body.donors).toBeDefined(); + expect(Array.isArray(body.donors)).toBe(true); +}); + +test("get projects yes donors test 🌞", async () => { + const res = await fetch("http://localhost:3000/projects/1/donors"); + expect(res.status).toBe(200); + let body = await res.json(); + console.log(body); + expect(body.donors).toBeDefined(); + expect(Array.isArray(body.donors)).toBe(true); + if (body.donors.length > 0) { + const donor = body.donors[0]; + expect(donor.project_id).toBeDefined(); + expect(donor.name).toBeDefined(); + expect(donor.total_budget).toBeDefined(); + expect(donor.start_date).toBeDefined(); + expect(donor.end_date).toBeDefined(); + expect(donor.currency).toBeDefined(); + expect(donor.created_at).toBeDefined(); + expect(donor.donation_id).toBeDefined(); + expect(donor.donor_id).toBeDefined(); + expect(donor.amount).toBeDefined(); + expect(donor.donated_at).toBeDefined(); + expect(donor.organization).toBeDefined(); + expect(donor.contact_name).toBeDefined(); + expect(donor.contact_email).toBeDefined(); + } +}); + + +test("404 when invalid project id 🌞", async () => { + const res = await fetch("http://localhost:3000/projects/1000/donors"); + expect(res.status).toBe(404); +}); + +test("400 when project id is not a number 🌞", async () => { + const res = await fetch("http://localhost:3000/projects/abc/donors"); + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.message).toContain("must be a valid number"); +}); + +test("400 when request has both body and query params 🌞", async () => { + const res = await fetch("http://localhost:3000/projects/1/donors?sort=desc"); + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.message).toContain("Bad Request"); +}); \ No newline at end of file