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/README.md b/apps/backend/lambdas/projects/README.md index aab30b3..36ca14d 100644 --- a/apps/backend/lambdas/projects/README.md +++ b/apps/backend/lambdas/projects/README.md @@ -11,6 +11,7 @@ Lambda for managing projects. | GET | /health | Health check | | GET | /projects/{id}/members | | | GET | /projects | | +| GET | /projects/{id}/donors | | | GET | /projects/{id} | | | PUT | /projects/{id} | | diff --git a/apps/backend/lambdas/projects/handler.ts b/apps/backend/lambdas/projects/handler.ts index cb765d2..cf4eb1a 100644 --- a/apps/backend/lambdas/projects/handler.ts +++ b/apps/backend/lambdas/projects/handler.ts @@ -42,6 +42,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 }); + } // GET /projects/{id} if (rawPath.startsWith('/') && rawPath.split('/').length === 2 && method === 'GET') { diff --git a/apps/backend/lambdas/projects/openapi.yaml b/apps/backend/lambdas/projects/openapi.yaml index a961f2a..17692f3 100644 --- a/apps/backend/lambdas/projects/openapi.yaml +++ b/apps/backend/lambdas/projects/openapi.yaml @@ -29,6 +29,11 @@ paths: schema: type: string /projects: + get: + summary: GET /projects + responses: + '200': + description: OK post: summary: POST /projects requestBody: @@ -37,7 +42,7 @@ paths: application/json: schema: type: object - required: + required: - name properties: name: @@ -50,6 +55,29 @@ paths: '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}/expenditures: get: summary: GET /projects/{id}/expenditures @@ -68,3 +96,17 @@ paths: type: array items: type: object + + /projects/{id}/donors: + get: + summary: GET /projects/{id}/donors + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + '200': + description: OK + diff --git a/apps/backend/lambdas/projects/package-lock.json b/apps/backend/lambdas/projects/package-lock.json index 7a576a8..2b4b6bc 100644 --- a/apps/backend/lambdas/projects/package-lock.json +++ b/apps/backend/lambdas/projects/package-lock.json @@ -2785,7 +2785,7 @@ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "license": "MIT", "engines": { - "node": ">=0.8.19" + "node": ">=6.0.0" } }, "node_modules/inflight": { @@ -2882,9 +2882,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" @@ -3417,9 +3417,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" @@ -3689,9 +3689,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" 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