diff --git a/apps/backend/lambdas/expenditures/README.md b/apps/backend/lambdas/expenditures/README.md index be4c09f0..cf1ca79e 100644 --- a/apps/backend/lambdas/expenditures/README.md +++ b/apps/backend/lambdas/expenditures/README.md @@ -9,6 +9,7 @@ Lambda for tracking project expenditures. | Method | Path | Description | |--------|------|-------------| | GET | /health | Health check | +| GET | /expenditures | | | POST | /expenditures | | ## Setup diff --git a/apps/backend/lambdas/expenditures/handler.ts b/apps/backend/lambdas/expenditures/handler.ts index 8d00915f..bc6338b0 100644 --- a/apps/backend/lambdas/expenditures/handler.ts +++ b/apps/backend/lambdas/expenditures/handler.ts @@ -19,7 +19,68 @@ export const handler = async (event: any): Promise => { // >>> ROUTES-START (do not remove this marker) // CLI-generated routes will be inserted here - + + // GET /expenditures + if ((normalizedPath === '/expenditures' || normalizedPath === '' || normalizedPath === '/') && method === 'GET') { + const authContext = await authenticateRequest(event); + if (!authContext.isAuthenticated) { + return json(401, { message: 'Authentication required' }); + } + + const queryParams = event.queryStringParameters || {}; + const pageStr = queryParams.page as string | undefined; + const limitStr = queryParams.limit as string | undefined; + const projectIdStr = queryParams.projectId as string | undefined; + + if (pageStr !== undefined) { + if (!/^\d+$/.test(pageStr) || parseInt(pageStr, 10) < 1) { + return json(400, { message: 'page must be a positive integer' }); + } + } + + if (limitStr !== undefined) { + if (!/^\d+$/.test(limitStr) || parseInt(limitStr, 10) < 1) { + return json(400, { message: 'limit must be a positive integer' }); + } + } + + if (projectIdStr !== undefined) { + if (!/^\d+$/.test(projectIdStr) || parseInt(projectIdStr, 10) < 1) { + return json(400, { message: 'projectId must be a positive integer' }); + } + } + + const page = pageStr ? parseInt(pageStr, 10) : null; + const limit = limitStr ? parseInt(limitStr, 10) : null; + const projectId = projectIdStr ? parseInt(projectIdStr, 10) : null; + + if (page && limit) { + const offset = (page - 1) * limit; + + const totalCount = projectId !== null + ? await db.selectFrom('branch.expenditures').where('project_id', '=', projectId).select(db.fn.count('expenditure_id').as('count')).executeTakeFirst() + : await db.selectFrom('branch.expenditures').select(db.fn.count('expenditure_id').as('count')).executeTakeFirst(); + + const totalItems = Number(totalCount?.count || 0); + const totalPages = Math.ceil(totalItems / limit); + + const expenditures = projectId !== null + ? await db.selectFrom('branch.expenditures').where('project_id', '=', projectId).selectAll().orderBy('spent_on', 'desc').limit(limit).offset(offset).execute() + : await db.selectFrom('branch.expenditures').selectAll().orderBy('spent_on', 'desc').limit(limit).offset(offset).execute(); + + return json(200, { + data: expenditures, + pagination: { page, limit, totalItems, totalPages }, + }); + } + + const expenditures = projectId !== null + ? await db.selectFrom('branch.expenditures').where('project_id', '=', projectId).selectAll().orderBy('spent_on', 'desc').execute() + : await db.selectFrom('branch.expenditures').selectAll().orderBy('spent_on', 'desc').execute(); + + return json(200, { data: expenditures }); + } + // POST /expenditures if ((normalizedPath === '/expenditures' || normalizedPath === '' || normalizedPath === '/') && method === 'POST') { // Authenticate the request @@ -96,7 +157,7 @@ export const handler = async (event: any): Promise => { }, }); } - // <<< ROUTES-END + // <<< ROUTES-END return json(404, { message: 'Not Found', path: normalizedPath, method }); } catch (err) { diff --git a/apps/backend/lambdas/expenditures/openapi.yaml b/apps/backend/lambdas/expenditures/openapi.yaml index 596334d6..6f2b5822 100644 --- a/apps/backend/lambdas/expenditures/openapi.yaml +++ b/apps/backend/lambdas/expenditures/openapi.yaml @@ -20,6 +20,54 @@ paths: type: boolean /expenditures: + get: + summary: GET /expenditures — paginated list + parameters: + - in: query + name: page + schema: + type: integer + minimum: 1 + description: Page number (requires limit) + - in: query + name: limit + schema: + type: integer + minimum: 1 + description: Items per page (requires page) + - in: query + name: projectId + schema: + type: integer + minimum: 1 + description: Filter expenditures by project + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + type: object + pagination: + type: object + properties: + page: + type: integer + limit: + type: integer + totalItems: + type: integer + totalPages: + type: integer + '400': + description: Invalid pagination params + '401': + description: Unauthorized post: summary: POST /expenditures requestBody: diff --git a/apps/backend/lambdas/expenditures/test/expenditures.e2e.test.ts b/apps/backend/lambdas/expenditures/test/expenditures.e2e.test.ts index f59b5e19..22ff2d00 100644 --- a/apps/backend/lambdas/expenditures/test/expenditures.e2e.test.ts +++ b/apps/backend/lambdas/expenditures/test/expenditures.e2e.test.ts @@ -33,11 +33,12 @@ function postEvent(body: Record) { }; } -function getEvent(path: string) { +function getEvent(path: string, queryStringParameters?: Record) { return { rawPath: path, requestContext: { http: { method: 'GET' } }, - headers: {}, + headers: { Authorization: 'Bearer fake-token' }, + queryStringParameters: queryStringParameters ?? {}, }; } @@ -120,6 +121,13 @@ describe('Expenditures integration tests', () => { expect(res.statusCode).toBe(401); expect(JSON.parse(res.body).message).toBe('Authentication required'); }); + + test('401: GET /expenditures rejects unauthenticated request', async () => { + mockAuthenticateRequest.mockResolvedValue({ isAuthenticated: false }); + + const res = await handler(getEvent('/')); + expect(res.statusCode).toBe(401); + }); }); describe('Authorization', () => { @@ -208,4 +216,128 @@ describe('Expenditures integration tests', () => { expect(JSON.parse(res.body).message).toBe('Project not found'); }); }); + + describe('GET /expenditures — list and pagination', () => { + test('200: returns all expenditures with data envelope', async () => { + mockAuthenticateRequest.mockResolvedValue(adminUser); + const res = await handler(getEvent('/')); + expect(res.statusCode).toBe(200); + const body = JSON.parse(res.body); + expect(Array.isArray(body.data)).toBe(true); + expect(body.data.length).toBe(3); + expect(body.pagination).toBeUndefined(); + }); + + test('200: ordered newest first by spent_on', async () => { + mockAuthenticateRequest.mockResolvedValue(adminUser); + const res = await handler(getEvent('/')); + const body = JSON.parse(res.body); + const dates = body.data.map((e: any) => new Date(e.spent_on).getTime()); + expect(dates[0]).toBeGreaterThanOrEqual(dates[1]); + expect(dates[1]).toBeGreaterThanOrEqual(dates[2]); + }); + + test('200: paginated response with page and limit', async () => { + mockAuthenticateRequest.mockResolvedValue(adminUser); + const res = await handler(getEvent('/', { page: '1', limit: '1' })); + expect(res.statusCode).toBe(200); + const body = JSON.parse(res.body); + expect(body.data.length).toBe(1); + expect(body.pagination).toBeDefined(); + expect(body.pagination.page).toBe(1); + expect(body.pagination.limit).toBe(1); + expect(body.pagination.totalItems).toBe(3); + expect(body.pagination.totalPages).toBe(3); + }); + + test('200: page 2 returns second item', async () => { + mockAuthenticateRequest.mockResolvedValue(adminUser); + const res = await handler(getEvent('/', { page: '2', limit: '1' })); + const body = JSON.parse(res.body); + expect(res.statusCode).toBe(200); + expect(body.data.length).toBe(1); + expect(body.pagination.page).toBe(2); + }); + + test('200: limit larger than total returns all items', async () => { + mockAuthenticateRequest.mockResolvedValue(adminUser); + const res = await handler(getEvent('/', { page: '1', limit: '100' })); + const body = JSON.parse(res.body); + expect(res.statusCode).toBe(200); + expect(body.data.length).toBe(3); + expect(body.pagination.totalItems).toBe(3); + expect(body.pagination.totalPages).toBe(1); + }); + + test('200: only page provided returns all without pagination', async () => { + mockAuthenticateRequest.mockResolvedValue(adminUser); + const res = await handler(getEvent('/', { page: '1' })); + const body = JSON.parse(res.body); + expect(res.statusCode).toBe(200); + expect(body.pagination).toBeUndefined(); + expect(body.data.length).toBe(3); + }); + + test('200: only limit provided returns all without pagination', async () => { + mockAuthenticateRequest.mockResolvedValue(adminUser); + const res = await handler(getEvent('/', { limit: '2' })); + const body = JSON.parse(res.body); + expect(res.statusCode).toBe(200); + expect(body.pagination).toBeUndefined(); + expect(body.data.length).toBe(3); + }); + + test('200: filter by projectId returns only matching expenditures', async () => { + mockAuthenticateRequest.mockResolvedValue(adminUser); + const res = await handler(getEvent('/', { projectId: '1' })); + const body = JSON.parse(res.body); + expect(res.statusCode).toBe(200); + expect(body.data.every((e: any) => e.project_id === 1)).toBe(true); + }); + + test('200: projectId filter with pagination', async () => { + mockAuthenticateRequest.mockResolvedValue(adminUser); + const res = await handler(getEvent('/', { projectId: '1', page: '1', limit: '10' })); + const body = JSON.parse(res.body); + expect(res.statusCode).toBe(200); + expect(body.pagination.totalItems).toBe(1); + expect(body.data.every((e: any) => e.project_id === 1)).toBe(true); + }); + + test('400: page=0 returns 400', async () => { + mockAuthenticateRequest.mockResolvedValue(adminUser); + const res = await handler(getEvent('/', { page: '0', limit: '10' })); + expect(res.statusCode).toBe(400); + }); + + test('400: negative page returns 400', async () => { + mockAuthenticateRequest.mockResolvedValue(adminUser); + const res = await handler(getEvent('/', { page: '-1', limit: '10' })); + expect(res.statusCode).toBe(400); + }); + + test('400: non-integer page returns 400', async () => { + mockAuthenticateRequest.mockResolvedValue(adminUser); + const res = await handler(getEvent('/', { page: 'abc', limit: '10' })); + expect(res.statusCode).toBe(400); + }); + + test('400: limit=0 returns 400', async () => { + mockAuthenticateRequest.mockResolvedValue(adminUser); + const res = await handler(getEvent('/', { page: '1', limit: '0' })); + expect(res.statusCode).toBe(400); + }); + + test('400: decimal limit returns 400', async () => { + mockAuthenticateRequest.mockResolvedValue(adminUser); + const res = await handler(getEvent('/', { page: '1', limit: '1.5' })); + expect(res.statusCode).toBe(400); + }); + + test('400: invalid projectId returns 400', async () => { + mockAuthenticateRequest.mockResolvedValue(adminUser); + const res = await handler(getEvent('/', { projectId: 'abc' })); + expect(res.statusCode).toBe(400); + }); + }); }); diff --git a/apps/backend/lambdas/expenditures/test/expenditures.unit.test.ts b/apps/backend/lambdas/expenditures/test/expenditures.unit.test.ts index d269c73a..00fe296e 100644 --- a/apps/backend/lambdas/expenditures/test/expenditures.unit.test.ts +++ b/apps/backend/lambdas/expenditures/test/expenditures.unit.test.ts @@ -27,6 +27,21 @@ function postEvent(body: Record) { }; } +function getEvent(queryStringParameters?: Record) { + return { + rawPath: '/', + requestContext: { + http: { + method: 'GET', + }, + }, + headers: { + Authorization: 'Bearer fake-token', + }, + queryStringParameters: queryStringParameters ?? {}, + }; +} + // Default authenticated admin user const adminAuthContext = { isAuthenticated: true as const, @@ -38,11 +53,21 @@ const adminAuthContext = { }, }; +const fakeExpenditures = [ + { expenditure_id: 3, project_id: 1, amount: '2500', category: 'Supplies', spent_on: new Date('2025-07-12') }, + { expenditure_id: 2, project_id: 2, amount: '3000', category: 'Equipment', spent_on: new Date('2025-04-05') }, + { expenditure_id: 1, project_id: 1, amount: '5000', category: 'Travel', spent_on: new Date('2025-02-10') }, +]; + describe('POST /expenditures unit tests', () => { beforeEach(() => { jest.clearAllMocks(); // Default: requests are from an authenticated admin mockAuthenticateRequest.mockResolvedValue(adminAuthContext); + // db.fn is used by GET pagination queries + mockDb.fn = { + count: jest.fn().mockReturnValue({ as: jest.fn().mockReturnValue('count') }), + }; }); describe('Authentication & Authorization', () => { @@ -422,4 +447,83 @@ describe('POST /expenditures unit tests', () => { expect(json.message).toBe('Internal Server Error'); }); }); + + describe('GET /expenditures unit tests', () => { + test('401: unauthenticated GET is rejected', async () => { + mockAuthenticateRequest.mockResolvedValue({ isAuthenticated: false }); + const res = await handler(getEvent()); + expect(res.statusCode).toBe(401); + }); + + test('200: returns data array without pagination when no params', async () => { + mockDb.selectFrom.mockReturnValue({ + selectAll: jest.fn().mockReturnValue({ + orderBy: jest.fn().mockReturnValue({ + execute: jest.fn().mockReturnValue(fakeExpenditures as any), + }), + }), + }); + + const res = await handler(getEvent()); + expect(res.statusCode).toBe(200); + const json = JSON.parse(res.body); + expect(Array.isArray(json.data)).toBe(true); + expect(json.pagination).toBeUndefined(); + }); + + test('200: returns paginated response with page and limit', async () => { + // count query + mockDb.selectFrom.mockReturnValueOnce({ + select: jest.fn().mockReturnValue({ + executeTakeFirst: jest.fn().mockReturnValue({ count: '3' } as any), + }), + }); + // data query + mockDb.selectFrom.mockReturnValueOnce({ + selectAll: jest.fn().mockReturnValue({ + orderBy: jest.fn().mockReturnValue({ + limit: jest.fn().mockReturnValue({ + offset: jest.fn().mockReturnValue({ + execute: jest.fn().mockReturnValue([fakeExpenditures[0]] as any), + }), + }), + }), + }), + }); + + const res = await handler(getEvent({ page: '1', limit: '1' })); + expect(res.statusCode).toBe(200); + const json = JSON.parse(res.body); + expect(json.pagination).toBeDefined(); + expect(json.pagination.page).toBe(1); + expect(json.pagination.limit).toBe(1); + expect(json.pagination.totalItems).toBe(3); + expect(json.pagination.totalPages).toBe(3); + }); + + test('400: page=0 returns 400', async () => { + const res = await handler(getEvent({ page: '0', limit: '10' })); + expect(res.statusCode).toBe(400); + }); + + test('400: non-integer page returns 400', async () => { + const res = await handler(getEvent({ page: 'abc', limit: '10' })); + expect(res.statusCode).toBe(400); + }); + + test('400: limit=0 returns 400', async () => { + const res = await handler(getEvent({ page: '1', limit: '0' })); + expect(res.statusCode).toBe(400); + }); + + test('400: decimal limit returns 400', async () => { + const res = await handler(getEvent({ page: '1', limit: '2.5' })); + expect(res.statusCode).toBe(400); + }); + + test('400: invalid projectId returns 400', async () => { + const res = await handler(getEvent({ projectId: '-5' })); + expect(res.statusCode).toBe(400); + }); + }); }); diff --git a/apps/backend/lambdas/reports/README.md b/apps/backend/lambdas/reports/README.md index dbc5c0fe..1036d293 100644 --- a/apps/backend/lambdas/reports/README.md +++ b/apps/backend/lambdas/reports/README.md @@ -9,6 +9,7 @@ Lambda for generating reports. | Method | Path | Description | |--------|------|-------------| | GET | /health | Health check | +| GET | /reports | | ## Setup diff --git a/apps/backend/lambdas/reports/auth.ts b/apps/backend/lambdas/reports/auth.ts new file mode 100644 index 00000000..7f266db2 --- /dev/null +++ b/apps/backend/lambdas/reports/auth.ts @@ -0,0 +1,90 @@ +import { CognitoJwtVerifier } from 'aws-jwt-verify'; +import db from './db'; + +// Load from environment variables +const COGNITO_USER_POOL_ID = process.env.COGNITO_USER_POOL_ID || ''; +const COGNITO_CLIENT_ID = process.env.COGNITO_CLIENT_ID || ''; + +// Create verifier instance lazily (only when needed) +let verifier: any = null; + +function getVerifier() { + if (!verifier) { + if (!COGNITO_USER_POOL_ID) { + throw new Error('COGNITO_USER_POOL_ID environment variable is not set'); + } + verifier = CognitoJwtVerifier.create({ + userPoolId: COGNITO_USER_POOL_ID, + tokenUse: 'access', + clientId: COGNITO_CLIENT_ID, + }); + } + return verifier; +} + +export interface AuthenticatedUser { + cognitoSub: string; + userId?: number; + email?: string; + isAdmin: boolean; + cognitoGroups?: string[]; +} + +export interface AuthContext { + user?: AuthenticatedUser; + isAuthenticated: boolean; +} + +function extractToken(event: any): string | null { + const authHeader = event.headers?.Authorization || event.headers?.authorization; + + if (!authHeader) { + return null; + } + + const parts = authHeader.split(' '); + if (parts.length === 2 && parts[0].toLowerCase() === 'bearer') { + return parts[1]; + } + + return authHeader; +} + +export async function authenticateRequest(event: any): Promise { + const token = extractToken(event); + + if (!token) { + return { isAuthenticated: false }; + } + + try { + const payload = await getVerifier().verify(token); + + const dbUser = await db + .selectFrom('branch.users') + .where('cognito_sub', '=', payload.sub) + .selectAll() + .executeTakeFirst(); + + if (!dbUser) { + return { isAuthenticated: false }; + } + + const user: AuthenticatedUser = { + cognitoSub: payload.sub, + userId: dbUser.user_id, + email: payload.email as string | undefined, + isAdmin: dbUser.is_admin === true, + cognitoGroups: payload['cognito:groups'] as string[] | undefined, + }; + + if (user.cognitoGroups?.includes('Admins')) { + user.isAdmin = true; + } + + return { user, isAuthenticated: true }; + } catch (error) { + console.error('Token verification failed:', error); + return { isAuthenticated: false }; + } +} diff --git a/apps/backend/lambdas/reports/db-types.d.ts b/apps/backend/lambdas/reports/db-types.d.ts new file mode 100644 index 00000000..f5ce7754 --- /dev/null +++ b/apps/backend/lambdas/reports/db-types.d.ts @@ -0,0 +1,87 @@ +/** + * This file was generated by kysely-codegen. + * Please do not edit it manually. + */ + +import type { ColumnType } from "kysely"; + +export type Generated = T extends ColumnType + ? ColumnType + : ColumnType; + +export type Numeric = ColumnType; + +export type Timestamp = ColumnType; + +export interface BranchDonors { + contact_email: string | null; + contact_name: string | null; + created_at: Generated; + donor_id: Generated; + organization: string; +} + +export interface BranchExpenditures { + amount: Numeric; + category: string | null; + created_at: Generated; + description: string | null; + entered_by: number | null; + expenditure_id: Generated; + project_id: number; + spent_on: Generated; +} + +export interface BranchProjectDonations { + amount: Numeric; + donated_at: Generated; + donation_id: Generated; + donor_id: number; + project_id: number; +} + +export interface BranchProjectMemberships { + hours: Numeric | null; + membership_id: Generated; + project_id: number; + role: string; + start_date: Timestamp | null; + user_id: number; +} + +export interface BranchProjects { + created_at: Generated; + currency: Generated; + end_date: Timestamp | null; + description: string; + name: string; + project_id: Generated; + start_date: Timestamp | null; + total_budget: Numeric | null; +} + +export interface BranchReports { + date_created: Generated; + object_url: string; + project_id: number; + report_id: Generated; +} + +export interface BranchUsers { + cognito_sub: string | null; + created_at: Generated; + email: string; + is_admin: Generated; + name: string; + user_id: Generated; +} + +export interface DB { + "branch.donors": BranchDonors; + "branch.expenditures": BranchExpenditures; + "branch.project_donations": BranchProjectDonations; + "branch.project_memberships": BranchProjectMemberships; + "branch.projects": BranchProjects; + "branch.reports": BranchReports; + "branch.users": BranchUsers; +} diff --git a/apps/backend/lambdas/reports/db.ts b/apps/backend/lambdas/reports/db.ts new file mode 100644 index 00000000..43a33d19 --- /dev/null +++ b/apps/backend/lambdas/reports/db.ts @@ -0,0 +1,18 @@ +import { Kysely, PostgresDialect } from 'kysely' +import { Pool } from 'pg' +import type { DB } from './db-types' + +const db = new Kysely({ + dialect: new PostgresDialect({ + pool: new Pool({ + host: process.env.DB_HOST ?? 'localhost', + port: Number(process.env.DB_PORT ?? 5432), + user: process.env.DB_USER ?? 'branch_dev', + password: process.env.DB_PASSWORD ?? 'password', + database: process.env.DB_NAME ?? 'branch_db', + ssl: false, + }), + }), +}) + +export default db diff --git a/apps/backend/lambdas/reports/handler.ts b/apps/backend/lambdas/reports/handler.ts index 4ad00a17..11109d2b 100644 --- a/apps/backend/lambdas/reports/handler.ts +++ b/apps/backend/lambdas/reports/handler.ts @@ -1,4 +1,6 @@ -import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; +import { APIGatewayProxyResult } from 'aws-lambda'; +import db from './db'; +import { authenticateRequest } from './auth'; export const handler = async (event: any): Promise => { try { @@ -16,6 +18,67 @@ export const handler = async (event: any): Promise => { // >>> ROUTES-START (do not remove this marker) // CLI-generated routes will be inserted here + + // GET /reports + if ((normalizedPath === '/reports' || normalizedPath === '' || normalizedPath === '/') && method === 'GET') { + const authContext = await authenticateRequest(event); + if (!authContext.isAuthenticated) { + return json(401, { message: 'Authentication required' }); + } + + const queryParams = event.queryStringParameters || {}; + const pageStr = queryParams.page as string | undefined; + const limitStr = queryParams.limit as string | undefined; + const projectIdStr = queryParams.projectId as string | undefined; + + if (pageStr !== undefined) { + if (!/^\d+$/.test(pageStr) || parseInt(pageStr, 10) < 1) { + return json(400, { message: 'page must be a positive integer' }); + } + } + + if (limitStr !== undefined) { + if (!/^\d+$/.test(limitStr) || parseInt(limitStr, 10) < 1) { + return json(400, { message: 'limit must be a positive integer' }); + } + } + + if (projectIdStr !== undefined) { + if (!/^\d+$/.test(projectIdStr) || parseInt(projectIdStr, 10) < 1) { + return json(400, { message: 'projectId must be a positive integer' }); + } + } + + const page = pageStr ? parseInt(pageStr, 10) : null; + const limit = limitStr ? parseInt(limitStr, 10) : null; + const projectId = projectIdStr ? parseInt(projectIdStr, 10) : null; + + if (page && limit) { + const offset = (page - 1) * limit; + + const totalCount = projectId !== null + ? await db.selectFrom('branch.reports').where('project_id', '=', projectId).select(db.fn.count('report_id').as('count')).executeTakeFirst() + : await db.selectFrom('branch.reports').select(db.fn.count('report_id').as('count')).executeTakeFirst(); + + const totalItems = Number(totalCount?.count || 0); + const totalPages = Math.ceil(totalItems / limit); + + const reports = projectId !== null + ? await db.selectFrom('branch.reports').where('project_id', '=', projectId).selectAll().orderBy('date_created', 'desc').limit(limit).offset(offset).execute() + : await db.selectFrom('branch.reports').selectAll().orderBy('date_created', 'desc').limit(limit).offset(offset).execute(); + + return json(200, { + data: reports, + pagination: { page, limit, totalItems, totalPages }, + }); + } + + const reports = projectId !== null + ? await db.selectFrom('branch.reports').where('project_id', '=', projectId).selectAll().orderBy('date_created', 'desc').execute() + : await db.selectFrom('branch.reports').selectAll().orderBy('date_created', 'desc').execute(); + + return json(200, { data: reports }); + } // <<< ROUTES-END return json(404, { message: 'Not Found', path: normalizedPath, method }); diff --git a/apps/backend/lambdas/reports/jest.config.js b/apps/backend/lambdas/reports/jest.config.js new file mode 100644 index 00000000..3ed2e70d --- /dev/null +++ b/apps/backend/lambdas/reports/jest.config.js @@ -0,0 +1,17 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/*.test.ts'], + extensionsToTreatAsEsm: ['.ts'], + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + useESM: true, + }, + ], + }, +}; diff --git a/apps/backend/lambdas/reports/openapi.yaml b/apps/backend/lambdas/reports/openapi.yaml index 254c1033..827ddbfa 100644 --- a/apps/backend/lambdas/reports/openapi.yaml +++ b/apps/backend/lambdas/reports/openapi.yaml @@ -18,3 +18,63 @@ paths: properties: ok: type: boolean + + /reports: + get: + summary: GET /reports — paginated list + parameters: + - in: query + name: page + schema: + type: integer + minimum: 1 + description: Page number (requires limit) + - in: query + name: limit + schema: + type: integer + minimum: 1 + description: Items per page (requires page) + - in: query + name: projectId + schema: + type: integer + minimum: 1 + description: Filter reports by project + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + type: object + properties: + report_id: + type: integer + project_id: + type: integer + object_url: + type: string + date_created: + type: string + format: date + pagination: + type: object + properties: + page: + type: integer + limit: + type: integer + totalItems: + type: integer + totalPages: + type: integer + '400': + description: Invalid pagination params + '401': + description: Unauthorized diff --git a/apps/backend/lambdas/reports/package-lock.json b/apps/backend/lambdas/reports/package-lock.json index 32a160b6..7fa2b14e 100644 --- a/apps/backend/lambdas/reports/package-lock.json +++ b/apps/backend/lambdas/reports/package-lock.json @@ -8,12 +8,21 @@ "name": "lambda-local", "version": "1.0.0", "dependencies": { - "jest": "^30.2.0" + "aws-jwt-verify": "^5.1.1", + "aws-lambda": "^1.0.7", + "jest": "^30.2.0", + "kysely": "^0.28.8", + "pg": "^8.16.3" }, "devDependencies": { + "@jest/globals": "^30.2.0", "@types/aws-lambda": "^8.10.131", + "@types/jest": "^30.0.0", "@types/node": "^20.11.30", + "@types/pg": "^8.15.6", "js-yaml": "^4.1.0", + "start-server-and-test": "^2.1.1", + "ts-jest": "^29.4.5", "ts-node": "^10.9.2", "typescript": "^5.4.5" } @@ -483,7 +492,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" @@ -496,7 +505,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", @@ -534,6 +543,60 @@ "tslib": "^2.4.0" } }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.6.tgz", + "integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1025,32 +1088,39 @@ "@sinonjs/commons": "^3.0.1" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@tsconfig/node10": { "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": { @@ -1135,6 +1205,17 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, "node_modules/@types/node": { "version": "20.19.23", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz", @@ -1144,6 +1225,18 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -1425,7 +1518,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" @@ -1438,7 +1531,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" @@ -1506,7 +1599,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": { @@ -1516,6 +1609,109 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-jwt-verify": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/aws-jwt-verify/-/aws-jwt-verify-5.1.1.tgz", + "integrity": "sha512-j6whGdGJmQ27agk4ijY8RPv6itb8JLb7SCJ86fEnneTcSBrpxuwL8kLq6y5WVH95aIknyAloEqAsaOLS1J8ITQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/aws-lambda": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/aws-lambda/-/aws-lambda-1.0.7.tgz", + "integrity": "sha512-9GNFMRrEMG5y3Jvv+V4azWvc+qNWdWLTjDdhf/zgMlz8haaaLWv0xeAIWxz9PuWUBawsVxy0zZotjCdR3Xq+2w==", + "license": "MIT", + "dependencies": { + "aws-sdk": "^2.814.0", + "commander": "^3.0.2", + "js-yaml": "^3.14.1", + "watchpack": "^2.0.0-beta.10" + }, + "bin": { + "lambda": "bin/lambda" + } + }, + "node_modules/aws-lambda/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/aws-lambda/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/aws-sdk": { + "version": "2.1693.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1693.0.tgz", + "integrity": "sha512-cJmb8xEnVLT+R6fBS5sn/EFJiX7tUnDaPtOPZ1vFbOJtd0fnZn/Ky2XGgsvvoeliWeH7mL3TWSX5zXXGSQV6gQ==", + "deprecated": "The AWS SDK for JavaScript (v2) has reached end-of-support, and no longer receives updates. Please migrate your code to use AWS SDK for JavaScript (v3). More info https://a.co/cUPnyil", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-jest": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", @@ -1616,6 +1812,26 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.8.19", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.19.tgz", @@ -1625,6 +1841,13 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -1679,6 +1902,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -1688,12 +1924,70 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1757,6 +2051,16 @@ "node": ">=10" } }, + "node_modules/check-more-types": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", + "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/ci-info": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", @@ -1884,6 +2188,25 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", + "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==", + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1900,7 +2223,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": { @@ -1957,6 +2280,33 @@ "node": ">=0.10.0" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -1970,12 +2320,33 @@ "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" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true, + "license": "MIT" + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2015,6 +2386,52 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2046,6 +2463,31 @@ "node": ">=4" } }, + "node_modules/event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + } + }, + "node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "license": "MIT", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -2141,21 +2583,81 @@ "node": ">=8" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", "engines": { - "node": ">=14" + "node": ">=4.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", + "dev": true, + "license": "MIT" }, "node_modules/fs.realpath": { "version": "1.0.0", @@ -2177,6 +2679,24 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2195,6 +2715,30 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -2204,6 +2748,19 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -2236,12 +2793,52 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2251,6 +2848,57 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -2266,6 +2914,12 @@ "node": ">=10.17.0" } }, + "node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "license": "BSD-3-Clause" + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -2311,12 +2965,40 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2335,6 +3017,25 @@ "node": ">=6" } }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -2344,6 +3045,24 @@ "node": ">=0.12.0" } }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -2356,6 +3075,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3032,6 +3772,34 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/joi": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.1.1.tgz", + "integrity": "sha512-pJkBiPtNo+o0h19LfSvUN46Y5zY+ck99AtHwch9n2HqVLNRgP0ZMyIH8FRMoP+HV8hy/+AG99dXFfwpf83iZfQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3081,6 +3849,25 @@ "node": ">=6" } }, + "node_modules/kysely": { + "version": "0.28.14", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.14.tgz", + "integrity": "sha512-SU3lgh0rPvq7upc6vvdVrCsSMUG1h3ChvHVOY7wJ2fw4C9QEB7X3d5eyYEyULUX7UQtxZJtZXGuT6U2US72UYA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "> 0.8" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -3108,6 +3895,20 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3148,7 +3949,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": { @@ -3160,6 +3961,21 @@ "tmpl": "1.0.5" } }, + "node_modules/map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", + "dev": true + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -3179,6 +3995,29 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -3203,6 +4042,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -3239,6 +4088,13 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "license": "MIT" }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -3420,6 +4276,108 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "dev": true, + "license": [ + "MIT", + "Apache2" + ], + "dependencies": { + "through": "~2.3" + } + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "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==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3459,6 +4417,54 @@ "node": ">=8" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.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==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pretty-format": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", @@ -3485,6 +4491,35 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ps-tree": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", + "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-stream": "=3.3.4" + }, + "bin": { + "ps-tree": "bin/ps-tree.js" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", + "license": "MIT" + }, "node_modules/pure-rand": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", @@ -3501,6 +4536,15 @@ ], "license": "MIT" }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -3537,6 +4581,39 @@ "node": ">=8" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", + "license": "ISC" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -3546,6 +4623,23 @@ "semver": "bin/semver.js" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3607,6 +4701,28 @@ "source-map": "^0.6.0" } }, + "node_modules/split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "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", @@ -3625,6 +4741,48 @@ "node": ">=10" } }, + "node_modules/start-server-and-test": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.1.5.tgz", + "integrity": "sha512-A/SbXpgXE25ScSkpLLqvGvVZT0ykN6+AzS8tVqMBCTxbJy2Nwuen59opT+afalK5aS+AuQmZs0EsLwjnuDN+/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "arg": "^5.0.2", + "bluebird": "3.7.2", + "check-more-types": "2.24.0", + "debug": "4.4.3", + "execa": "5.1.1", + "lazy-ass": "1.6.0", + "ps-tree": "1.2.0", + "wait-on": "9.0.4" + }, + "bin": { + "server-test": "src/bin/start.js", + "start-server-and-test": "src/bin/start.js", + "start-test": "src/bin/start.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/start-server-and-test/node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "~0.1.1" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -3869,6 +5027,13 @@ "node": "*" } }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -3887,11 +5052,90 @@ "node": ">=8.0" } }, + "node_modules/ts-jest": { + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ts-node": { "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", @@ -3935,8 +5179,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true + "devOptional": true, + "license": "0BSD" }, "node_modules/type-detect": { "version": "4.0.8", @@ -3963,7 +5207,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", @@ -3973,6 +5217,20 @@ "node": ">=14.17" } }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -4043,11 +5301,43 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "license": "MIT", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "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": { @@ -4064,6 +5354,26 @@ "node": ">=10.12.0" } }, + "node_modules/wait-on": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.4.tgz", + "integrity": "sha512-k8qrgfwrPVJXTeFY8tl6BxVHiclK11u72DVKhpybHfUL/K6KM4bdyK9EhIVYGytB5MJe/3lq4Tf0hrjM+pvJZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.13.5", + "joi": "^18.0.2", + "lodash": "^4.17.23", + "minimist": "^1.2.8", + "rxjs": "^7.8.2" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -4073,6 +5383,19 @@ "makeerror": "1.0.12" } }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4088,6 +5411,34 @@ "node": ">= 8" } }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -4195,6 +5546,37 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -4282,7 +5664,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/reports/package.json b/apps/backend/lambdas/reports/package.json index 65a04d21..590c0e5c 100644 --- a/apps/backend/lambdas/reports/package.json +++ b/apps/backend/lambdas/reports/package.json @@ -6,17 +6,27 @@ "scripts": { "dev": "ts-node --transpile-only dev-server.ts", "build": "tsc", - "test": "jest", + "test": "jest --forceExit", + "test:e2e": "start-server-and-test dev http-get://localhost:3000/reports/health 'jest --testMatch=\"**/*.e2e.test.ts\" --forceExit'", "package": "npm run build && cd dist && zip -r ../lambda.zip . -x '*.map' 'dev-server.*' 'swagger-utils.*'" }, "devDependencies": { + "@jest/globals": "^30.2.0", "@types/aws-lambda": "^8.10.131", + "@types/jest": "^30.0.0", "@types/node": "^20.11.30", + "@types/pg": "^8.15.6", + "js-yaml": "^4.1.0", + "start-server-and-test": "^2.1.1", + "ts-jest": "^29.4.5", "ts-node": "^10.9.2", - "typescript": "^5.4.5", - "js-yaml": "^4.1.0" + "typescript": "^5.4.5" }, "dependencies": { - "jest":"^30.2.0" + "aws-jwt-verify": "^5.1.1", + "aws-lambda": "^1.0.7", + "jest": "^30.2.0", + "kysely": "^0.28.8", + "pg": "^8.16.3" } } diff --git a/apps/backend/lambdas/reports/test/reports.e2e.test.ts b/apps/backend/lambdas/reports/test/reports.e2e.test.ts new file mode 100644 index 00000000..d5cc9747 --- /dev/null +++ b/apps/backend/lambdas/reports/test/reports.e2e.test.ts @@ -0,0 +1,190 @@ +import { describe, test, expect, beforeEach, afterAll, jest } from '@jest/globals'; +import fs from 'fs'; +import path from 'path'; +import { Pool } from 'pg'; + +jest.mock('../auth'); + +import { handler } from '../handler'; +import { authenticateRequest } from '../auth'; + +const mockAuthenticateRequest = authenticateRequest as jest.MockedFunction; + +const pool = new Pool({ + host: 'localhost', + port: Number(5432), + user: 'branch_dev', + password: 'password', + database: 'branch_db', + ssl: false, +}); + +const seedSqlPath = path.resolve(__dirname, '../../../db/db_setup.sql'); +const seedSql = fs.readFileSync(seedSqlPath, 'utf8'); + +function getEvent(queryStringParameters?: Record) { + return { + rawPath: '/', + requestContext: { http: { method: 'GET' } }, + headers: { Authorization: 'Bearer fake-token' }, + queryStringParameters: queryStringParameters ?? {}, + }; +} + +const adminUser = { + isAuthenticated: true as const, + user: { + cognitoSub: 'admin-sub', + userId: 1, + email: 'ashley@branch.org', + isAdmin: true, + }, +}; + +describe('Reports e2e tests', () => { + beforeEach(async () => { + jest.clearAllMocks(); + mockAuthenticateRequest.mockResolvedValue(adminUser); + + const client = await pool.connect(); + try { + await client.query(seedSql); + } finally { + client.release(); + } + }); + + afterAll(async () => { + await pool.end(); + }); + + describe('Health check', () => { + test('200: health check returns ok', async () => { + const res = await handler({ + rawPath: '/health', + requestContext: { http: { method: 'GET' } }, + headers: {}, + queryStringParameters: {}, + }); + expect(res.statusCode).toBe(200); + const body = JSON.parse(res.body); + expect(body.ok).toBe(true); + }); + }); + + describe('Authentication', () => { + test('401: unauthenticated request is rejected', async () => { + mockAuthenticateRequest.mockResolvedValue({ isAuthenticated: false }); + const res = await handler(getEvent()); + expect(res.statusCode).toBe(401); + }); + }); + + describe('GET /reports — list and pagination', () => { + test('200: returns all reports with data envelope', async () => { + const res = await handler(getEvent()); + expect(res.statusCode).toBe(200); + const body = JSON.parse(res.body); + expect(Array.isArray(body.data)).toBe(true); + expect(body.data.length).toBe(5); // seed has 5 reports + expect(body.pagination).toBeUndefined(); + }); + + test('200: ordered newest first by date_created', async () => { + const res = await handler(getEvent()); + const body = JSON.parse(res.body); + const dates = body.data.map((r: any) => new Date(r.date_created).getTime()); + expect(dates[0]).toBeGreaterThanOrEqual(dates[1]); + }); + + test('200: paginated with page=1 limit=2', async () => { + const res = await handler(getEvent({ page: '1', limit: '2' })); + expect(res.statusCode).toBe(200); + const body = JSON.parse(res.body); + expect(body.data.length).toBe(2); + expect(body.pagination.page).toBe(1); + expect(body.pagination.limit).toBe(2); + expect(body.pagination.totalItems).toBe(5); + expect(body.pagination.totalPages).toBe(3); + }); + + test('200: page=2 limit=2 returns next slice', async () => { + const res = await handler(getEvent({ page: '2', limit: '2' })); + const body = JSON.parse(res.body); + expect(res.statusCode).toBe(200); + expect(body.data.length).toBe(2); + expect(body.pagination.page).toBe(2); + }); + + test('200: limit larger than total returns all', async () => { + const res = await handler(getEvent({ page: '1', limit: '100' })); + const body = JSON.parse(res.body); + expect(res.statusCode).toBe(200); + expect(body.data.length).toBe(5); + expect(body.pagination.totalPages).toBe(1); + }); + + test('200: only page provided returns all without pagination', async () => { + const res = await handler(getEvent({ page: '1' })); + const body = JSON.parse(res.body); + expect(res.statusCode).toBe(200); + expect(body.pagination).toBeUndefined(); + expect(body.data.length).toBe(5); + }); + + test('200: only limit provided returns all without pagination', async () => { + const res = await handler(getEvent({ limit: '3' })); + const body = JSON.parse(res.body); + expect(res.statusCode).toBe(200); + expect(body.pagination).toBeUndefined(); + expect(body.data.length).toBe(5); + }); + + test('200: filter by projectId returns only matching reports', async () => { + const res = await handler(getEvent({ projectId: '1' })); + const body = JSON.parse(res.body); + expect(res.statusCode).toBe(200); + expect(body.data.every((r: any) => r.project_id === 1)).toBe(true); + expect(body.data.length).toBe(1); + }); + + test('200: projectId filter with pagination', async () => { + // project 2 has 2 reports, project 3 has 2 reports in seed data + const res = await handler(getEvent({ projectId: '2', page: '1', limit: '10' })); + const body = JSON.parse(res.body); + expect(res.statusCode).toBe(200); + expect(body.data.every((r: any) => r.project_id === 2)).toBe(true); + expect(body.pagination.totalItems).toBe(2); + }); + + test('400: page=0 returns 400', async () => { + const res = await handler(getEvent({ page: '0', limit: '10' })); + expect(res.statusCode).toBe(400); + }); + + test('400: negative page returns 400', async () => { + const res = await handler(getEvent({ page: '-1', limit: '10' })); + expect(res.statusCode).toBe(400); + }); + + test('400: non-integer page returns 400', async () => { + const res = await handler(getEvent({ page: 'abc', limit: '10' })); + expect(res.statusCode).toBe(400); + }); + + test('400: limit=0 returns 400', async () => { + const res = await handler(getEvent({ page: '1', limit: '0' })); + expect(res.statusCode).toBe(400); + }); + + test('400: decimal limit returns 400', async () => { + const res = await handler(getEvent({ page: '1', limit: '2.5' })); + expect(res.statusCode).toBe(400); + }); + + test('400: invalid projectId returns 400', async () => { + const res = await handler(getEvent({ projectId: 'abc' })); + expect(res.statusCode).toBe(400); + }); + }); +}); diff --git a/apps/backend/lambdas/reports/test/reports.unit.test.ts b/apps/backend/lambdas/reports/test/reports.unit.test.ts new file mode 100644 index 00000000..2e466190 --- /dev/null +++ b/apps/backend/lambdas/reports/test/reports.unit.test.ts @@ -0,0 +1,206 @@ +import { describe, test, expect, beforeEach, jest } from '@jest/globals'; + +jest.mock('../db'); +jest.mock('../auth'); + +import { handler } from '../handler'; +import db from '../db'; +import { authenticateRequest } from '../auth'; + +const mockDb = db as any; +const mockAuthenticateRequest = authenticateRequest as jest.MockedFunction; + +function getEvent(queryStringParameters?: Record) { + return { + rawPath: '/', + requestContext: { + http: { + method: 'GET', + }, + }, + headers: { + Authorization: 'Bearer fake-token', + }, + queryStringParameters: queryStringParameters ?? {}, + }; +} + +const adminAuthContext = { + isAuthenticated: true as const, + user: { + cognitoSub: 'admin-sub', + userId: 1, + email: 'ashley@branch.org', + isAdmin: true, + }, +}; + +const fakeReports = [ + { report_id: 3, project_id: 2, object_url: 'https://s3.amazonaws.com/reports/c.pdf', date_created: new Date('2025-07-01') }, + { report_id: 2, project_id: 1, object_url: 'https://s3.amazonaws.com/reports/b.pdf', date_created: new Date('2025-04-01') }, + { report_id: 1, project_id: 1, object_url: 'https://s3.amazonaws.com/reports/a.pdf', date_created: new Date('2025-01-01') }, +]; + +describe('GET /reports unit tests', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockAuthenticateRequest.mockResolvedValue(adminAuthContext); + // db.fn is used by pagination count queries + mockDb.fn = { + count: jest.fn().mockReturnValue({ as: jest.fn().mockReturnValue('count') }), + }; + }); + + describe('Authentication', () => { + test('401: unauthenticated request is rejected', async () => { + mockAuthenticateRequest.mockResolvedValue({ isAuthenticated: false }); + const res = await handler(getEvent()); + expect(res.statusCode).toBe(401); + expect(JSON.parse(res.body).message).toBe('Authentication required'); + }); + }); + + describe('Health check', () => { + test('200: health check returns ok', async () => { + const res = await handler({ + rawPath: '/health', + requestContext: { http: { method: 'GET' } }, + headers: {}, + queryStringParameters: {}, + }); + expect(res.statusCode).toBe(200); + expect(JSON.parse(res.body).ok).toBe(true); + }); + }); + + describe('Response format', () => { + test('200: returns data array without pagination when no params', async () => { + mockDb.selectFrom.mockReturnValue({ + selectAll: jest.fn().mockReturnValue({ + orderBy: jest.fn().mockReturnValue({ + execute: jest.fn().mockReturnValue(fakeReports as any), + }), + }), + }); + + const res = await handler(getEvent()); + expect(res.statusCode).toBe(200); + const json = JSON.parse(res.body); + expect(Array.isArray(json.data)).toBe(true); + expect(json.pagination).toBeUndefined(); + expect(res.headers?.['Content-Type']).toBe('application/json'); + expect(res.headers?.['Access-Control-Allow-Origin']).toBe('*'); + }); + + test('404: unknown path returns 404', async () => { + const res = await handler({ + rawPath: '/unknown', + requestContext: { http: { method: 'GET' } }, + headers: {}, + queryStringParameters: {}, + }); + expect(res.statusCode).toBe(404); + }); + }); + + describe('Pagination', () => { + test('200: returns paginated response with page and limit', async () => { + // count query + mockDb.selectFrom.mockReturnValueOnce({ + select: jest.fn().mockReturnValue({ + executeTakeFirst: jest.fn().mockReturnValue({ count: '3' } as any), + }), + }); + // data query + mockDb.selectFrom.mockReturnValueOnce({ + selectAll: jest.fn().mockReturnValue({ + orderBy: jest.fn().mockReturnValue({ + limit: jest.fn().mockReturnValue({ + offset: jest.fn().mockReturnValue({ + execute: jest.fn().mockReturnValue([fakeReports[0]] as any), + }), + }), + }), + }), + }); + + const res = await handler(getEvent({ page: '1', limit: '1' })); + expect(res.statusCode).toBe(200); + const json = JSON.parse(res.body); + expect(json.pagination.page).toBe(1); + expect(json.pagination.limit).toBe(1); + expect(json.pagination.totalItems).toBe(3); + expect(json.pagination.totalPages).toBe(3); + expect(json.data.length).toBe(1); + }); + + test('200: only page provided returns all without pagination', async () => { + mockDb.selectFrom.mockReturnValue({ + selectAll: jest.fn().mockReturnValue({ + orderBy: jest.fn().mockReturnValue({ + execute: jest.fn().mockReturnValue(fakeReports as any), + }), + }), + }); + + const res = await handler(getEvent({ page: '1' })); + const json = JSON.parse(res.body); + expect(res.statusCode).toBe(200); + expect(json.pagination).toBeUndefined(); + }); + + test('200: only limit provided returns all without pagination', async () => { + mockDb.selectFrom.mockReturnValue({ + selectAll: jest.fn().mockReturnValue({ + orderBy: jest.fn().mockReturnValue({ + execute: jest.fn().mockReturnValue(fakeReports as any), + }), + }), + }); + + const res = await handler(getEvent({ limit: '2' })); + const json = JSON.parse(res.body); + expect(res.statusCode).toBe(200); + expect(json.pagination).toBeUndefined(); + }); + }); + + describe('Validation', () => { + test('400: page=0 returns 400', async () => { + const res = await handler(getEvent({ page: '0', limit: '10' })); + expect(res.statusCode).toBe(400); + expect(JSON.parse(res.body).message).toContain('page'); + }); + + test('400: negative page returns 400', async () => { + const res = await handler(getEvent({ page: '-1', limit: '10' })); + expect(res.statusCode).toBe(400); + }); + + test('400: non-integer page returns 400', async () => { + const res = await handler(getEvent({ page: 'abc', limit: '10' })); + expect(res.statusCode).toBe(400); + }); + + test('400: limit=0 returns 400', async () => { + const res = await handler(getEvent({ page: '1', limit: '0' })); + expect(res.statusCode).toBe(400); + expect(JSON.parse(res.body).message).toContain('limit'); + }); + + test('400: decimal limit returns 400', async () => { + const res = await handler(getEvent({ page: '1', limit: '1.5' })); + expect(res.statusCode).toBe(400); + }); + + test('400: invalid projectId returns 400', async () => { + const res = await handler(getEvent({ projectId: 'abc' })); + expect(res.statusCode).toBe(400); + }); + + test('400: projectId=0 returns 400', async () => { + const res = await handler(getEvent({ projectId: '0' })); + expect(res.statusCode).toBe(400); + }); + }); +});