From f993c6f913711294dce94f7061e0bec31c3e9b4e Mon Sep 17 00:00:00 2001 From: maotora Date: Mon, 16 Mar 2026 18:15:58 +0300 Subject: [PATCH 1/8] chore: migrate runtime to Prisma 7 --- .env.example | 3 + .eslintrc.js | 24 - .gitignore | 3 + eslint.config.js | 34 + jest.config.cjs | 18 + jest.config.js | 7 - package.json | 59 +- pnpm-lock.yaml | 2294 ++++++++++++++--- prisma.config.ts | 13 + .../20250411175910_cleanup/migration.sql | 32 - .../20260316190000_baseline/migration.sql | 169 ++ prisma/migrations/init/migration.sql | 115 - prisma/schema.prisma | 9 +- prisma/seed.ts | 180 ++ scripts/export-openapi.ts | 10 + server.ts | 48 +- src/app.ts | 50 +- src/config.ts | 19 +- src/db/prisma.ts | 33 + src/docs/swagger.ts | 267 +- src/middleware/errorHandler.ts | 42 +- src/middleware/requestContext.ts | 24 + src/middleware/validation.ts | 2 +- src/routes.ts | 802 +++--- src/types.ts | 52 +- tests/locations-api.test.ts | 167 +- tsconfig.build.json | 9 + tsconfig.json | 20 +- tsconfig.test.json | 7 + 29 files changed, 3375 insertions(+), 1137 deletions(-) create mode 100644 .env.example delete mode 100644 .eslintrc.js create mode 100644 eslint.config.js create mode 100644 jest.config.cjs delete mode 100644 jest.config.js create mode 100644 prisma.config.ts delete mode 100644 prisma/migrations/20250411175910_cleanup/migration.sql create mode 100644 prisma/migrations/20260316190000_baseline/migration.sql delete mode 100644 prisma/migrations/init/migration.sql create mode 100644 prisma/seed.ts create mode 100644 scripts/export-openapi.ts create mode 100644 src/db/prisma.ts create mode 100644 src/middleware/requestContext.ts create mode 100644 tsconfig.build.json create mode 100644 tsconfig.test.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..92c795e --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/locations_api" +PORT="8080" +PAGE_SIZE="10" diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index f6c62be..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,24 +0,0 @@ -module.exports = { - parser: '@typescript-eslint/parser', - parserOptions: { - project: 'tsconfig.json', - sourceType: 'module', - }, - plugins: ['@typescript-eslint/eslint-plugin'], - extends: [ - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', - ], - root: true, - env: { - node: true, - jest: true, - }, - ignorePatterns: ['.eslintrc.js'], - rules: { - '@typescript-eslint/interface-name-prefix': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'off', - }, -}; diff --git a/.gitignore b/.gitignore index ec80321..64764fd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ /dist /node_modules /prisma/node_modules +/src/generated +/generated +/coverage # Logs logs diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..071e9d0 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,34 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: ['coverage/**', 'dist/**', 'node_modules/**', 'src/generated/**'], + }, + js.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + { + files: ['**/*.ts'], + languageOptions: { + globals: { + ...globals.node, + ...globals.jest, + }, + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + '@typescript-eslint/no-misused-promises': 'off', + '@typescript-eslint/no-namespace': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/require-await': 'off', + }, + }, +); diff --git a/jest.config.cjs b/jest.config.cjs new file mode 100644 index 0000000..8d11c9f --- /dev/null +++ b/jest.config.cjs @@ -0,0 +1,18 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + testEnvironment: 'node', + extensionsToTreatAsEsm: ['.ts'], + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + tsconfig: './tsconfig.test.json', + useESM: true, + }, + ], + }, + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + setupFiles: ['dotenv/config'], +}; diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index fa584a2..0000000 --- a/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -/** @type {import('ts-jest').JestConfigWithTsJest} **/ -module.exports = { - testEnvironment: "node", - transform: { - "^.+\.tsx?$": ["ts-jest",{}], - }, -}; diff --git a/package.json b/package.json index 77c5199..00b959b 100644 --- a/package.json +++ b/package.json @@ -2,19 +2,25 @@ "name": "locations_api", "version": "2.0.0", "description": "Tanzania Locations API is a gateway that provides information about various locations in Tanzania, including regions, districts, wards, and places.", + "type": "module", "main": "./dist/server.js", "engines": { - "node": ">=18.0.0" + "node": ">=20.19.0" }, "scripts": { - "dev": "node --experimental-strip-types --watch ./server.ts", - "generate": "prisma generate --no-engine", - "generate:ci": "prisma generate", - "build:ci": "npm run generate:ci && tsc --noEmit", - "build": "npm run generate && tsc", + "dev": "tsx watch server.ts", + "generate": "prisma generate", + "db:migrate": "prisma migrate deploy", + "db:seed": "prisma db seed", + "lint": "pnpm generate && eslint server.ts \"src/**/*.ts\" \"tests/**/*.ts\" \"scripts/**/*.ts\" \"prisma/**/*.ts\"", + "typecheck": "pnpm generate && tsc --noEmit", + "build:ci": "pnpm generate && pnpm lint && pnpm typecheck && pnpm build", + "build": "pnpm generate && tsc -p tsconfig.build.json", "start": "node ./dist/server.js", - "test": "jest", - "test:watch": "jest --watch" + "test": "pnpm generate && NODE_OPTIONS=--experimental-vm-modules jest --runInBand", + "test:ci": "pnpm generate && pnpm db:migrate && pnpm db:seed && NODE_OPTIONS=--experimental-vm-modules jest --runInBand", + "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch", + "openapi:json": "tsx scripts/export-openapi.ts" }, "keywords": [ "Tanzania", @@ -29,30 +35,37 @@ "license": "copyleft", "packageManager": "pnpm@10.7.0", "dependencies": { - "@prisma/client": "^6.6.0", - "body-parser": "^2.2.0", - "cors": "^2.8.5", - "dotenv": "^16.5.0", - "express": "^5.1.0", + "@prisma/adapter-pg": "^7.5.0", + "@prisma/client": "^7.5.0", + "cors": "^2.8.6", + "dotenv": "^17.3.1", + "express": "^5.2.1", "helmet": "^8.1.0", - "morgan": "^1.10.0", + "morgan": "^1.10.1", + "pg": "^8.20.0", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", "zod": "^3.24.2" }, "devDependencies": { - "@types/cors": "^2.8.17", - "@types/express": "^5.0.1", + "@eslint/js": "^9.39.4", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", "@types/jest": "^29.5.14", - "@types/morgan": "^1.9.9", - "@types/node": "^22.14.0", - "@types/supertest": "^6.0.3", + "@types/morgan": "^1.9.10", + "@types/node": "^25.5.0", + "@types/pg": "^8.15.6", + "@types/supertest": "^7.0.0", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.8", + "eslint": "^9.39.4", + "globals": "^15.15.0", "jest": "^29.7.0", - "prisma": "^6.6.0", - "supertest": "^7.1.0", - "ts-jest": "^29.3.1", - "typescript": "^5.8.3" + "prisma": "^7.5.0", + "supertest": "^7.2.2", + "ts-jest": "^29.4.6", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.57.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 849e22f..011b29b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,76 +8,97 @@ importers: .: dependencies: + '@prisma/adapter-pg': + specifier: ^7.5.0 + version: 7.5.0 '@prisma/client': - specifier: ^6.6.0 - version: 6.6.0(prisma@6.6.0(typescript@5.8.3))(typescript@5.8.3) - body-parser: - specifier: ^2.2.0 - version: 2.2.0 + specifier: ^7.5.0 + version: 7.5.0(prisma@7.5.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3) cors: - specifier: ^2.8.5 - version: 2.8.5 + specifier: ^2.8.6 + version: 2.8.6 dotenv: - specifier: ^16.5.0 - version: 16.5.0 + specifier: ^17.3.1 + version: 17.3.1 express: - specifier: ^5.1.0 - version: 5.1.0 + specifier: ^5.2.1 + version: 5.2.1 helmet: specifier: ^8.1.0 version: 8.1.0 morgan: - specifier: ^1.10.0 - version: 1.10.0 + specifier: ^1.10.1 + version: 1.10.1 + pg: + specifier: ^8.20.0 + version: 8.20.0 swagger-jsdoc: specifier: ^6.2.8 version: 6.2.8(openapi-types@12.1.3) swagger-ui-express: specifier: ^5.0.1 - version: 5.0.1(express@5.1.0) + version: 5.0.1(express@5.2.1) zod: specifier: ^3.24.2 version: 3.24.2 devDependencies: + '@eslint/js': + specifier: ^9.39.4 + version: 9.39.4 '@types/cors': - specifier: ^2.8.17 - version: 2.8.17 + specifier: ^2.8.19 + version: 2.8.19 '@types/express': - specifier: ^5.0.1 - version: 5.0.1 + specifier: ^5.0.6 + version: 5.0.6 '@types/jest': specifier: ^29.5.14 version: 29.5.14 '@types/morgan': - specifier: ^1.9.9 - version: 1.9.9 + specifier: ^1.9.10 + version: 1.9.10 '@types/node': - specifier: ^22.14.0 - version: 22.14.0 + specifier: ^25.5.0 + version: 25.5.0 + '@types/pg': + specifier: ^8.15.6 + version: 8.18.0 '@types/supertest': - specifier: ^6.0.3 - version: 6.0.3 + specifier: ^7.0.0 + version: 7.2.0 '@types/swagger-jsdoc': specifier: ^6.0.4 version: 6.0.4 '@types/swagger-ui-express': specifier: ^4.1.8 version: 4.1.8 + eslint: + specifier: ^9.39.4 + version: 9.39.4(jiti@2.6.1) + globals: + specifier: ^15.15.0 + version: 15.15.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.14.0) + version: 29.7.0(@types/node@25.5.0) prisma: - specifier: ^6.6.0 - version: 6.6.0(typescript@5.8.3) + specifier: ^7.5.0 + version: 7.5.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) supertest: - specifier: ^7.1.0 - version: 7.1.0 + specifier: ^7.2.2 + version: 7.2.2 ts-jest: - specifier: ^29.3.1 - version: 29.3.1(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(esbuild@0.25.2)(jest@29.7.0(@types/node@22.14.0))(typescript@5.8.3) + specifier: ^29.4.6 + version: 29.4.6(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(jest-util@29.7.0)(jest@29.7.0(@types/node@25.5.0))(typescript@5.9.3) + tsx: + specifier: ^4.21.0 + version: 4.21.0 typescript: - specifier: ^5.8.3 - version: 5.8.3 + specifier: ^5.9.3 + version: 5.9.3 + typescript-eslint: + specifier: ^8.57.0 + version: 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) packages: @@ -261,156 +282,248 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - '@esbuild/aix-ppc64@0.25.2': - resolution: {integrity: sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==} + '@chevrotain/cst-dts-gen@10.5.0': + resolution: {integrity: sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==} + + '@chevrotain/gast@10.5.0': + resolution: {integrity: sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==} + + '@chevrotain/types@10.5.0': + resolution: {integrity: sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==} + + '@chevrotain/utils@10.5.0': + resolution: {integrity: sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==} + + '@electric-sql/pglite-socket@0.0.20': + resolution: {integrity: sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg==} + hasBin: true + peerDependencies: + '@electric-sql/pglite': 0.3.15 + + '@electric-sql/pglite-tools@0.2.20': + resolution: {integrity: sha512-BK50ZnYa3IG7ztXhtgYf0Q7zijV32Iw1cYS8C+ThdQlwx12V5VZ9KRJ42y82Hyb4PkTxZQklVQA9JHyUlex33A==} + peerDependencies: + '@electric-sql/pglite': 0.3.15 + + '@electric-sql/pglite@0.3.15': + resolution: {integrity: sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==} + + '@esbuild/aix-ppc64@0.27.4': + resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.2': - resolution: {integrity: sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==} + '@esbuild/android-arm64@0.27.4': + resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.2': - resolution: {integrity: sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==} + '@esbuild/android-arm@0.27.4': + resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.2': - resolution: {integrity: sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==} + '@esbuild/android-x64@0.27.4': + resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.2': - resolution: {integrity: sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==} + '@esbuild/darwin-arm64@0.27.4': + resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.2': - resolution: {integrity: sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==} + '@esbuild/darwin-x64@0.27.4': + resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.2': - resolution: {integrity: sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==} + '@esbuild/freebsd-arm64@0.27.4': + resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.2': - resolution: {integrity: sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==} + '@esbuild/freebsd-x64@0.27.4': + resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.2': - resolution: {integrity: sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==} + '@esbuild/linux-arm64@0.27.4': + resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.2': - resolution: {integrity: sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==} + '@esbuild/linux-arm@0.27.4': + resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.2': - resolution: {integrity: sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==} + '@esbuild/linux-ia32@0.27.4': + resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.2': - resolution: {integrity: sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==} + '@esbuild/linux-loong64@0.27.4': + resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.2': - resolution: {integrity: sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==} + '@esbuild/linux-mips64el@0.27.4': + resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.2': - resolution: {integrity: sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==} + '@esbuild/linux-ppc64@0.27.4': + resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.2': - resolution: {integrity: sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==} + '@esbuild/linux-riscv64@0.27.4': + resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.2': - resolution: {integrity: sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==} + '@esbuild/linux-s390x@0.27.4': + resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.2': - resolution: {integrity: sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==} + '@esbuild/linux-x64@0.27.4': + resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.2': - resolution: {integrity: sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==} + '@esbuild/netbsd-arm64@0.27.4': + resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.2': - resolution: {integrity: sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==} + '@esbuild/netbsd-x64@0.27.4': + resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.2': - resolution: {integrity: sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==} + '@esbuild/openbsd-arm64@0.27.4': + resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.2': - resolution: {integrity: sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==} + '@esbuild/openbsd-x64@0.27.4': + resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.25.2': - resolution: {integrity: sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==} + '@esbuild/openharmony-arm64@0.27.4': + resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.4': + resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.2': - resolution: {integrity: sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==} + '@esbuild/win32-arm64@0.27.4': + resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.2': - resolution: {integrity: sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==} + '@esbuild/win32-ia32@0.27.4': + resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.2': - resolution: {integrity: sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==} + '@esbuild/win32-x64@0.27.4': + resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@hono/node-server@1.19.9': + resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + '@istanbuljs/load-nyc-config@1.1.0': resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} @@ -506,35 +619,75 @@ packages: '@jsdevtools/ono@7.1.3': resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} - '@prisma/client@6.6.0': - resolution: {integrity: sha512-vfp73YT/BHsWWOAuthKQ/1lBgESSqYqAWZEYyTdGXyFAHpmewwWL2Iz6ErIzkj4aHbuc6/cGSsE6ZY+pBO04Cg==} - engines: {node: '>=18.18'} + '@mrleebo/prisma-ast@0.13.1': + resolution: {integrity: sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==} + engines: {node: '>=16'} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@paralleldrive/cuid2@2.3.1': + resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + + '@prisma/adapter-pg@7.5.0': + resolution: {integrity: sha512-EJx7OLULahcC3IjJgdx2qRDNCT+ToY2v66UkeETMCLhNOTgqVzRzYvOEphY7Zp0eHyzfkC33Edd/qqeadf9R4A==} + + '@prisma/client-runtime-utils@7.5.0': + resolution: {integrity: sha512-KnJ2b4Si/pcWEtK68uM+h0h1oh80CZt2suhLTVuLaSKg4n58Q9jBF/A42Kw6Ma+aThy1yAhfDeTC0JvEmeZnFQ==} + + '@prisma/client@7.5.0': + resolution: {integrity: sha512-h4hF9ctp+kSRs7ENHGsFQmHAgHcfkOCxbYt6Ti9Xi8x7D+kP4tTi9x51UKmiTH/OqdyJAO+8V+r+JA5AWdav7w==} + engines: {node: ^20.19 || ^22.12 || >=24.0} peerDependencies: prisma: '*' - typescript: '>=5.1.0' + typescript: '>=5.4.0' peerDependenciesMeta: prisma: optional: true typescript: optional: true - '@prisma/config@6.6.0': - resolution: {integrity: sha512-d8FlXRHsx72RbN8nA2QCRORNv5AcUnPXgtPvwhXmYkQSMF/j9cKaJg+9VcUzBRXGy9QBckNzEQDEJZdEOZ+ubA==} + '@prisma/config@7.5.0': + resolution: {integrity: sha512-1J/9YEX7A889xM46PYg9e8VAuSL1IUmXJW3tEhMv7XQHDWlfC9YSkIw9sTYRaq5GswGlxZ+GnnyiNsUZ9JJhSQ==} + + '@prisma/debug@7.2.0': + resolution: {integrity: sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==} + + '@prisma/debug@7.5.0': + resolution: {integrity: sha512-163+nffny0JoPEkDhfNco0vcuT3ymIJc9+WX7MHSQhfkeKUmKe9/wqvGk5SjppT93DtBjVwr5HPJYlXbzm6qtg==} - '@prisma/debug@6.6.0': - resolution: {integrity: sha512-DL6n4IKlW5k2LEXzpN60SQ1kP/F6fqaCgU/McgaYsxSf43GZ8lwtmXLke9efS+L1uGmrhtBUP4npV/QKF8s2ZQ==} + '@prisma/dev@0.20.0': + resolution: {integrity: sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ==} - '@prisma/engines-version@6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a': - resolution: {integrity: sha512-JzRaQ5Em1fuEcbR3nUsMNYaIYrOT1iMheenjCvzZblJcjv/3JIuxXN7RCNT5i6lRkLodW5ojCGhR7n5yvnNKrw==} + '@prisma/driver-adapter-utils@7.5.0': + resolution: {integrity: sha512-B79N/amgV677mFesFDBAdrW0OIaqawap9E0sjgLBtzIz2R3hIMS1QB8mLZuUEiS4q5Y8Oh3I25Kw4SLxMypk9Q==} - '@prisma/engines@6.6.0': - resolution: {integrity: sha512-nC0IV4NHh7500cozD1fBoTwTD1ydJERndreIjpZr/S3mno3P6tm8qnXmIND5SwUkibNeSJMpgl4gAnlqJ/gVlg==} + '@prisma/engines-version@7.5.0-15.280c870be64f457428992c43c1f6d557fab6e29e': + resolution: {integrity: sha512-E+iRV/vbJLl8iGjVr6g/TEWokA+gjkV/doZkaQN1i/ULVdDwGnPJDfLUIFGS3BVwlG/m6L8T4x1x5isl8hGMxA==} - '@prisma/fetch-engine@6.6.0': - resolution: {integrity: sha512-Ohfo8gKp05LFLZaBlPUApM0M7k43a0jmo86YY35u1/4t+vuQH9mRGU7jGwVzGFY3v+9edeb/cowb1oG4buM1yw==} + '@prisma/engines@7.5.0': + resolution: {integrity: sha512-ondGRhzoaVpRWvFaQ5wH5zS1BIbhzbKqczKjCn6j3L0Zfe/LInjcEg8+xtB49AuZBX30qyx1ZtGoootUohz2pw==} - '@prisma/get-platform@6.6.0': - resolution: {integrity: sha512-3qCwmnT4Jh5WCGUrkWcc6VZaw0JY7eWN175/pcb5Z6FiLZZ3ygY93UX0WuV41bG51a6JN/oBH0uywJ90Y+V5eA==} + '@prisma/fetch-engine@7.5.0': + resolution: {integrity: sha512-kZCl2FV54qnyrVdnII8MI6qvt7HfU6Cbiz8dZ8PXz4f4lbSw45jEB9/gEMK2SGdiNhBKyk/Wv95uthoLhGMLYA==} + + '@prisma/get-platform@7.2.0': + resolution: {integrity: sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==} + + '@prisma/get-platform@7.5.0': + resolution: {integrity: sha512-7I+2y1nu/gkEKSiHHbcZ1HPe/euGdEqJZxEEMT0246q4De1+hla0ZzlTgvaT9dHcVCgLSuCG8v39db5qUUWNgw==} + + '@prisma/query-plan-executor@7.2.0': + resolution: {integrity: sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==} + + '@prisma/studio-core@0.21.1': + resolution: {integrity: sha512-bOGqG/eMQtKC0XVvcVLRmhWWzm/I+0QUWqAEhEBtetpuS3k3V4IWqKGUONkAIT223DNXJMxMtZp36b1FmcdPeg==} + engines: {node: ^20.19 || ^22.12 || ^24.0, pnpm: '8'} + peerDependencies: + '@types/react': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 '@scarf/scarf@1.4.0': resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} @@ -548,6 +701,9 @@ packages: '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -569,14 +725,17 @@ packages: '@types/cookiejar@2.1.5': resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} - '@types/cors@2.8.17': - resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} '@types/express-serve-static-core@5.0.6': resolution: {integrity: sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==} - '@types/express@5.0.1': - resolution: {integrity: sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==} + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} @@ -605,11 +764,17 @@ packages: '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - '@types/morgan@1.9.9': - resolution: {integrity: sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==} + '@types/morgan@1.9.10': + resolution: {integrity: sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==} + + '@types/node@25.5.0': + resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} - '@types/node@22.14.0': - resolution: {integrity: sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==} + '@types/pg@8.11.11': + resolution: {integrity: sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw==} + + '@types/pg@8.18.0': + resolution: {integrity: sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==} '@types/qs@6.9.18': resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==} @@ -617,20 +782,26 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/send@0.17.4': resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} '@types/serve-static@1.15.7': resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} '@types/superagent@8.1.9': resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} - '@types/supertest@6.0.3': - resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} + '@types/supertest@7.2.0': + resolution: {integrity: sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==} '@types/swagger-jsdoc@6.0.4': resolution: {integrity: sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==} @@ -644,10 +815,82 @@ packages: '@types/yargs@17.0.33': resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + '@typescript-eslint/eslint-plugin@8.57.0': + resolution: {integrity: sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.57.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.57.0': + resolution: {integrity: sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.57.0': + resolution: {integrity: sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.57.0': + resolution: {integrity: sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.57.0': + resolution: {integrity: sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.57.0': + resolution: {integrity: sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.57.0': + resolution: {integrity: sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.57.0': + resolution: {integrity: sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.57.0': + resolution: {integrity: sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.57.0': + resolution: {integrity: sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -677,12 +920,13 @@ packages: asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} - async@3.2.6: - resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + babel-jest@29.7.0: resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -711,19 +955,24 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + basic-auth@2.0.1: resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} engines: {node: '>= 0.8'} - body-parser@2.2.0: - resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -748,6 +997,14 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + c12@3.1.0: + resolution: {integrity: sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==} + peerDependencies: + magicast: ^0.3.5 + peerDependenciesMeta: + magicast: + optional: true + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -782,10 +1039,23 @@ packages: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} + chevrotain@10.5.0: + resolution: {integrity: sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + + citty@0.2.1: + resolution: {integrity: sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==} + cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} @@ -825,6 +1095,13 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + content-disposition@1.0.0: resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} engines: {node: '>= 0.6'} @@ -847,8 +1124,8 @@ packages: cookiejar@2.1.4: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} - cors@2.8.5: - resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} create-jest@29.7.0: @@ -860,6 +1137,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -877,6 +1157,15 @@ packages: supports-color: optional: true + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dedent@1.5.3: resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} peerDependencies: @@ -885,18 +1174,35 @@ packages: babel-plugin-macros: optional: true + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge-ts@7.1.5: + resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} + engines: {node: '>=16.0.0'} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -912,8 +1218,12 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} - dotenv@16.5.0: - resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dotenv@17.3.1: + resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} engines: {node: '>=12'} dunder-proto@1.0.1: @@ -923,10 +1233,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - ejs@3.1.10: - resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} - engines: {node: '>=0.10.0'} - hasBin: true + effect@3.18.4: + resolution: {integrity: sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==} electron-to-chromium@1.5.136: resolution: {integrity: sha512-kL4+wUTD7RSA5FHx5YwWtjDnEEkIIikFgWHR4P6fqjw1PPLlqYkxeOb++wAauAssat0YClCy8Y3C5SxgSkjibQ==} @@ -938,6 +1246,10 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -961,13 +1273,8 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} - esbuild-register@3.6.0: - resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} - peerDependencies: - esbuild: '>=0.12 <1' - - esbuild@0.25.2: - resolution: {integrity: sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==} + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} engines: {node: '>=18'} hasBin: true @@ -982,11 +1289,57 @@ packages: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -1007,21 +1360,44 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - express@5.1.0: - resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + + fast-check@3.23.2: + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} + engines: {node: '>=8.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - filelist@1.0.4: - resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} @@ -1035,12 +1411,32 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.1: + resolution: {integrity: sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + form-data@4.0.2: resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} engines: {node: '>= 6'} - formidable@3.5.2: - resolution: {integrity: sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + formidable@3.5.4: + resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} + engines: {node: '>=14.0.0'} forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} @@ -1061,6 +1457,9 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -1077,6 +1476,9 @@ packages: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} + get-port-please@3.2.0: + resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} + get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} @@ -1085,6 +1487,17 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + get-tsconfig@4.13.6: + resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + + giget@2.0.0: + resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} + hasBin: true + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + glob@7.1.6: resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} deprecated: Glob versions prior to v9 are no longer supported @@ -1093,6 +1506,14 @@ packages: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -1100,6 +1521,17 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + grammex@3.1.12: + resolution: {integrity: sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==} + + graphmatch@1.1.1: + resolution: {integrity: sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==} + + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1120,9 +1552,9 @@ packages: resolution: {integrity: sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==} engines: {node: '>=18.0.0'} - hexoid@2.0.0: - resolution: {integrity: sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==} - engines: {node: '>=8'} + hono@4.11.4: + resolution: {integrity: sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==} + engines: {node: '>=16.9.0'} html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -1131,14 +1563,33 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-status-codes@2.3.0: + resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + import-local@3.2.0: resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} engines: {node: '>=8'} @@ -1166,6 +1617,10 @@ packages: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -1174,6 +1629,10 @@ packages: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} engines: {node: '>=6'} + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -1181,6 +1640,9 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -1212,11 +1674,6 @@ packages: resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} engines: {node: '>=8'} - jake@10.9.2: - resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} - engines: {node: '>=10'} - hasBin: true - jest-changed-files@29.7.0: resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1346,6 +1803,10 @@ packages: node-notifier: optional: true + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1357,19 +1818,35 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} hasBin: true + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} hasBin: true + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -1378,6 +1855,14 @@ packages: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -1385,6 +1870,10 @@ packages: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + lodash.get@4.4.2: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. @@ -1396,12 +1885,25 @@ packages: lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.mergewith@4.6.2: resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru.min@1.1.4: + resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} @@ -1460,15 +1962,21 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - minimatch@5.1.6: - resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} - engines: {node: '>=10'} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} - morgan@1.10.0: - resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==} + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + morgan@1.10.1: + resolution: {integrity: sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==} engines: {node: '>= 0.8.0'} ms@2.0.0: @@ -1477,6 +1985,14 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mysql2@3.15.3: + resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==} + engines: {node: '>= 8.0'} + + named-placeholders@1.1.6: + resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} + engines: {node: '>=8.0.0'} + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -1484,6 +2000,12 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -1498,6 +2020,11 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + nypm@0.6.5: + resolution: {integrity: sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==} + engines: {node: '>=18'} + hasBin: true + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1506,6 +2033,12 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + obuf@1.1.2: + resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + on-finished@2.3.0: resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} engines: {node: '>= 0.8'} @@ -1514,8 +2047,8 @@ packages: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} - on-headers@1.0.2: - resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} engines: {node: '>= 0.8'} once@1.4.0: @@ -1528,6 +2061,10 @@ packages: openapi-types@12.1.3: resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -1540,10 +2077,18 @@ packages: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -1571,6 +2116,54 @@ packages: resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} engines: {node: '>=16'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + pg-cloudflare@1.3.0: + resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} + + pg-connection-string@2.12.0: + resolution: {integrity: sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-numeric@1.0.2: + resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} + engines: {node: '>=4'} + + pg-pool@3.13.0: + resolution: {integrity: sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.13.0: + resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg-types@4.1.0: + resolution: {integrity: sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg==} + engines: {node: '>=10'} + + pg@8.20.0: + resolution: {integrity: sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1578,6 +2171,10 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + pirates@4.0.7: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} @@ -1586,17 +2183,66 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-array@3.0.4: + resolution: {integrity: sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==} + engines: {node: '>=12'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-bytea@3.0.0: + resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==} + engines: {node: '>= 6'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-date@2.1.0: + resolution: {integrity: sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==} + engines: {node: '>=12'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + postgres-interval@3.0.0: + resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==} + engines: {node: '>=12'} + + postgres-range@1.1.4: + resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} + + postgres@3.4.7: + resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} + engines: {node: '>=12'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + pretty-format@29.7.0: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - prisma@6.6.0: - resolution: {integrity: sha512-SYCUykz+1cnl6Ugd8VUvtTQq5+j1Q7C0CtzKPjQ8JyA2ALh0EEJkMCS+KgdnvKW1lrxjtjCyJSHOOT236mENYg==} - engines: {node: '>=18.18'} + prisma@7.5.0: + resolution: {integrity: sha512-n30qZpWehaYQzigLjmuPisyEsvOzHt7bZeRyg8gZ5DvJo9FGjD+gNaY59Ns3hlLD5/jZH5GBeftIss0jDbUoLg==} + engines: {node: ^20.19 || ^22.12 || >=24.0} hasBin: true peerDependencies: - typescript: '>=5.1.0' + better-sqlite3: '>=9.0.0' + typescript: '>=5.4.0' peerDependenciesMeta: + better-sqlite3: + optional: true typescript: optional: true @@ -1604,10 +2250,17 @@ packages: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} @@ -1615,17 +2268,43 @@ packages: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - raw-body@3.0.0: - resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} - engines: {node: '>= 0.8'} + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + rc9@2.1.2: + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + peerDependencies: + react: ^19.2.4 react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + engines: {node: '>=0.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + regexp-to-ast@0.5.0: + resolution: {integrity: sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==} + + remeda@2.33.4: + resolution: {integrity: sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -1634,10 +2313,17 @@ packages: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve.exports@2.0.3: resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} engines: {node: '>=10'} @@ -1647,6 +2333,10 @@ packages: engines: {node: '>= 0.4'} hasBin: true + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -1660,6 +2350,9 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -1669,10 +2362,18 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + send@1.2.0: resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} engines: {node: '>= 18'} + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + serve-static@2.2.0: resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} engines: {node: '>= 18'} @@ -1707,6 +2408,10 @@ packages: signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -1721,9 +2426,17 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -1732,6 +2445,13 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} @@ -1756,12 +2476,12 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - superagent@9.0.2: - resolution: {integrity: sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==} + superagent@10.3.0: + resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==} engines: {node: '>=14.18.0'} - supertest@7.1.0: - resolution: {integrity: sha512-5QeSO8hSrKghtcWEoPiO036fxH0Ii2wVQfFZSP0oqQhmjk8bOLhDFXr4JrvaFmPuEWUoq4znY3uSi8UzLKxGqw==} + supertest@7.2.2: + resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==} engines: {node: '>=14.18.0'} supports-color@7.2.0: @@ -1798,6 +2518,14 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} + tinyexec@1.0.4: + resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -1809,17 +2537,24 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - ts-jest@29.3.1: - resolution: {integrity: sha512-FT2PIRtZABwl6+ZCry8IY7JZ3xMuppsEV9qFVHOVe8jDzggwUZ9TsM4chyJxL9yi6LvkqcZYU3LmapEE454zBQ==} + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-jest@29.4.6: + resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/transform': ^29.0.0 - '@jest/types': ^29.0.0 - babel-jest: ^29.0.0 + '@jest/transform': ^29.0.0 || ^30.0.0 + '@jest/types': ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 esbuild: '*' - jest: ^29.0.0 + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 typescript: '>=4.3 <6' peerDependenciesMeta: '@babel/core': @@ -1832,6 +2567,17 @@ packages: optional: true esbuild: optional: true + jest-util: + optional: true + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} type-detect@4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} @@ -1841,21 +2587,33 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} - type-fest@4.39.1: - resolution: {integrity: sha512-uW9qzd66uyHYxwyVBYiwS4Oi0qZyUqwjU+Oevr6ZogYiXt99EOYtwvzMSLw1c3lYo2HzJsep/NB23iEVEgjG/w==} + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} type-is@2.0.1: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} - typescript@5.8.3: - resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + typescript-eslint@8.57.0: + resolution: {integrity: sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} @@ -1867,10 +2625,21 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + v8-to-istanbul@9.3.0: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} + valibot@1.2.0: + resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + validator@13.15.0: resolution: {integrity: sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==} engines: {node: '>= 0.10'} @@ -1887,6 +2656,13 @@ packages: engines: {node: '>= 8'} hasBin: true + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -1898,6 +2674,10 @@ packages: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -1926,6 +2706,9 @@ packages: engines: {node: '>=8.0.0'} hasBin: true + zeptomatch@2.1.0: + resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==} + zod@3.24.2: resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} @@ -2144,81 +2927,170 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@esbuild/aix-ppc64@0.25.2': + '@chevrotain/cst-dts-gen@10.5.0': + dependencies: + '@chevrotain/gast': 10.5.0 + '@chevrotain/types': 10.5.0 + lodash: 4.17.21 + + '@chevrotain/gast@10.5.0': + dependencies: + '@chevrotain/types': 10.5.0 + lodash: 4.17.21 + + '@chevrotain/types@10.5.0': {} + + '@chevrotain/utils@10.5.0': {} + + '@electric-sql/pglite-socket@0.0.20(@electric-sql/pglite@0.3.15)': + dependencies: + '@electric-sql/pglite': 0.3.15 + + '@electric-sql/pglite-tools@0.2.20(@electric-sql/pglite@0.3.15)': + dependencies: + '@electric-sql/pglite': 0.3.15 + + '@electric-sql/pglite@0.3.15': {} + + '@esbuild/aix-ppc64@0.27.4': optional: true - '@esbuild/android-arm64@0.25.2': + '@esbuild/android-arm64@0.27.4': optional: true - '@esbuild/android-arm@0.25.2': + '@esbuild/android-arm@0.27.4': optional: true - '@esbuild/android-x64@0.25.2': + '@esbuild/android-x64@0.27.4': optional: true - '@esbuild/darwin-arm64@0.25.2': + '@esbuild/darwin-arm64@0.27.4': optional: true - '@esbuild/darwin-x64@0.25.2': + '@esbuild/darwin-x64@0.27.4': optional: true - '@esbuild/freebsd-arm64@0.25.2': + '@esbuild/freebsd-arm64@0.27.4': optional: true - '@esbuild/freebsd-x64@0.25.2': + '@esbuild/freebsd-x64@0.27.4': optional: true - '@esbuild/linux-arm64@0.25.2': + '@esbuild/linux-arm64@0.27.4': optional: true - '@esbuild/linux-arm@0.25.2': + '@esbuild/linux-arm@0.27.4': optional: true - '@esbuild/linux-ia32@0.25.2': + '@esbuild/linux-ia32@0.27.4': optional: true - '@esbuild/linux-loong64@0.25.2': + '@esbuild/linux-loong64@0.27.4': optional: true - '@esbuild/linux-mips64el@0.25.2': + '@esbuild/linux-mips64el@0.27.4': optional: true - '@esbuild/linux-ppc64@0.25.2': + '@esbuild/linux-ppc64@0.27.4': optional: true - '@esbuild/linux-riscv64@0.25.2': + '@esbuild/linux-riscv64@0.27.4': optional: true - '@esbuild/linux-s390x@0.25.2': + '@esbuild/linux-s390x@0.27.4': optional: true - '@esbuild/linux-x64@0.25.2': + '@esbuild/linux-x64@0.27.4': optional: true - '@esbuild/netbsd-arm64@0.25.2': + '@esbuild/netbsd-arm64@0.27.4': optional: true - '@esbuild/netbsd-x64@0.25.2': + '@esbuild/netbsd-x64@0.27.4': optional: true - '@esbuild/openbsd-arm64@0.25.2': + '@esbuild/openbsd-arm64@0.27.4': optional: true - '@esbuild/openbsd-x64@0.25.2': + '@esbuild/openbsd-x64@0.27.4': optional: true - '@esbuild/sunos-x64@0.25.2': + '@esbuild/openharmony-arm64@0.27.4': optional: true - '@esbuild/win32-arm64@0.25.2': + '@esbuild/sunos-x64@0.27.4': optional: true - '@esbuild/win32-ia32@0.25.2': + '@esbuild/win32-arm64@0.27.4': optional: true - '@esbuild/win32-x64@0.25.2': + '@esbuild/win32-ia32@0.27.4': optional: true + '@esbuild/win32-x64@0.27.4': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))': + dependencies: + eslint: 9.39.4(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.0 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.14.0 + debug: 4.4.0 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@hono/node-server@1.19.9(hono@4.11.4)': + dependencies: + hono: 4.11.4 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + '@istanbuljs/load-nyc-config@1.1.0': dependencies: camelcase: 5.3.1 @@ -2232,7 +3104,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 22.14.0 + '@types/node': 25.5.0 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -2245,14 +3117,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.14.0 + '@types/node': 25.5.0 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.14.0) + jest-config: 29.7.0(@types/node@25.5.0) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -2277,7 +3149,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.14.0 + '@types/node': 25.5.0 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -2295,7 +3167,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 22.14.0 + '@types/node': 25.5.0 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -2317,7 +3189,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 - '@types/node': 22.14.0 + '@types/node': 25.5.0 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -2387,7 +3259,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.14.0 + '@types/node': 25.5.0 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -2410,38 +3282,104 @@ snapshots: '@jsdevtools/ono@7.1.3': {} - '@prisma/client@6.6.0(prisma@6.6.0(typescript@5.8.3))(typescript@5.8.3)': + '@mrleebo/prisma-ast@0.13.1': + dependencies: + chevrotain: 10.5.0 + lilconfig: 2.1.0 + + '@noble/hashes@1.8.0': {} + + '@paralleldrive/cuid2@2.3.1': + dependencies: + '@noble/hashes': 1.8.0 + + '@prisma/adapter-pg@7.5.0': + dependencies: + '@prisma/driver-adapter-utils': 7.5.0 + '@types/pg': 8.11.11 + pg: 8.20.0 + postgres-array: 3.0.4 + transitivePeerDependencies: + - pg-native + + '@prisma/client-runtime-utils@7.5.0': {} + + '@prisma/client@7.5.0(prisma@7.5.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3)': + dependencies: + '@prisma/client-runtime-utils': 7.5.0 optionalDependencies: - prisma: 6.6.0(typescript@5.8.3) - typescript: 5.8.3 + prisma: 7.5.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + typescript: 5.9.3 - '@prisma/config@6.6.0': + '@prisma/config@7.5.0': dependencies: - esbuild: 0.25.2 - esbuild-register: 3.6.0(esbuild@0.25.2) + c12: 3.1.0 + deepmerge-ts: 7.1.5 + effect: 3.18.4 + empathic: 2.0.0 transitivePeerDependencies: - - supports-color + - magicast + + '@prisma/debug@7.2.0': {} + + '@prisma/debug@7.5.0': {} + + '@prisma/dev@0.20.0(typescript@5.9.3)': + dependencies: + '@electric-sql/pglite': 0.3.15 + '@electric-sql/pglite-socket': 0.0.20(@electric-sql/pglite@0.3.15) + '@electric-sql/pglite-tools': 0.2.20(@electric-sql/pglite@0.3.15) + '@hono/node-server': 1.19.9(hono@4.11.4) + '@mrleebo/prisma-ast': 0.13.1 + '@prisma/get-platform': 7.2.0 + '@prisma/query-plan-executor': 7.2.0 + foreground-child: 3.3.1 + get-port-please: 3.2.0 + hono: 4.11.4 + http-status-codes: 2.3.0 + pathe: 2.0.3 + proper-lockfile: 4.1.2 + remeda: 2.33.4 + std-env: 3.10.0 + valibot: 1.2.0(typescript@5.9.3) + zeptomatch: 2.1.0 + transitivePeerDependencies: + - typescript + + '@prisma/driver-adapter-utils@7.5.0': + dependencies: + '@prisma/debug': 7.5.0 - '@prisma/debug@6.6.0': {} + '@prisma/engines-version@7.5.0-15.280c870be64f457428992c43c1f6d557fab6e29e': {} - '@prisma/engines-version@6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a': {} + '@prisma/engines@7.5.0': + dependencies: + '@prisma/debug': 7.5.0 + '@prisma/engines-version': 7.5.0-15.280c870be64f457428992c43c1f6d557fab6e29e + '@prisma/fetch-engine': 7.5.0 + '@prisma/get-platform': 7.5.0 + + '@prisma/fetch-engine@7.5.0': + dependencies: + '@prisma/debug': 7.5.0 + '@prisma/engines-version': 7.5.0-15.280c870be64f457428992c43c1f6d557fab6e29e + '@prisma/get-platform': 7.5.0 - '@prisma/engines@6.6.0': + '@prisma/get-platform@7.2.0': dependencies: - '@prisma/debug': 6.6.0 - '@prisma/engines-version': 6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a - '@prisma/fetch-engine': 6.6.0 - '@prisma/get-platform': 6.6.0 + '@prisma/debug': 7.2.0 - '@prisma/fetch-engine@6.6.0': + '@prisma/get-platform@7.5.0': dependencies: - '@prisma/debug': 6.6.0 - '@prisma/engines-version': 6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a - '@prisma/get-platform': 6.6.0 + '@prisma/debug': 7.5.0 + + '@prisma/query-plan-executor@7.2.0': {} - '@prisma/get-platform@6.6.0': + '@prisma/studio-core@0.21.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@prisma/debug': 6.6.0 + '@types/react': 19.2.14 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) '@scarf/scarf@1.4.0': {} @@ -2455,6 +3393,8 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@standard-schema/spec@1.1.0': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.27.0 @@ -2479,34 +3419,36 @@ snapshots: '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 - '@types/node': 22.14.0 + '@types/node': 25.5.0 '@types/connect@3.4.38': dependencies: - '@types/node': 22.14.0 + '@types/node': 25.5.0 '@types/cookiejar@2.1.5': {} - '@types/cors@2.8.17': + '@types/cors@2.8.19': dependencies: - '@types/node': 22.14.0 + '@types/node': 25.5.0 + + '@types/estree@1.0.8': {} '@types/express-serve-static-core@5.0.6': dependencies: - '@types/node': 22.14.0 + '@types/node': 25.5.0 '@types/qs': 6.9.18 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 - '@types/express@5.0.1': + '@types/express@5.0.6': dependencies: '@types/body-parser': 1.19.5 '@types/express-serve-static-core': 5.0.6 - '@types/serve-static': 1.15.7 + '@types/serve-static': 2.2.0 '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 22.14.0 + '@types/node': 25.5.0 '@types/http-errors@2.0.4': {} @@ -2531,39 +3473,60 @@ snapshots: '@types/mime@1.3.5': {} - '@types/morgan@1.9.9': + '@types/morgan@1.9.10': dependencies: - '@types/node': 22.14.0 + '@types/node': 25.5.0 - '@types/node@22.14.0': + '@types/node@25.5.0': dependencies: - undici-types: 6.21.0 + undici-types: 7.18.2 + + '@types/pg@8.11.11': + dependencies: + '@types/node': 25.5.0 + pg-protocol: 1.13.0 + pg-types: 4.1.0 + + '@types/pg@8.18.0': + dependencies: + '@types/node': 25.5.0 + pg-protocol: 1.13.0 + pg-types: 2.2.0 '@types/qs@6.9.18': {} '@types/range-parser@1.2.7': {} + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 22.14.0 + '@types/node': 25.5.0 '@types/serve-static@1.15.7': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 22.14.0 + '@types/node': 25.5.0 '@types/send': 0.17.4 + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.4 + '@types/node': 25.5.0 + '@types/stack-utils@2.0.3': {} '@types/superagent@8.1.9': dependencies: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 - '@types/node': 22.14.0 + '@types/node': 25.5.0 form-data: 4.0.2 - '@types/supertest@6.0.3': + '@types/supertest@7.2.0': dependencies: '@types/methods': 1.1.4 '@types/superagent': 8.1.9 @@ -2572,7 +3535,7 @@ snapshots: '@types/swagger-ui-express@4.1.8': dependencies: - '@types/express': 5.0.1 + '@types/express': 5.0.6 '@types/serve-static': 1.15.7 '@types/yargs-parser@21.0.3': {} @@ -2581,11 +3544,115 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 + '@typescript-eslint/eslint-plugin@8.57.0(@typescript-eslint/parser@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.0 + '@typescript-eslint/type-utils': 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.0 + eslint: 9.39.4(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.57.0 + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.0 + debug: 4.4.3 + eslint: 9.39.4(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.57.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.57.0(typescript@5.9.3) + '@typescript-eslint/types': 8.57.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.57.0': + dependencies: + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/visitor-keys': 8.57.0 + + '@typescript-eslint/tsconfig-utils@8.57.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.4(jiti@2.6.1) + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.57.0': {} + + '@typescript-eslint/typescript-estree@8.57.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.57.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.57.0(typescript@5.9.3) + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/visitor-keys': 8.57.0 + debug: 4.4.3 + minimatch: 10.2.4 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.57.0 + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3) + eslint: 9.39.4(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.57.0': + dependencies: + '@typescript-eslint/types': 8.57.0 + eslint-visitor-keys: 5.0.1 + accepts@2.0.0: dependencies: mime-types: 3.0.1 negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 @@ -2611,10 +3678,10 @@ snapshots: asap@2.0.6: {} - async@3.2.6: {} - asynckit@0.4.0: {} + aws-ssl-profiles@1.1.2: {} + babel-jest@29.7.0(@babel/core@7.26.10): dependencies: '@babel/core': 7.26.10 @@ -2672,20 +3739,22 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + basic-auth@2.0.1: dependencies: safe-buffer: 5.1.2 - body-parser@2.2.0: + body-parser@2.2.2: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.0 + debug: 4.4.3 http-errors: 2.0.0 - iconv-lite: 0.6.3 + iconv-lite: 0.7.2 on-finished: 2.4.1 - qs: 6.14.0 - raw-body: 3.0.0 + qs: 6.15.0 + raw-body: 3.0.2 type-is: 2.0.1 transitivePeerDependencies: - supports-color @@ -2695,9 +3764,9 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.1: + brace-expansion@5.0.4: dependencies: - balanced-match: 1.0.2 + balanced-match: 4.0.4 braces@3.0.3: dependencies: @@ -2722,6 +3791,21 @@ snapshots: bytes@3.1.2: {} + c12@3.1.0: + dependencies: + chokidar: 4.0.3 + confbox: 0.2.4 + defu: 6.1.4 + dotenv: 16.6.1 + exsolve: 1.0.8 + giget: 2.0.0 + jiti: 2.6.1 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 2.3.0 + rc9: 2.1.2 + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -2749,8 +3833,27 @@ snapshots: char-regex@1.0.2: {} + chevrotain@10.5.0: + dependencies: + '@chevrotain/cst-dts-gen': 10.5.0 + '@chevrotain/gast': 10.5.0 + '@chevrotain/types': 10.5.0 + '@chevrotain/utils': 10.5.0 + lodash: 4.17.21 + regexp-to-ast: 0.5.0 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + ci-info@3.9.0: {} + citty@0.1.6: + dependencies: + consola: 3.4.2 + + citty@0.2.1: {} + cjs-module-lexer@1.4.3: {} cliui@8.0.1: @@ -2782,6 +3885,10 @@ snapshots: concat-map@0.0.1: {} + confbox@0.2.4: {} + + consola@3.4.2: {} + content-disposition@1.0.0: dependencies: safe-buffer: 5.2.1 @@ -2796,18 +3903,18 @@ snapshots: cookiejar@2.1.4: {} - cors@2.8.5: + cors@2.8.6: dependencies: object-assign: 4.1.1 vary: 1.1.2 - create-jest@29.7.0(@types/node@22.14.0): + create-jest@29.7.0(@types/node@25.5.0): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.14.0) + jest-config: 29.7.0(@types/node@25.5.0) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -2822,6 +3929,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + csstype@3.2.3: {} + debug@2.6.9: dependencies: ms: 2.0.0 @@ -2830,14 +3939,28 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.3: + dependencies: + ms: 2.1.3 + dedent@1.5.3: {} + deep-is@0.1.4: {} + + deepmerge-ts@7.1.5: {} + deepmerge@4.3.1: {} + defu@6.1.4: {} + delayed-stream@1.0.0: {} + denque@2.1.0: {} + depd@2.0.0: {} + destr@2.0.5: {} + detect-newline@3.1.0: {} dezalgo@1.0.4: @@ -2851,7 +3974,9 @@ snapshots: dependencies: esutils: 2.0.3 - dotenv@16.5.0: {} + dotenv@16.6.1: {} + + dotenv@17.3.1: {} dunder-proto@1.0.1: dependencies: @@ -2861,9 +3986,10 @@ snapshots: ee-first@1.1.1: {} - ejs@3.1.10: + effect@3.18.4: dependencies: - jake: 10.9.2 + '@standard-schema/spec': 1.1.0 + fast-check: 3.23.2 electron-to-chromium@1.5.136: {} @@ -2871,6 +3997,8 @@ snapshots: emoji-regex@8.0.0: {} + empathic@2.0.0: {} + encodeurl@2.0.0: {} error-ex@1.3.2: @@ -2892,40 +4020,34 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 - esbuild-register@3.6.0(esbuild@0.25.2): - dependencies: - debug: 4.4.0 - esbuild: 0.25.2 - transitivePeerDependencies: - - supports-color - - esbuild@0.25.2: + esbuild@0.27.4: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.2 - '@esbuild/android-arm': 0.25.2 - '@esbuild/android-arm64': 0.25.2 - '@esbuild/android-x64': 0.25.2 - '@esbuild/darwin-arm64': 0.25.2 - '@esbuild/darwin-x64': 0.25.2 - '@esbuild/freebsd-arm64': 0.25.2 - '@esbuild/freebsd-x64': 0.25.2 - '@esbuild/linux-arm': 0.25.2 - '@esbuild/linux-arm64': 0.25.2 - '@esbuild/linux-ia32': 0.25.2 - '@esbuild/linux-loong64': 0.25.2 - '@esbuild/linux-mips64el': 0.25.2 - '@esbuild/linux-ppc64': 0.25.2 - '@esbuild/linux-riscv64': 0.25.2 - '@esbuild/linux-s390x': 0.25.2 - '@esbuild/linux-x64': 0.25.2 - '@esbuild/netbsd-arm64': 0.25.2 - '@esbuild/netbsd-x64': 0.25.2 - '@esbuild/openbsd-arm64': 0.25.2 - '@esbuild/openbsd-x64': 0.25.2 - '@esbuild/sunos-x64': 0.25.2 - '@esbuild/win32-arm64': 0.25.2 - '@esbuild/win32-ia32': 0.25.2 - '@esbuild/win32-x64': 0.25.2 + '@esbuild/aix-ppc64': 0.27.4 + '@esbuild/android-arm': 0.27.4 + '@esbuild/android-arm64': 0.27.4 + '@esbuild/android-x64': 0.27.4 + '@esbuild/darwin-arm64': 0.27.4 + '@esbuild/darwin-x64': 0.27.4 + '@esbuild/freebsd-arm64': 0.27.4 + '@esbuild/freebsd-x64': 0.27.4 + '@esbuild/linux-arm': 0.27.4 + '@esbuild/linux-arm64': 0.27.4 + '@esbuild/linux-ia32': 0.27.4 + '@esbuild/linux-loong64': 0.27.4 + '@esbuild/linux-mips64el': 0.27.4 + '@esbuild/linux-ppc64': 0.27.4 + '@esbuild/linux-riscv64': 0.27.4 + '@esbuild/linux-s390x': 0.27.4 + '@esbuild/linux-x64': 0.27.4 + '@esbuild/netbsd-arm64': 0.27.4 + '@esbuild/netbsd-x64': 0.27.4 + '@esbuild/openbsd-arm64': 0.27.4 + '@esbuild/openbsd-x64': 0.27.4 + '@esbuild/openharmony-arm64': 0.27.4 + '@esbuild/sunos-x64': 0.27.4 + '@esbuild/win32-arm64': 0.27.4 + '@esbuild/win32-ia32': 0.27.4 + '@esbuild/win32-x64': 0.27.4 escalade@3.2.0: {} @@ -2933,8 +4055,78 @@ snapshots: escape-string-regexp@2.0.0: {} + escape-string-regexp@4.0.0: {} + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.14.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.0 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + esprima@4.0.1: {} + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + esutils@2.0.3: {} etag@1.8.1: {} @@ -2961,15 +4153,16 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 - express@5.1.0: + express@5.2.1: dependencies: accepts: 2.0.0 - body-parser: 2.2.0 + body-parser: 2.2.2 content-disposition: 1.0.0 content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 debug: 4.4.0 + depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -2993,17 +4186,31 @@ snapshots: transitivePeerDependencies: - supports-color + exsolve@1.0.8: {} + + fast-check@3.23.2: + dependencies: + pure-rand: 6.1.0 + + fast-deep-equal@3.1.3: {} + fast-json-stable-stringify@2.1.0: {} + fast-levenshtein@2.0.6: {} + fast-safe-stringify@2.1.1: {} fb-watchman@2.0.2: dependencies: bser: 2.1.1 - filelist@1.0.4: + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: dependencies: - minimatch: 5.1.6 + flat-cache: 4.0.1 fill-range@7.1.1: dependencies: @@ -3025,6 +4232,23 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.1 + keyv: 4.5.4 + + flatted@3.4.1: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + form-data@4.0.2: dependencies: asynckit: 0.4.0 @@ -3032,10 +4256,18 @@ snapshots: es-set-tostringtag: 2.1.0 mime-types: 2.1.35 - formidable@3.5.2: + form-data@4.0.5: dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + formidable@3.5.4: + dependencies: + '@paralleldrive/cuid2': 2.3.1 dezalgo: 1.0.4 - hexoid: 2.0.0 once: 1.4.0 forwarded@0.2.0: {} @@ -3049,6 +4281,10 @@ snapshots: function-bind@1.1.2: {} + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -3068,6 +4304,8 @@ snapshots: get-package-type@0.1.0: {} + get-port-please@3.2.0: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 @@ -3075,6 +4313,23 @@ snapshots: get-stream@6.0.1: {} + get-tsconfig@4.13.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + giget@2.0.0: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + defu: 6.1.4 + node-fetch-native: 1.6.7 + nypm: 0.6.5 + pathe: 2.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + glob@7.1.6: dependencies: fs.realpath: 1.0.0 @@ -3086,10 +4341,27 @@ snapshots: globals@11.12.0: {} + globals@14.0.0: {} + + globals@15.15.0: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} + grammex@3.1.12: {} + + graphmatch@1.1.1: {} + + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + has-flag@4.0.0: {} has-symbols@1.1.0: {} @@ -3104,7 +4376,7 @@ snapshots: helmet@8.1.0: {} - hexoid@2.0.0: {} + hono@4.11.4: {} html-escaper@2.0.2: {} @@ -3116,12 +4388,31 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + http-status-codes@2.3.0: {} + human-signals@2.1.0: {} - iconv-lite@0.6.3: + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + import-local@3.2.0: dependencies: pkg-dir: 4.2.0 @@ -3144,14 +4435,22 @@ snapshots: dependencies: hasown: 2.0.2 + is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} is-generator-fn@2.1.0: {} + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + is-number@7.0.0: {} is-promise@4.0.0: {} + is-property@1.0.2: {} + is-stream@2.0.1: {} isexe@2.0.0: {} @@ -3197,13 +4496,6 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 - jake@10.9.2: - dependencies: - async: 3.2.6 - chalk: 4.1.2 - filelist: 1.0.4 - minimatch: 3.1.2 - jest-changed-files@29.7.0: dependencies: execa: 5.1.1 @@ -3216,7 +4508,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.14.0 + '@types/node': 25.5.0 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.3 @@ -3236,16 +4528,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@22.14.0): + jest-cli@29.7.0(@types/node@25.5.0): dependencies: '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.14.0) + create-jest: 29.7.0(@types/node@25.5.0) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@22.14.0) + jest-config: 29.7.0(@types/node@25.5.0) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -3255,7 +4547,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@22.14.0): + jest-config@29.7.0(@types/node@25.5.0): dependencies: '@babel/core': 7.26.10 '@jest/test-sequencer': 29.7.0 @@ -3280,7 +4572,7 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 22.14.0 + '@types/node': 25.5.0 transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -3309,7 +4601,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.14.0 + '@types/node': 25.5.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -3319,7 +4611,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 22.14.0 + '@types/node': 25.5.0 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -3358,7 +4650,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.14.0 + '@types/node': 25.5.0 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -3393,7 +4685,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.14.0 + '@types/node': 25.5.0 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -3421,7 +4713,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.14.0 + '@types/node': 25.5.0 chalk: 4.1.2 cjs-module-lexer: 1.4.3 collect-v8-coverage: 1.0.2 @@ -3467,7 +4759,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.14.0 + '@types/node': 25.5.0 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -3486,7 +4778,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.14.0 + '@types/node': 25.5.0 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -3495,23 +4787,25 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 22.14.0 + '@types/node': 25.5.0 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@22.14.0): + jest@29.7.0(@types/node@25.5.0): dependencies: '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@22.14.0) + jest-cli: 29.7.0(@types/node@25.5.0) transitivePeerDependencies: - '@types/node' - babel-plugin-macros - supports-color - ts-node + jiti@2.6.1: {} + js-tokens@4.0.0: {} js-yaml@3.14.1: @@ -3523,34 +4817,67 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + jsesc@3.1.0: {} + json-buffer@3.0.1: {} + json-parse-even-better-errors@2.3.1: {} + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + json5@2.2.3: {} + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + kleur@3.0.3: {} leven@3.1.0: {} + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@2.1.0: {} + lines-and-columns@1.2.4: {} locate-path@5.0.0: dependencies: p-locate: 4.1.0 + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + lodash.get@4.4.2: {} lodash.isequal@4.5.0: {} lodash.memoize@4.1.2: {} + lodash.merge@4.6.2: {} + lodash.mergewith@4.6.2: {} + lodash@4.17.21: {} + + long@5.3.2: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 + lru.min@1.1.4: {} + make-dir@4.0.0: dependencies: semver: 7.7.1 @@ -3592,21 +4919,27 @@ snapshots: mimic-fn@2.1.0: {} + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.4 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 - minimatch@5.1.6: + minimatch@3.1.5: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 1.1.11 - morgan@1.10.0: + minimist@1.2.8: {} + + morgan@1.10.1: dependencies: basic-auth: 2.0.1 debug: 2.6.9 depd: 2.0.0 on-finished: 2.3.0 - on-headers: 1.0.2 + on-headers: 1.1.0 transitivePeerDependencies: - supports-color @@ -3614,10 +4947,30 @@ snapshots: ms@2.1.3: {} + mysql2@3.15.3: + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.7.2 + long: 5.3.2 + lru.min: 1.1.4 + named-placeholders: 1.1.6 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + + named-placeholders@1.1.6: + dependencies: + lru.min: 1.1.4 + natural-compare@1.4.0: {} negotiator@1.0.0: {} + neo-async@2.6.2: {} + + node-fetch-native@1.6.7: {} + node-int64@0.4.0: {} node-releases@2.0.19: {} @@ -3628,10 +4981,20 @@ snapshots: dependencies: path-key: 3.1.1 + nypm@0.6.5: + dependencies: + citty: 0.2.1 + pathe: 2.0.3 + tinyexec: 1.0.4 + object-assign@4.1.1: {} object-inspect@1.13.4: {} + obuf@1.1.2: {} + + ohash@2.0.11: {} + on-finished@2.3.0: dependencies: ee-first: 1.1.1 @@ -3640,7 +5003,7 @@ snapshots: dependencies: ee-first: 1.1.1 - on-headers@1.0.2: {} + on-headers@1.1.0: {} once@1.4.0: dependencies: @@ -3652,6 +5015,15 @@ snapshots: openapi-types@12.1.3: {} + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -3664,8 +5036,16 @@ snapshots: dependencies: p-limit: 2.3.0 + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + p-try@2.2.0: {} + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.26.2 @@ -3685,67 +5065,192 @@ snapshots: path-to-regexp@8.2.0: {} + pathe@2.0.3: {} + + perfect-debounce@1.0.0: {} + + pg-cloudflare@1.3.0: + optional: true + + pg-connection-string@2.12.0: {} + + pg-int8@1.0.1: {} + + pg-numeric@1.0.2: {} + + pg-pool@3.13.0(pg@8.20.0): + dependencies: + pg: 8.20.0 + + pg-protocol@1.13.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg-types@4.1.0: + dependencies: + pg-int8: 1.0.1 + pg-numeric: 1.0.2 + postgres-array: 3.0.4 + postgres-bytea: 3.0.0 + postgres-date: 2.1.0 + postgres-interval: 3.0.0 + postgres-range: 1.1.4 + + pg@8.20.0: + dependencies: + pg-connection-string: 2.12.0 + pg-pool: 3.13.0(pg@8.20.0) + pg-protocol: 1.13.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.3.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@2.3.1: {} + picomatch@4.0.3: {} + pirates@4.0.7: {} pkg-dir@4.2.0: dependencies: find-up: 4.1.0 + pkg-types@2.3.0: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + + postgres-array@2.0.0: {} + + postgres-array@3.0.4: {} + + postgres-bytea@1.0.1: {} + + postgres-bytea@3.0.0: + dependencies: + obuf: 1.1.2 + + postgres-date@1.0.7: {} + + postgres-date@2.1.0: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + postgres-interval@3.0.0: {} + + postgres-range@1.1.4: {} + + postgres@3.4.7: {} + + prelude-ls@1.2.1: {} + pretty-format@29.7.0: dependencies: '@jest/schemas': 29.6.3 ansi-styles: 5.2.0 react-is: 18.3.1 - prisma@6.6.0(typescript@5.8.3): + prisma@7.5.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3): dependencies: - '@prisma/config': 6.6.0 - '@prisma/engines': 6.6.0 + '@prisma/config': 7.5.0 + '@prisma/dev': 0.20.0(typescript@5.9.3) + '@prisma/engines': 7.5.0 + '@prisma/studio-core': 0.21.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + mysql2: 3.15.3 + postgres: 3.4.7 optionalDependencies: - fsevents: 2.3.3 - typescript: 5.8.3 + typescript: 5.9.3 transitivePeerDependencies: - - supports-color + - '@types/react' + - magicast + - react + - react-dom prompts@2.4.2: dependencies: kleur: 3.0.3 sisteransi: 1.0.5 + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 + punycode@2.3.1: {} + pure-rand@6.1.0: {} qs@6.14.0: dependencies: side-channel: 1.1.0 + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + range-parser@1.2.1: {} - raw-body@3.0.0: + raw-body@3.0.2: dependencies: bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.6.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 unpipe: 1.0.0 + rc9@2.1.2: + dependencies: + defu: 6.1.4 + destr: 2.0.5 + + react-dom@19.2.4(react@19.2.4): + dependencies: + react: 19.2.4 + scheduler: 0.27.0 + react-is@18.3.1: {} + react@19.2.4: {} + + readdirp@4.1.2: {} + + regexp-to-ast@0.5.0: {} + + remeda@2.33.4: {} + require-directory@2.1.1: {} resolve-cwd@3.0.0: dependencies: resolve-from: 5.0.0 + resolve-from@4.0.0: {} + resolve-from@5.0.0: {} + resolve-pkg-maps@1.0.0: {} + resolve.exports@2.0.3: {} resolve@1.22.10: @@ -3754,6 +5259,8 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + retry@0.12.0: {} + router@2.2.0: dependencies: debug: 4.4.0 @@ -3770,10 +5277,14 @@ snapshots: safer-buffer@2.1.2: {} + scheduler@0.27.0: {} + semver@6.3.1: {} semver@7.7.1: {} + semver@7.7.4: {} + send@1.2.0: dependencies: debug: 4.4.0 @@ -3790,6 +5301,8 @@ snapshots: transitivePeerDependencies: - supports-color + seq-queue@0.0.5: {} + serve-static@2.2.0: dependencies: encodeurl: 2.0.0 @@ -3837,6 +5350,8 @@ snapshots: signal-exit@3.0.7: {} + signal-exit@4.1.0: {} + sisteransi@1.0.5: {} slash@3.0.0: {} @@ -3848,14 +5363,22 @@ snapshots: source-map@0.6.1: {} + split2@4.2.0: {} + sprintf-js@1.0.3: {} + sqlstring@2.3.3: {} + stack-utils@2.0.6: dependencies: escape-string-regexp: 2.0.0 statuses@2.0.1: {} + statuses@2.0.2: {} + + std-env@3.10.0: {} + string-length@4.0.2: dependencies: char-regex: 1.0.2 @@ -3877,24 +5400,25 @@ snapshots: strip-json-comments@3.1.1: {} - superagent@9.0.2: + superagent@10.3.0: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 debug: 4.4.0 fast-safe-stringify: 2.1.1 - form-data: 4.0.2 - formidable: 3.5.2 + form-data: 4.0.5 + formidable: 3.5.4 methods: 1.1.2 mime: 2.6.0 - qs: 6.14.0 + qs: 6.15.0 transitivePeerDependencies: - supports-color - supertest@7.1.0: + supertest@7.2.2: dependencies: + cookie-signature: 1.2.2 methods: 1.1.2 - superagent: 9.0.2 + superagent: 10.3.0 transitivePeerDependencies: - supports-color @@ -3929,9 +5453,9 @@ snapshots: dependencies: '@scarf/scarf': 1.4.0 - swagger-ui-express@5.0.1(express@5.1.0): + swagger-ui-express@5.0.1(express@5.2.1): dependencies: - express: 5.1.0 + express: 5.2.1 swagger-ui-dist: 5.20.8 test-exclude@6.0.0: @@ -3940,6 +5464,13 @@ snapshots: glob: 7.1.6 minimatch: 3.1.2 + tinyexec@1.0.4: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + tmpl@1.0.5: {} to-regex-range@5.0.1: @@ -3948,32 +5479,46 @@ snapshots: toidentifier@1.0.1: {} - ts-jest@29.3.1(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(esbuild@0.25.2)(jest@29.7.0(@types/node@22.14.0))(typescript@5.8.3): + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-jest@29.4.6(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(jest-util@29.7.0)(jest@29.7.0(@types/node@25.5.0))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 - ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@22.14.0) - jest-util: 29.7.0 + handlebars: 4.7.8 + jest: 29.7.0(@types/node@25.5.0) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.7.1 - type-fest: 4.39.1 - typescript: 5.8.3 + semver: 7.7.4 + type-fest: 4.41.0 + typescript: 5.9.3 yargs-parser: 21.1.1 optionalDependencies: '@babel/core': 7.26.10 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.26.10) - esbuild: 0.25.2 + jest-util: 29.7.0 + + tsx@4.21.0: + dependencies: + esbuild: 0.27.4 + get-tsconfig: 4.13.6 + optionalDependencies: + fsevents: 2.3.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 type-detect@4.0.8: {} type-fest@0.21.3: {} - type-fest@4.39.1: {} + type-fest@4.41.0: {} type-is@2.0.1: dependencies: @@ -3981,9 +5526,23 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.1 - typescript@5.8.3: {} + typescript-eslint@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.57.0(@typescript-eslint/parser@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.4(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + uglify-js@3.19.3: + optional: true - undici-types@6.21.0: {} + undici-types@7.18.2: {} unpipe@1.0.0: {} @@ -3993,12 +5552,20 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + v8-to-istanbul@9.3.0: dependencies: '@jridgewell/trace-mapping': 0.3.25 '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 + valibot@1.2.0(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + validator@13.15.0: {} vary@1.1.2: {} @@ -4011,6 +5578,10 @@ snapshots: dependencies: isexe: 2.0.0 + word-wrap@1.2.5: {} + + wordwrap@1.0.0: {} + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -4024,6 +5595,8 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 + xtend@4.0.2: {} + y18n@5.0.8: {} yallist@3.1.1: {} @@ -4052,4 +5625,9 @@ snapshots: optionalDependencies: commander: 9.5.0 + zeptomatch@2.1.0: + dependencies: + grammex: 3.1.12 + graphmatch: 1.1.1 + zod@3.24.2: {} diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..e5e83d7 --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,13 @@ +import 'dotenv/config'; +import { defineConfig } from 'prisma/config'; + +export default defineConfig({ + schema: 'prisma/schema.prisma', + migrations: { + path: 'prisma/migrations', + seed: 'tsx prisma/seed.ts', + }, + datasource: { + url: process.env.DATABASE_URL ?? 'postgresql://postgres:postgres@localhost:5432/locations_api', + }, +}); diff --git a/prisma/migrations/20250411175910_cleanup/migration.sql b/prisma/migrations/20250411175910_cleanup/migration.sql deleted file mode 100644 index 7841308..0000000 --- a/prisma/migrations/20250411175910_cleanup/migration.sql +++ /dev/null @@ -1,32 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `properties_count` on the `districts` table. All the data in the column will be lost. - - You are about to drop the column `view_count` on the `districts` table. All the data in the column will be lost. - - You are about to drop the column `watcher_count` on the `districts` table. All the data in the column will be lost. - - You are about to drop the column `properties_count` on the `places` table. All the data in the column will be lost. - - You are about to drop the column `view_count` on the `places` table. All the data in the column will be lost. - - You are about to drop the column `properties_count` on the `regions` table. All the data in the column will be lost. - - You are about to drop the column `view_count` on the `regions` table. All the data in the column will be lost. - - You are about to drop the column `watcher_count` on the `regions` table. All the data in the column will be lost. - - You are about to drop the column `properties_count` on the `wards` table. All the data in the column will be lost. - - You are about to drop the column `view_count` on the `wards` table. All the data in the column will be lost. - -*/ --- AlterTable -ALTER TABLE "districts" DROP COLUMN "properties_count", -DROP COLUMN "view_count", -DROP COLUMN "watcher_count"; - --- AlterTable -ALTER TABLE "places" DROP COLUMN "properties_count", -DROP COLUMN "view_count"; - --- AlterTable -ALTER TABLE "regions" DROP COLUMN "properties_count", -DROP COLUMN "view_count", -DROP COLUMN "watcher_count"; - --- AlterTable -ALTER TABLE "wards" DROP COLUMN "properties_count", -DROP COLUMN "view_count"; diff --git a/prisma/migrations/20260316190000_baseline/migration.sql b/prisma/migrations/20260316190000_baseline/migration.sql new file mode 100644 index 0000000..456cacf --- /dev/null +++ b/prisma/migrations/20260316190000_baseline/migration.sql @@ -0,0 +1,169 @@ +CREATE TABLE "countries" ( + "id" SERIAL NOT NULL, + "iso" CHAR(2) NOT NULL, + "name" TEXT NOT NULL, + "nicename" TEXT NOT NULL, + "iso3" CHAR(3), + "numcode" INTEGER, + "phonecode" INTEGER NOT NULL, + CONSTRAINT "countries_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX "idx_countries_iso" ON "countries"("iso"); + +CREATE TABLE "general" ( + "id" SERIAL NOT NULL, + "country_id" INTEGER, + "region" VARCHAR NOT NULL, + "regioncode" INTEGER NOT NULL, + "district" VARCHAR NOT NULL, + "districtcode" INTEGER NOT NULL, + "ward" VARCHAR NOT NULL, + "wardcode" INTEGER NOT NULL, + "street" VARCHAR, + "places" VARCHAR, + "search_vector" tsvector, + CONSTRAINT "general_pkey" PRIMARY KEY ("id") +); + +CREATE INDEX "idx_general_search_vector" ON "general" USING GIN ("search_vector"); + +CREATE FUNCTION update_general_search_vector() RETURNS trigger +LANGUAGE plpgsql +AS $$ +BEGIN + NEW.search_vector := to_tsvector('simple'::regconfig, concat_ws(' '::text, NEW.region, NEW.district, NEW.ward, NEW.street, NEW.places)); + RETURN NEW; +END +$$; + +CREATE TRIGGER set_general_search_vector +BEFORE INSERT OR UPDATE ON "general" +FOR EACH ROW +EXECUTE FUNCTION update_general_search_vector(); + +CREATE TABLE "regions" ( + "region_name" TEXT, + "region_code" INTEGER NOT NULL, + "general_locations_id" INTEGER, + "country_id" INTEGER, + CONSTRAINT "regions_pkey" PRIMARY KEY ("region_code") +); + +CREATE INDEX "idx_regions_country_id" ON "regions"("country_id"); +CREATE INDEX "idx_regions_general_id" ON "regions"("general_locations_id"); + +CREATE TABLE "districts" ( + "district_name" TEXT, + "district_code" INTEGER NOT NULL, + "general_locations_id" INTEGER, + "region_id" INTEGER, + "country_id" INTEGER, + CONSTRAINT "districts_pkey" PRIMARY KEY ("district_code") +); + +CREATE INDEX "idx_districts_country_id" ON "districts"("country_id"); +CREATE INDEX "idx_districts_general_id" ON "districts"("general_locations_id"); +CREATE INDEX "idx_districts_region_id" ON "districts"("region_id"); + +CREATE TABLE "wards" ( + "ward_name" TEXT, + "ward_code" INTEGER NOT NULL, + "district_id" INTEGER, + "region_id" INTEGER, + "country_id" INTEGER, + "general_locations_id" INTEGER, + CONSTRAINT "wards_pkey" PRIMARY KEY ("ward_code") +); + +CREATE INDEX "idx_wards_country_id" ON "wards"("country_id"); +CREATE INDEX "idx_wards_district_id" ON "wards"("district_id"); +CREATE INDEX "idx_wards_general_id" ON "wards"("general_locations_id"); +CREATE INDEX "idx_wards_region_id" ON "wards"("region_id"); + +CREATE TABLE "places" ( + "id" SERIAL NOT NULL, + "place_name" TEXT, + "ward_id" INTEGER, + "district_id" INTEGER, + "region_id" INTEGER, + "country_id" INTEGER, + "general_locations_id" INTEGER, + CONSTRAINT "places_pkey" PRIMARY KEY ("id") +); + +CREATE INDEX "idx_places_country_id" ON "places"("country_id"); +CREATE INDEX "idx_places_district_id" ON "places"("district_id"); +CREATE INDEX "idx_places_general_id" ON "places"("general_locations_id"); +CREATE INDEX "idx_places_region_id" ON "places"("region_id"); +CREATE INDEX "idx_places_ward_id" ON "places"("ward_id"); + +ALTER TABLE "regions" + ADD CONSTRAINT "regions_country_id_fkey" + FOREIGN KEY ("country_id") + REFERENCES "countries"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +ALTER TABLE "regions" + ADD CONSTRAINT "regions_general_locations_id_fkey" + FOREIGN KEY ("general_locations_id") + REFERENCES "general"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +ALTER TABLE "districts" + ADD CONSTRAINT "districts_country_id_fkey" + FOREIGN KEY ("country_id") + REFERENCES "countries"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +ALTER TABLE "districts" + ADD CONSTRAINT "districts_general_locations_id_fkey" + FOREIGN KEY ("general_locations_id") + REFERENCES "general"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +ALTER TABLE "districts" + ADD CONSTRAINT "districts_region_id_fkey" + FOREIGN KEY ("region_id") + REFERENCES "regions"("region_code") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +ALTER TABLE "wards" + ADD CONSTRAINT "wards_country_id_fkey" + FOREIGN KEY ("country_id") + REFERENCES "countries"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +ALTER TABLE "wards" + ADD CONSTRAINT "wards_district_id_fkey" + FOREIGN KEY ("district_id") + REFERENCES "districts"("district_code") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +ALTER TABLE "wards" + ADD CONSTRAINT "wards_general_locations_id_fkey" + FOREIGN KEY ("general_locations_id") + REFERENCES "general"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +ALTER TABLE "wards" + ADD CONSTRAINT "wards_region_id_fkey" + FOREIGN KEY ("region_id") + REFERENCES "regions"("region_code") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +ALTER TABLE "places" + ADD CONSTRAINT "places_general_locations_id_fkey" + FOREIGN KEY ("general_locations_id") + REFERENCES "general"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; diff --git a/prisma/migrations/init/migration.sql b/prisma/migrations/init/migration.sql deleted file mode 100644 index 5825673..0000000 --- a/prisma/migrations/init/migration.sql +++ /dev/null @@ -1,115 +0,0 @@ --- CreateTable -CREATE TABLE "countries" ( - "id" SERIAL NOT NULL, - "iso" CHAR(2) NOT NULL, - "name" TEXT NOT NULL, - "nicename" TEXT NOT NULL, - "iso3" CHAR(3), - "numcode" INTEGER, - "phonecode" INTEGER NOT NULL, - - CONSTRAINT "countries_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "districts" ( - "district_name" TEXT, - "district_code" INTEGER NOT NULL, - "watcher_count" INTEGER, - "view_count" INTEGER, - "general_locations_id" INTEGER, - "properties_count" INTEGER, - "region_id" INTEGER, - "country_id" INTEGER, - - CONSTRAINT "districts_pkey" PRIMARY KEY ("district_code") -); - --- CreateTable -CREATE TABLE "places" ( - "id" SERIAL NOT NULL, - "place_name" TEXT, - "view_count" INTEGER, - "properties_count" INTEGER, - "ward_id" INTEGER, - "district_id" INTEGER, - "region_id" INTEGER, - "country_id" INTEGER, - "general_locations_id" INTEGER, - - CONSTRAINT "places_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "regions" ( - "region_name" TEXT, - "region_code" INTEGER NOT NULL, - "watcher_count" INTEGER, - "view_count" INTEGER, - "general_locations_id" INTEGER, - "properties_count" INTEGER, - "country_id" INTEGER, - - CONSTRAINT "regions_pkey" PRIMARY KEY ("region_code") -); - --- CreateTable -CREATE TABLE "wards" ( - "ward_name" TEXT, - "ward_code" INTEGER NOT NULL, - "district_id" INTEGER, - "region_id" INTEGER, - "country_id" INTEGER, - "view_count" INTEGER, - "properties_count" INTEGER, - "general_locations_id" INTEGER, - - CONSTRAINT "wards_pkey" PRIMARY KEY ("ward_code") -); - --- CreateTable -CREATE TABLE "general" ( - "id" SERIAL NOT NULL, - "country_id" INTEGER, - "region" VARCHAR NOT NULL, - "regioncode" INTEGER NOT NULL, - "district" VARCHAR NOT NULL, - "districtcode" INTEGER NOT NULL, - "ward" VARCHAR NOT NULL, - "wardcode" INTEGER NOT NULL, - "street" VARCHAR, - "places" VARCHAR, - - CONSTRAINT "general_pkey" PRIMARY KEY ("id") -); - --- AddForeignKey -ALTER TABLE "districts" ADD CONSTRAINT "districts_country_id_fkey" FOREIGN KEY ("country_id") REFERENCES "countries"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; - --- AddForeignKey -ALTER TABLE "districts" ADD CONSTRAINT "districts_general_locations_id_fkey" FOREIGN KEY ("general_locations_id") REFERENCES "general"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; - --- AddForeignKey -ALTER TABLE "districts" ADD CONSTRAINT "districts_region_id_fkey" FOREIGN KEY ("region_id") REFERENCES "regions"("region_code") ON DELETE NO ACTION ON UPDATE NO ACTION; - --- AddForeignKey -ALTER TABLE "places" ADD CONSTRAINT "places_general_locations_id_fkey" FOREIGN KEY ("general_locations_id") REFERENCES "general"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; - --- AddForeignKey -ALTER TABLE "regions" ADD CONSTRAINT "regions_country_id_fkey" FOREIGN KEY ("country_id") REFERENCES "countries"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; - --- AddForeignKey -ALTER TABLE "regions" ADD CONSTRAINT "regions_general_locations_id_fkey" FOREIGN KEY ("general_locations_id") REFERENCES "general"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; - --- AddForeignKey -ALTER TABLE "wards" ADD CONSTRAINT "wards_country_id_fkey" FOREIGN KEY ("country_id") REFERENCES "countries"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; - --- AddForeignKey -ALTER TABLE "wards" ADD CONSTRAINT "wards_district_id_fkey" FOREIGN KEY ("district_id") REFERENCES "districts"("district_code") ON DELETE NO ACTION ON UPDATE NO ACTION; - --- AddForeignKey -ALTER TABLE "wards" ADD CONSTRAINT "wards_general_locations_id_fkey" FOREIGN KEY ("general_locations_id") REFERENCES "general"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; - --- AddForeignKey -ALTER TABLE "wards" ADD CONSTRAINT "wards_region_id_fkey" FOREIGN KEY ("region_id") REFERENCES "regions"("region_code") ON DELETE NO ACTION ON UPDATE NO ACTION; - diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5abddf3..452c434 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,11 +1,12 @@ generator client { - provider = "prisma-client-js" - previewFeatures = ["fullTextSearchPostgres"] + provider = "prisma-client" + output = "../src/generated/prisma" + moduleFormat = "esm" + engineType = "client" } datasource db { provider = "postgresql" - url = env("DATABASE_URL") } model Countries { @@ -103,7 +104,7 @@ model General { wardcode Int street String? @db.VarChar places String? @db.VarChar - search_vector Unsupported("tsvector")? @default(dbgenerated("to_tsvector('simple'::regconfig, (((((((((COALESCE(region, ''::character varying))::text || ' '::text) || (COALESCE(district, ''::character varying))::text) || ' '::text) || (COALESCE(ward, ''::character varying))::text) || ' '::text) || (COALESCE(street, ''::character varying))::text) || ' '::text) || (COALESCE(places, ''::character varying))::text))")) + search_vector Unsupported("tsvector")? districts Districts[] @relation("districtsTogeneral") places_generalToplaces Places[] @relation("generalToplaces") regions Regions[] @relation("generalToregions") diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..8055493 --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,180 @@ +import { disconnectPrisma, prisma } from '../src/db/prisma.js'; + +async function seed() { + await prisma.$transaction([ + prisma.places.deleteMany(), + prisma.wards.deleteMany(), + prisma.districts.deleteMany(), + prisma.regions.deleteMany(), + prisma.general.deleteMany(), + prisma.countries.deleteMany(), + ]); + + await prisma.countries.createMany({ + data: [ + { + id: 1, + iso: 'TZ', + iso3: 'TZA', + name: 'Tanzania', + nicename: 'United Republic of Tanzania', + numcode: 834, + phonecode: 255, + }, + { + id: 2, + iso: 'KE', + iso3: 'KEN', + name: 'Kenya', + nicename: 'Republic of Kenya', + numcode: 404, + phonecode: 254, + }, + ], + }); + + await prisma.general.createMany({ + data: [ + { + id: 1, + countryId: 1, + region: 'Dodoma', + regioncode: 12, + district: 'Dodoma Urban', + districtcode: 1201, + ward: 'Nzuguni', + wardcode: 120101, + street: 'Nzuguni Road', + places: 'Nzuguni Center', + }, + { + id: 2, + countryId: 1, + region: 'Arusha', + regioncode: 11, + district: 'Arusha Urban', + districtcode: 1101, + ward: 'Kaloleni', + wardcode: 110101, + street: 'Clock Tower Avenue', + places: 'Arusha Clock Tower', + }, + { + id: 3, + countryId: 2, + region: 'Nairobi', + regioncode: 21, + district: 'Westlands', + districtcode: 2101, + ward: 'Parklands', + wardcode: 210101, + street: 'Westlands Road', + places: 'Sarit Centre', + }, + ], + }); + + await prisma.regions.createMany({ + data: [ + { countryId: 1, general_locations_id: 2, regionCode: 11, regionName: 'Arusha' }, + { countryId: 1, general_locations_id: 1, regionCode: 12, regionName: 'Dodoma' }, + { countryId: 2, general_locations_id: 3, regionCode: 21, regionName: 'Nairobi' }, + ], + }); + + await prisma.districts.createMany({ + data: [ + { + country_id: 1, + districtCode: 1101, + districtName: 'Arusha Urban', + general_locations_id: 2, + regionId: 11, + }, + { + country_id: 1, + districtCode: 1201, + districtName: 'Dodoma Urban', + general_locations_id: 1, + regionId: 12, + }, + { + country_id: 2, + districtCode: 2101, + districtName: 'Westlands', + general_locations_id: 3, + regionId: 21, + }, + ], + }); + + await prisma.wards.createMany({ + data: [ + { + country_id: 1, + districtId: 1101, + general_locations_id: 2, + region_id: 11, + wardCode: 110101, + wardName: 'Kaloleni', + }, + { + country_id: 1, + districtId: 1201, + general_locations_id: 1, + region_id: 12, + wardCode: 120101, + wardName: 'Nzuguni', + }, + { + country_id: 2, + districtId: 2101, + general_locations_id: 3, + region_id: 21, + wardCode: 210101, + wardName: 'Parklands', + }, + ], + }); + + await prisma.places.createMany({ + data: [ + { + country_id: 1, + district_id: 1101, + general_locations_id: 2, + id: 1, + placeName: 'Arusha Clock Tower', + region_id: 11, + wardId: 110101, + }, + { + country_id: 1, + district_id: 1201, + general_locations_id: 1, + id: 2, + placeName: 'Nzuguni Center', + region_id: 12, + wardId: 120101, + }, + { + country_id: 2, + district_id: 2101, + general_locations_id: 3, + id: 3, + placeName: 'Sarit Centre', + region_id: 21, + wardId: 210101, + }, + ], + }); +} + +seed() + .catch((error) => { + console.error('Seed failed', error); + process.exitCode = 1; + }) + .finally(async () => { + await disconnectPrisma(); + }); diff --git a/scripts/export-openapi.ts b/scripts/export-openapi.ts new file mode 100644 index 0000000..e41f8b2 --- /dev/null +++ b/scripts/export-openapi.ts @@ -0,0 +1,10 @@ +import { mkdir, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { openApiSpec } from '../src/docs/swagger.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const outputDir = path.resolve(__dirname, '../generated/openapi'); + +await mkdir(outputDir, { recursive: true }); +await writeFile(path.join(outputDir, 'openapi.json'), `${JSON.stringify(openApiSpec, null, 2)}\n`, 'utf8'); diff --git a/server.ts b/server.ts index 50acb30..3c6e6e3 100644 --- a/server.ts +++ b/server.ts @@ -1,22 +1,38 @@ -import { PrismaClient } from '@prisma/client'; -import app from './src/app'; +import app from './src/app.js'; +import config from './src/config.js'; +import { disconnectPrisma } from './src/db/prisma.js'; -const PORT = process.env.PORT || 8080; -const prisma = new PrismaClient(); - -process.on('SIGINT', async () => { - console.log('SIGINT received. Shutting down gracefully'); - await prisma.$disconnect(); - process.exit(0); +const server = app.listen(config.port, () => { + console.log( + JSON.stringify({ + environment: config.nodeEnv, + message: 'Server started', + openApiUrl: `http://localhost:${config.port}/openapi.json`, + port: config.port, + swaggerUrl: `http://localhost:${config.port}/api-docs`, + }), + ); }); -process.on('SIGTERM', async () => { - console.log('SIGTERM received. Shutting down gracefully'); - await prisma.$disconnect(); - process.exit(0); +async function shutdown(signal: NodeJS.Signals) { + console.log(JSON.stringify({ message: 'Graceful shutdown requested', signal })); + + server.close(() => { + void disconnectPrisma() + .then(() => { + process.exit(0); + }) + .catch((error: unknown) => { + console.error(JSON.stringify({ error, message: 'Failed to disconnect Prisma cleanly' })); + process.exit(1); + }); + }); +} + +process.on('SIGINT', () => { + void shutdown('SIGINT'); }); -app.listen(PORT, () => { - console.log(`Server running on port ${PORT} in ${process.env.NODE_ENV || 'development'} mode`); - console.log(`API Documentation available at http://localhost:${PORT}/api-docs`); +process.on('SIGTERM', () => { + void shutdown('SIGTERM'); }); diff --git a/src/app.ts b/src/app.ts index b134df7..e61cf92 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,34 +1,46 @@ -import express, { Request, Response, NextFunction } from 'express'; +import express from 'express'; import cors from 'cors'; import helmet from 'helmet'; import morgan from 'morgan'; -import { PrismaClient } from '@prisma/client'; -import { errorHandler } from './middleware/errorHandler'; -import { setupSwagger } from './docs/swagger'; -import routes from './routes'; +import type { Request, Response } from 'express'; +import config from './config.js'; +import { setupSwagger } from './docs/swagger.js'; +import { errorHandler } from './middleware/errorHandler.js'; +import { + apiCompatibilityHeaders, + attachRequestContext, +} from './middleware/requestContext.js'; +import routes from './routes.js'; const app = express(); -export const prisma = new PrismaClient(); -app.use(helmet()); -app.use(cors()); -app.use(morgan('dev')); +morgan.token('request-id', (req) => (req as Request).requestId ?? '-'); -if (process.env.NODE_ENV !== 'production') { - app.use((req: Request, _: Response, next: NextFunction) => { - console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); - next(); +const logFormatter: morgan.FormatFn = (tokens, req, res) => { + return JSON.stringify({ + contentLength: tokens.res(req, res, 'content-length') ?? '0', + method: tokens.method(req, res), + path: tokens.url(req, res), + requestId: tokens['request-id'](req, res), + responseTimeMs: Number(tokens['response-time'](req, res)), + status: Number(tokens.status(req, res)), }); -} +}; + +app.use(helmet()); +app.use(cors()); +app.disable('x-powered-by'); +app.use(attachRequestContext); +app.use(morgan(logFormatter)); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.get('/health', (_: Request, res: Response) => { - res.status(200).json({ - status: 'UP', + res.status(200).json({ + status: 'UP', timestamp: new Date().toISOString(), - environment: process.env.NODE_ENV || 'development', + environment: config.nodeEnv, version: process.env.npm_package_version || '1.0.0' }); }); @@ -38,6 +50,9 @@ app.get('/', (_: Request, res: Response) => { }); app.use('/v1', routes); +app.use('/api', apiCompatibilityHeaders, routes); + +setupSwagger(app); app.use((req, res) => { res.status(404).json({ @@ -50,5 +65,4 @@ app.use((req, res) => { app.use(errorHandler); -setupSwagger(app); export default app; diff --git a/src/config.ts b/src/config.ts index d20b2c2..3d4abe1 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,11 +1,22 @@ import dotenv from 'dotenv'; +import { z } from 'zod'; + dotenv.config(); +const envSchema = z.object({ + DATABASE_URL: z.string().min(1, 'DATABASE_URL is required'), + NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), + PAGE_SIZE: z.coerce.number().int().positive().max(100).default(10), + PORT: z.coerce.number().int().positive().default(8080), +}); + +const env = envSchema.parse(process.env); + const config = { - nodeEnv: process.env.NODE_ENV || 'development', - port: process.env.PORT || 8080, - databaseUrl: process.env.DATABASE_URL, - pageSize: parseInt(process.env.PAGE_SIZE || '10', 10) + databaseUrl: env.DATABASE_URL, + nodeEnv: env.NODE_ENV, + pageSize: env.PAGE_SIZE, + port: env.PORT, }; export default config; diff --git a/src/db/prisma.ts b/src/db/prisma.ts new file mode 100644 index 0000000..5809fef --- /dev/null +++ b/src/db/prisma.ts @@ -0,0 +1,33 @@ +import { PrismaPg } from '@prisma/adapter-pg'; +import { Pool } from 'pg'; +import { PrismaClient } from '../generated/prisma/client.js'; +import config from '../config.js'; + +const globalForPrisma = globalThis as typeof globalThis & { + pgPool?: Pool; + prisma?: PrismaClient; +}; + +const pool = + globalForPrisma.pgPool ?? + new Pool({ + connectionString: config.databaseUrl, + }); + +const adapter = new PrismaPg(pool as unknown as ConstructorParameters[0]); + +export const prisma = + globalForPrisma.prisma ?? + new PrismaClient({ + adapter, + }); + +if (config.nodeEnv !== 'production') { + globalForPrisma.pgPool = pool; + globalForPrisma.prisma = prisma; +} + +export async function disconnectPrisma() { + await prisma.$disconnect(); + await pool.end(); +} diff --git a/src/docs/swagger.ts b/src/docs/swagger.ts index 0208708..4b2ff15 100644 --- a/src/docs/swagger.ts +++ b/src/docs/swagger.ts @@ -1,36 +1,243 @@ -import swaggerJsdoc from 'swagger-jsdoc'; +import type { Express } from 'express'; import swaggerUi from 'swagger-ui-express'; -import { Express } from 'express'; -const options = { - definition: { - openapi: '3.0.0', - info: { - title: 'Tanzania Location API', - version: '2.0.0', - description: 'API for retrieving location data in Tanzania including countries, regions, districts, and wards.', - contact: { - name: 'HackEAC', - url: 'https://maotora.com', - email: 'maotoramm@gmail.com' - }, - license: { - name: 'copyleft', - url: 'https://opensource.org/licenses/GNU' - } - }, - servers: [ - { - url: 'http://localhost:8080/api', - description: 'Local development server' - } - ] +export const openApiSpec = { + openapi: '3.1.0', + info: { + title: 'Tanzania Location API', + version: '2.0.0', + description: + 'Compatibility-first API for Tanzania and East Africa location data with dual base paths.', + contact: { + name: 'HackEAC', + url: 'https://maotora.com', + email: 'maotoramm@gmail.com', + }, + }, + servers: [ + { + url: '/v1', + description: 'Canonical API base path', + }, + { + url: '/api', + description: 'Compatibility alias', + }, + ], + components: { + schemas: { + ErrorResponse: { + type: 'object', + properties: { + error: { + type: 'object', + properties: { + message: { type: 'string' }, + }, + required: ['message'], + }, + }, + required: ['error'], + }, + Pagination: { + type: 'object', + properties: { + page: { type: 'integer' }, + limit: { type: 'integer' }, + total: { type: 'integer' }, + pages: { type: 'integer' }, + }, + required: ['page', 'limit', 'total', 'pages'], + }, + }, + }, + paths: { + '/countries': { + get: { + summary: 'List countries', + parameters: [ + { in: 'query', name: 'page', schema: { type: 'integer', minimum: 1 } }, + { in: 'query', name: 'limit', schema: { type: 'integer', minimum: 1, maximum: 100 } }, + { in: 'query', name: 'search', schema: { type: 'string' } }, + ], + responses: { + '200': { description: 'Countries list' }, + }, + }, + }, + '/countries/{id}': { + get: { + summary: 'Get a country by id', + parameters: [{ in: 'path', name: 'id', required: true, schema: { type: 'integer' } }], + responses: { + '200': { description: 'Country details' }, + '404': { description: 'Country not found' }, + }, + }, + }, + '/countries/{countryCode}/regions': { + get: { + summary: 'List country regions', + parameters: [{ in: 'path', name: 'countryCode', required: true, schema: { type: 'integer' } }], + responses: { + '200': { description: 'Country regions' }, + '404': { description: 'Country not found' }, + }, + }, + }, + '/regions': { + get: { + summary: 'List regions', + parameters: [ + { in: 'query', name: 'page', schema: { type: 'integer', minimum: 1 } }, + { in: 'query', name: 'limit', schema: { type: 'integer', minimum: 1, maximum: 100 } }, + { in: 'query', name: 'search', schema: { type: 'string' } }, + { in: 'query', name: 'countryId', schema: { type: 'integer', minimum: 1 } }, + ], + responses: { + '200': { description: 'Regions list' }, + }, + }, + }, + '/regions/{regionCode}': { + get: { + summary: 'Get a region by code', + parameters: [{ in: 'path', name: 'regionCode', required: true, schema: { type: 'integer' } }], + responses: { + '200': { description: 'Region details' }, + '404': { description: 'Region not found' }, + }, + }, + }, + '/regions/{regionCode}/districts': { + get: { + summary: 'List region districts', + parameters: [ + { in: 'path', name: 'regionCode', required: true, schema: { type: 'integer' } }, + { in: 'query', name: 'page', schema: { type: 'integer', minimum: 1 } }, + { in: 'query', name: 'limit', schema: { type: 'integer', minimum: 1, maximum: 100 } }, + ], + responses: { + '200': { description: 'District list for the region' }, + '404': { description: 'Region not found' }, + }, + }, + }, + '/districts': { + get: { + summary: 'List districts', + parameters: [ + { in: 'query', name: 'page', schema: { type: 'integer', minimum: 1 } }, + { in: 'query', name: 'limit', schema: { type: 'integer', minimum: 1, maximum: 100 } }, + { in: 'query', name: 'search', schema: { type: 'string' } }, + { in: 'query', name: 'countryId', schema: { type: 'integer', minimum: 1 } }, + { in: 'query', name: 'regionCode', schema: { type: 'integer', minimum: 1 } }, + ], + responses: { + '200': { description: 'Districts list' }, + }, + }, + }, + '/districts/{districtCode}': { + get: { + summary: 'Get a district by code', + parameters: [{ in: 'path', name: 'districtCode', required: true, schema: { type: 'integer' } }], + responses: { + '200': { description: 'District details' }, + '404': { description: 'District not found' }, + }, + }, + }, + '/districts/{districtCode}/wards': { + get: { + summary: 'List district wards', + parameters: [{ in: 'path', name: 'districtCode', required: true, schema: { type: 'integer' } }], + responses: { + '200': { description: 'Wards for the district' }, + '404': { description: 'District not found' }, + }, + }, + }, + '/wards': { + get: { + summary: 'List wards', + parameters: [ + { in: 'query', name: 'page', schema: { type: 'integer', minimum: 1 } }, + { in: 'query', name: 'limit', schema: { type: 'integer', minimum: 1, maximum: 100 } }, + { in: 'query', name: 'search', schema: { type: 'string' } }, + { in: 'query', name: 'countryId', schema: { type: 'integer', minimum: 1 } }, + { in: 'query', name: 'regionCode', schema: { type: 'integer', minimum: 1 } }, + { in: 'query', name: 'districtCode', schema: { type: 'integer', minimum: 1 } }, + ], + responses: { + '200': { description: 'Wards list' }, + }, + }, + }, + '/wards/{wardCode}': { + get: { + summary: 'Get a ward by code', + parameters: [{ in: 'path', name: 'wardCode', required: true, schema: { type: 'integer' } }], + responses: { + '200': { description: 'Ward details' }, + '404': { description: 'Ward not found' }, + }, + }, + }, + '/wards/{wardCode}/places': { + get: { + summary: 'List ward places', + parameters: [{ in: 'path', name: 'wardCode', required: true, schema: { type: 'integer' } }], + responses: { + '200': { description: 'Places for the ward' }, + '404': { description: 'Ward not found' }, + }, + }, + }, + '/places': { + get: { + summary: 'List places', + parameters: [ + { in: 'query', name: 'page', schema: { type: 'integer', minimum: 1 } }, + { in: 'query', name: 'limit', schema: { type: 'integer', minimum: 1, maximum: 100 } }, + { in: 'query', name: 'search', schema: { type: 'string' } }, + { in: 'query', name: 'countryId', schema: { type: 'integer', minimum: 1 } }, + { in: 'query', name: 'regionCode', schema: { type: 'integer', minimum: 1 } }, + { in: 'query', name: 'districtCode', schema: { type: 'integer', minimum: 1 } }, + { in: 'query', name: 'wardCode', schema: { type: 'integer', minimum: 1 } }, + ], + responses: { + '200': { description: 'Places list' }, + }, + }, + }, + '/places/{id}': { + get: { + summary: 'Get a place by id', + parameters: [{ in: 'path', name: 'id', required: true, schema: { type: 'integer' } }], + responses: { + '200': { description: 'Place details' }, + '404': { description: 'Place not found' }, + }, + }, + }, + '/search': { + get: { + summary: 'Full-text search across the general locations view', + parameters: [{ in: 'query', name: 'q', required: true, schema: { type: 'string', minLength: 2 } }], + responses: { + '200': { description: 'Search results' }, + '400': { description: 'Invalid query', content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }, + }, + }, + }, }, - apis: ['./src/routes.ts'], }; -const swaggerSpec = swaggerJsdoc(options); +export function setupSwagger(app: Express) { + app.get('/openapi.json', (_, res) => { + res.json(openApiSpec); + }); -export const setupSwagger = (app: Express) => { - app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); -}; + app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(openApiSpec)); +} diff --git a/src/middleware/errorHandler.ts b/src/middleware/errorHandler.ts index c349d12..06ac9f4 100644 --- a/src/middleware/errorHandler.ts +++ b/src/middleware/errorHandler.ts @@ -1,6 +1,6 @@ -import { Request, Response } from 'express'; +import type { Request, Response } from 'express'; import { ZodError } from 'zod'; -import { ErrorResponse } from '../types'; +import type { ErrorResponse } from '../types.js'; export class ApiError extends Error { statusCode: number; @@ -15,44 +15,52 @@ export class ApiError extends Error { } export const errorHandler = ( - err: Error | ApiError | ZodError, - _: Request, - res: Response, + err: Error | ApiError | ZodError, + req: Request, + res: Response, ) => { - console.error(`[${new Date().toISOString()}] Error:`, err); - + console.error( + JSON.stringify({ + level: 'error', + message: err.message, + name: err.name, + requestId: req.requestId, + stack: err.stack, + }), + ); + let statusCode = 500; let message = 'Something went wrong'; - + if (err instanceof ApiError) { statusCode = err.statusCode; message = err.message; } - + if (err instanceof ZodError) { statusCode = 400; message = `Validation error: ${err.errors.map(e => e.message).join(', ')}`; } - + if ('code' in err && err.code === 'P2025') { statusCode = 404; message = 'Requested resource not found'; } - + if (err instanceof SyntaxError || err instanceof TypeError) { statusCode = 400; message = 'Invalid request data'; } - + const isProduction = process.env.NODE_ENV === 'production'; - + res.status(statusCode).json({ error: { message, - ...(isProduction ? {} : { + ...(isProduction ? {} : { stack: err.stack, - details: err instanceof ZodError ? err.errors : err - }) - } + details: err instanceof ZodError ? err.errors : err, + }), + }, }); }; diff --git a/src/middleware/requestContext.ts b/src/middleware/requestContext.ts new file mode 100644 index 0000000..2a914d1 --- /dev/null +++ b/src/middleware/requestContext.ts @@ -0,0 +1,24 @@ +import { randomUUID } from 'node:crypto'; +import type { RequestHandler } from 'express'; + +export const attachRequestContext: RequestHandler = (req, res, next) => { + const requestId = req.header('x-request-id')?.trim() || randomUUID(); + + req.requestId = requestId; + res.setHeader('X-Request-Id', requestId); + + next(); +}; + +export const apiCompatibilityHeaders: RequestHandler = (_, res, next) => { + res.setHeader('X-API-Base-Path', 'compatibility'); + res.setHeader('Link', '; rel="canonical"'); + next(); +}; + +export const cacheControl = (value: string): RequestHandler => { + return (_, res, next) => { + res.setHeader('Cache-Control', value); + next(); + }; +}; diff --git a/src/middleware/validation.ts b/src/middleware/validation.ts index dbb0da6..d1fea8d 100644 --- a/src/middleware/validation.ts +++ b/src/middleware/validation.ts @@ -1,4 +1,4 @@ -import { RequestHandler } from 'express'; +import type { RequestHandler } from 'express'; import { ZodSchema, ZodError } from 'zod'; export const validate = (schemas: { diff --git a/src/routes.ts b/src/routes.ts index a99ea4e..a656eb9 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -1,143 +1,198 @@ import { Router } from 'express'; -import { validate } from './middleware/validation'; -import { prisma } from './app'; -import { ApiError } from './middleware/errorHandler'; -import { +import { Prisma } from './generated/prisma/client.js'; +import { prisma } from './db/prisma.js'; +import { ApiError } from './middleware/errorHandler.js'; +import { cacheControl } from './middleware/requestContext.js'; +import { validate } from './middleware/validation.js'; +import { codeParamSchema, + countryCodeParamSchema, + districtsQuerySchema, idParamSchema, paginationSchema, -} from './types'; + placesQuerySchema, + regionsQuerySchema, + searchQuerySchema, + wardsQuerySchema, +} from './types.js'; const router = Router(); -// ==================== COUNTRIES ROUTES ==================== +const RESOURCE_CACHE = 'public, max-age=300, stale-while-revalidate=60'; +const SEARCH_CACHE = 'public, max-age=60, stale-while-revalidate=30'; + +interface SearchResult { + id: number; + region: string; + district: string; + ward: string; + street: string | null; + places: string | null; + regioncode: number; + districtcode: number; + wardcode: number; +} + +function toPagination(page: number, limit: number, total: number) { + return { + page, + limit, + total, + pages: Math.ceil(total / limit), + }; +} + +function contains(search?: string) { + if (!search) { + return undefined; + } + + return { + contains: search, + mode: 'insensitive' as const, + }; +} -/** - * @route GET /api/countries - * @description Get all countries with optional pagination and search - */ -router.get('/countries', validate({ query: paginationSchema }), async (req, res, next) => { +router.get('/countries', cacheControl(RESOURCE_CACHE), validate({ query: paginationSchema }), async (req, res, next) => { try { - const { page, limit, search } = req.validatedQuery; + const { limit, page, search } = req.validatedQuery; const skip = (page - 1) * limit; - const whereClause = search ? { name: { contains: search } } : {}; - - const total = await prisma.countries.count({ where: whereClause }); - - const countries = await prisma.countries.findMany({ - skip, - take: limit, - where: whereClause, - select: { - id: true, - name: true, - iso: true, - nicename: true, - phonecode: true, - numcode: true, - }, - orderBy: { name: 'asc' } - }); - + const where: Prisma.CountriesWhereInput = search + ? { + OR: [{ name: contains(search) }, { nicename: contains(search) }, { iso: contains(search) }], + } + : {}; + + const [total, countries] = await Promise.all([ + prisma.countries.count({ where }), + prisma.countries.findMany({ + skip, + take: limit, + where, + select: { + id: true, + iso: true, + name: true, + nicename: true, + phonecode: true, + numcode: true, + }, + orderBy: { name: 'asc' }, + }), + ]); + res.json({ data: countries, - pagination: { - page, - limit, - total, - pages: Math.ceil(total / limit) - } + pagination: toPagination(page, limit, total), }); } catch (error) { next(error); } }); -/** - * @route GET /api/countries/:id - * @description Get country by ID - */ -router.get('/countries/:id', validate({ params: idParamSchema }), async (req, res, next) => { +router.get('/countries/:id', cacheControl(RESOURCE_CACHE), validate({ params: idParamSchema }), async (req, res, next) => { try { - const countryId = req.params.id; - + const { id } = req.validatedParams; + const country = await prisma.countries.findUnique({ - where: { id: +countryId }, + where: { id }, select: { id: true, - name: true, iso: true, + name: true, nicename: true, phonecode: true, - numcode: true - } + numcode: true, + }, }); - + if (!country) { throw new ApiError(404, 'Country not found'); } - + res.json({ data: country }); } catch (error) { next(error); } }); -// ==================== REGIONS ROUTES ==================== +router.get( + '/countries/:countryCode/regions', + cacheControl(RESOURCE_CACHE), + validate({ params: countryCodeParamSchema }), + async (req, res, next) => { + try { + const { countryCode } = req.validatedParams; -/** - * @route GET /api/regions - * @description Get all regions with pagination and optional filtering - */ -router.get('/regions', validate({ query: paginationSchema }), async (req, res, next) => { + const country = await prisma.countries.findUnique({ + where: { id: countryCode }, + select: { id: true }, + }); + + if (!country) { + throw new ApiError(404, 'Country not found'); + } + + const regions = await prisma.regions.findMany({ + where: { countryId: countryCode }, + select: { + regionCode: true, + regionName: true, + countryId: true, + }, + orderBy: { regionName: 'asc' }, + }); + + res.json({ data: regions }); + } catch (error) { + next(error); + } + }, +); + +router.get('/regions', cacheControl(RESOURCE_CACHE), validate({ query: regionsQuerySchema }), async (req, res, next) => { try { - const { page, limit, search } = req.validatedQuery; + const { countryId, limit, page, search } = req.validatedQuery; const skip = (page - 1) * limit; - - // Handle search query if present - const whereClause = search ? { regionName: { contains: search } } : {}; - - const total = await prisma.regions.count({ where: whereClause }); - - const regions = await prisma.regions.findMany({ - skip, - take: limit, - where: whereClause, - select: { - regionCode: true, - regionName: true, - countryId: true, - countries: { - select: { - name: true - } - } - }, - orderBy: { regionName: 'asc' } - }); - + const where: Prisma.RegionsWhereInput = { + ...(countryId ? { countryId } : {}), + ...(search ? { regionName: contains(search) } : {}), + }; + + const [total, regions] = await Promise.all([ + prisma.regions.count({ where }), + prisma.regions.findMany({ + skip, + take: limit, + where, + select: { + regionCode: true, + regionName: true, + countryId: true, + countries: { + select: { + iso: true, + name: true, + }, + }, + }, + orderBy: { regionName: 'asc' }, + }), + ]); + res.json({ data: regions, - pagination: { - page, - limit, - total, - pages: Math.ceil(total / limit) - } + pagination: toPagination(page, limit, total), }); } catch (error) { next(error); } }); -/** - * @route GET /api/regions/:regionCode - * @description Get a specific region by code - */ -router.get('/regions/:regionCode', validate({ params: codeParamSchema.pick({ regionCode: true }) }), async (req, res, next) => { +router.get('/regions/:regionCode', cacheControl(RESOURCE_CACHE), validate({ params: codeParamSchema.pick({ regionCode: true }) }), async (req, res, next) => { try { - const regionCode = +req.params.regionCode; - + const { regionCode } = req.validatedParams; + const region = await prisma.regions.findUnique({ where: { regionCode }, select: { @@ -146,135 +201,117 @@ router.get('/regions/:regionCode', validate({ params: codeParamSchema.pick({ reg countryId: true, countries: { select: { + iso: true, name: true, - nicename: true - } - } - } + nicename: true, + }, + }, + }, }); - - if (!region || Object.keys(region).length === 0) { + + if (!region) { throw new ApiError(404, 'Region not found'); } - + res.json({ data: region }); } catch (error) { next(error); } }); -/** - * @route GET /api/regions/:regionCode/districts - * @description Get all districts in a region - */ -router.get('/regions/:regionCode/districts', - validate({ params: codeParamSchema.pick({ regionCode: true }) }), - validate({ query: paginationSchema }), +router.get( + '/regions/:regionCode/districts', + cacheControl(RESOURCE_CACHE), + validate({ + params: codeParamSchema.pick({ regionCode: true }), + query: paginationSchema, + }), async (req, res, next) => { try { - const regionCode = +req.params.regionCode; - const { page, limit } = req.validatedQuery; + const { regionCode } = req.validatedParams; + const { limit, page } = req.validatedQuery; const skip = (page - 1) * limit; - - // Verify region exists - const regionExists = await prisma.regions.findUnique({ + + const region = await prisma.regions.findUnique({ where: { regionCode }, - select: { regionCode: true } + select: { regionCode: true }, }); - - if (!regionExists) { + + if (!region) { throw new ApiError(404, 'Region not found'); } - - // Get districts count - const total = await prisma.districts.count({ - where: { regionId: regionCode } - }); - - // Get districts - const districts = await prisma.districts.findMany({ - where: { regionId: regionCode }, - skip, - take: limit, - select: { - districtCode: true, - districtName: true, - regionId: true - }, - orderBy: { districtName: 'asc' } - }); - + + const [total, districts] = await Promise.all([ + prisma.districts.count({ where: { regionId: regionCode } }), + prisma.districts.findMany({ + where: { regionId: regionCode }, + skip, + take: limit, + select: { + districtCode: true, + districtName: true, + regionId: true, + country_id: true, + }, + orderBy: { districtName: 'asc' }, + }), + ]); + res.json({ data: districts, - pagination: { - page, - limit, - total, - pages: Math.ceil(total / limit) - } + pagination: toPagination(page, limit, total), }); } catch (error) { next(error); } - } + }, ); -// ==================== DISTRICTS ROUTES ==================== - -/** - * @route GET /api/districts - * @description Get all districts with pagination and optional search - */ -router.get('/districts', validate({ query: paginationSchema }), async (req, res, next) => { +router.get('/districts', cacheControl(RESOURCE_CACHE), validate({ query: districtsQuerySchema }), async (req, res, next) => { try { - const { page, limit, search } = req.validatedQuery; + const { countryId, limit, page, regionCode, search } = req.validatedQuery; const skip = (page - 1) * limit; - - // Handle search if present - const whereClause = search ? { districtName: { contains: search } } : {}; - - const total = await prisma.districts.count({ where: whereClause }); - - const districts = await prisma.districts.findMany({ - skip, - take: limit, - where: whereClause, - select: { - districtCode: true, - districtName: true, - regionId: true, - country_id: true, - regions: { - select: { - regionName: true - } - } - }, - orderBy: { districtName: 'asc' } - }); - + const where: Prisma.DistrictsWhereInput = { + ...(countryId ? { country_id: countryId } : {}), + ...(regionCode ? { regionId: regionCode } : {}), + ...(search ? { districtName: contains(search) } : {}), + }; + + const [total, districts] = await Promise.all([ + prisma.districts.count({ where }), + prisma.districts.findMany({ + skip, + take: limit, + where, + select: { + districtCode: true, + districtName: true, + regionId: true, + country_id: true, + regions: { + select: { + regionCode: true, + regionName: true, + }, + }, + }, + orderBy: { districtName: 'asc' }, + }), + ]); + res.json({ data: districts, - pagination: { - page, - limit, - total, - pages: Math.ceil(total / limit) - } + pagination: toPagination(page, limit, total), }); } catch (error) { next(error); } }); -/** - * @route GET /api/districts/:districtCode - * @description Get district by code - */ -router.get('/districts/:districtCode', validate({ params: codeParamSchema.pick({ districtCode: true }) }), async (req, res, next) => { +router.get('/districts/:districtCode', cacheControl(RESOURCE_CACHE), validate({ params: codeParamSchema.pick({ districtCode: true }) }), async (req, res, next) => { try { - const districtCode = +req.params.districtCode; - + const { districtCode } = req.validatedParams; + const district = await prisma.districts.findUnique({ where: { districtCode }, select: { @@ -284,84 +321,112 @@ router.get('/districts/:districtCode', validate({ params: codeParamSchema.pick({ country_id: true, regions: { select: { - regionName: true - } + regionCode: true, + regionName: true, + }, }, countries: { select: { - name: true - } - } - } + iso: true, + name: true, + }, + }, + }, }); - + if (!district) { throw new ApiError(404, 'District not found'); } - + res.json({ data: district }); } catch (error) { next(error); } }); -// ==================== WARDS ROUTES ==================== +router.get( + '/districts/:districtCode/wards', + cacheControl(RESOURCE_CACHE), + validate({ params: codeParamSchema.pick({ districtCode: true }) }), + async (req, res, next) => { + try { + const { districtCode } = req.validatedParams; + + const district = await prisma.districts.findUnique({ + where: { districtCode }, + select: { districtCode: true }, + }); -/** - * @route GET /api/wards - * @description Get all wards with pagination and search - */ -router.get('/wards', validate({ query: paginationSchema }), async (req, res, next) => { + if (!district) { + throw new ApiError(404, 'District not found'); + } + + const wards = await prisma.wards.findMany({ + where: { districtId: districtCode }, + select: { + wardCode: true, + wardName: true, + districtId: true, + region_id: true, + country_id: true, + }, + orderBy: { wardName: 'asc' }, + }); + + res.json({ data: wards }); + } catch (error) { + next(error); + } + }, +); + +router.get('/wards', cacheControl(RESOURCE_CACHE), validate({ query: wardsQuerySchema }), async (req, res, next) => { try { - const { page, limit, search } = req.validatedQuery; + const { countryId, districtCode, limit, page, regionCode, search } = req.validatedQuery; const skip = (page - 1) * limit; - - // Handle search if present - const whereClause = search ? { wardName: { contains: search } } : {}; - - const total = await prisma.wards.count({ where: whereClause }); - - const wards = await prisma.wards.findMany({ - skip, - take: limit, - where: whereClause, - select: { - wardCode: true, - wardName: true, - districtId: true, - region_id: true, - country_id: true, - districts: { - select: { - districtName: true - } - } - }, - orderBy: { wardName: 'asc' } - }); - + const where: Prisma.WardsWhereInput = { + ...(countryId ? { country_id: countryId } : {}), + ...(districtCode ? { districtId: districtCode } : {}), + ...(regionCode ? { region_id: regionCode } : {}), + ...(search ? { wardName: contains(search) } : {}), + }; + + const [total, wards] = await Promise.all([ + prisma.wards.count({ where }), + prisma.wards.findMany({ + skip, + take: limit, + where, + select: { + wardCode: true, + wardName: true, + districtId: true, + region_id: true, + country_id: true, + districts: { + select: { + districtCode: true, + districtName: true, + }, + }, + }, + orderBy: { wardName: 'asc' }, + }), + ]); + res.json({ data: wards, - pagination: { - page, - limit, - total, - pages: Math.ceil(total / limit) - } + pagination: toPagination(page, limit, total), }); } catch (error) { next(error); } }); -/** - * @route GET /api/wards/:wardCode - * @description Get ward by code - */ -router.get('/wards/:wardCode', validate({ params: codeParamSchema.pick({ wardCode: true }) }), async (req, res, next) => { +router.get('/wards/:wardCode', cacheControl(RESOURCE_CACHE), validate({ params: codeParamSchema.pick({ wardCode: true }) }), async (req, res, next) => { try { - const wardCode = +req.params.wardCode; - + const { wardCode } = req.validatedParams; + const ward = await prisma.wards.findUnique({ where: { wardCode }, select: { @@ -372,222 +437,153 @@ router.get('/wards/:wardCode', validate({ params: codeParamSchema.pick({ wardCod country_id: true, districts: { select: { - districtName: true - } + districtCode: true, + districtName: true, + }, }, regions: { select: { - regionName: true - } + regionCode: true, + regionName: true, + }, }, countries: { select: { - name: true - } - } - } + iso: true, + name: true, + }, + }, + }, }); - + if (!ward) { throw new ApiError(404, 'Ward not found'); } - + res.json({ data: ward }); } catch (error) { next(error); } }); -// ==================== PLACES ROUTES ==================== +router.get( + '/wards/:wardCode/places', + cacheControl(RESOURCE_CACHE), + validate({ params: codeParamSchema.pick({ wardCode: true }) }), + async (req, res, next) => { + try { + const { wardCode } = req.validatedParams; + + const ward = await prisma.wards.findUnique({ + where: { wardCode }, + select: { wardCode: true }, + }); + + if (!ward) { + throw new ApiError(404, 'Ward not found'); + } + + const places = await prisma.places.findMany({ + where: { wardId: wardCode }, + select: { + id: true, + placeName: true, + wardId: true, + district_id: true, + region_id: true, + country_id: true, + }, + orderBy: { placeName: 'asc' }, + }); + + res.json({ data: places }); + } catch (error) { + next(error); + } + }, +); -/** - * @route GET /api/places - * @description Get all places with pagination and search - */ -router.get('/places', validate({ query: paginationSchema }), async (req, res, next) => { +router.get('/places', cacheControl(RESOURCE_CACHE), validate({ query: placesQuerySchema }), async (req, res, next) => { try { - const { page, limit, search } = req.validatedQuery; + const { countryId, districtCode, limit, page, regionCode, search, wardCode } = req.validatedQuery; const skip = (page - 1) * limit; - - // Handle search if present - const whereClause = search ? { placeName: { contains: search } } : {}; - - const total = await prisma.places.count({ where: whereClause }); - - const places = await prisma.places.findMany({ - skip, - take: limit, - where: whereClause, - select: { - id: true, - placeName: true, - wardId: true, - district_id: true, - region_id: true, - country_id: true - }, - orderBy: { placeName: 'asc' } - }); - + const where: Prisma.PlacesWhereInput = { + ...(countryId ? { country_id: countryId } : {}), + ...(districtCode ? { district_id: districtCode } : {}), + ...(regionCode ? { region_id: regionCode } : {}), + ...(wardCode ? { wardId: wardCode } : {}), + ...(search ? { placeName: contains(search) } : {}), + }; + + const [total, places] = await Promise.all([ + prisma.places.count({ where }), + prisma.places.findMany({ + skip, + take: limit, + where, + select: { + id: true, + placeName: true, + wardId: true, + district_id: true, + region_id: true, + country_id: true, + }, + orderBy: { placeName: 'asc' }, + }), + ]); + res.json({ data: places, - pagination: { - page, - limit, - total, - pages: Math.ceil(total / limit) - } + pagination: toPagination(page, limit, total), }); } catch (error) { next(error); } }); -/** - * @route GET /api/places/:id - * @description Get place by ID - */ -router.get('/places/:id', validate({ params: idParamSchema }), async (req, res, next) => { +router.get('/places/:id', cacheControl(RESOURCE_CACHE), validate({ params: idParamSchema }), async (req, res, next) => { try { - const placeId = +req.params.id; - + const { id } = req.validatedParams; + const place = await prisma.places.findUnique({ - where: { id: placeId }, + where: { id }, select: { id: true, placeName: true, wardId: true, district_id: true, region_id: true, - country_id: true - } + country_id: true, + }, }); - + if (!place) { throw new ApiError(404, 'Place not found'); } - - res.json({ data: place }); - } catch (error) { - next(error); - } -}); - -// ==================== NESTED ROUTES ==================== - -/** - * @route GET /api/countries/:countryCode/regions - * @description Get all regions in a given country - */ -router.get('/countries/:countryCode/regions', async (req, res, next) => { - try { - const countryCode = Number(req.params.countryCode); - const regions = await prisma.regions.findMany({ - where: { countryId: countryCode }, - orderBy: { regionName: 'asc' } - }); - - if(!regions || regions.length === 0) { - throw new ApiError(404, 'No regions found for this country'); - } - res.json({ data: regions }); - } catch (error) { - next(error); - } -}); - -/** - * @route GET /api/regions/:regionCode/districts - * @description Get all districts in a given region - */ -router.get('/regions/:regionCode/districts', async (req, res, next) => { - try { - const regionCode = Number(req.params.regionCode); - const districts = await prisma.districts.findMany({ - where: { regionId: regionCode }, - orderBy: { districtName: 'asc' } - }); - - if(!districts || districts.length === 0) { - throw new ApiError(404, 'No districts found for this region'); - } - res.json({ data: districts }); - } catch (error) { - next(error); - } -}); - -/** - * @route GET /api/districts/:districtCode/wards - * @description Get all wards in a given district - */ -router.get('/districts/:districtCode/wards', async (req, res, next) => { - try { - const districtCode = Number(req.params.districtCode); - const wards = await prisma.wards.findMany({ - where: { districtId: districtCode }, - orderBy: { wardName: 'asc' } - }); - - if(!wards || wards.length === 0) { - throw new ApiError(404, 'No wards found for this district'); - } - res.json({ data: wards }); - } catch (error) { - next(error); - } -}); - -/** - * @route GET /api/wards/:wardCode/places - * @description Get all places in a given ward - */ -router.get('/wards/:wardCode/places', async (req, res, next) => { - try { - const wardCode = Number(req.params.wardCode); - const places = await prisma.places.findMany({ - where: { wardId: wardCode }, - orderBy: { placeName: 'asc' } - }); - - if(!places || places.length === 0) { - throw new ApiError(404, 'No places found for this ward'); - } - res.json({ data: places }); + res.json({ data: place }); } catch (error) { next(error); } }); -/** - * @route GET /api/search - * @description Search for places, wards, districts, regions, or countries - */ - -router.get('/search', async (req, res: any, next) => { +router.get('/search', cacheControl(SEARCH_CACHE), validate({ query: searchQuerySchema }), async (req, res, next) => { try { - const q = req.query.q?.toString().trim(); - if (!q || q.length < 2) { - return res.status(400).json({ error: 'Query too short' }); - } + const { q } = req.validatedQuery; - const escapedQuery = q.replace(/'/g, "''"); //- Escapes single quotes - const sql = ` + const results = await prisma.$queryRaw(Prisma.sql` SELECT id, region, district, ward, street, places, regioncode, districtcode, wardcode FROM "general" - WHERE "search_vector" @@ plainto_tsquery('simple', $1) - ORDER BY ts_rank("search_vector", plainto_tsquery('simple', $1)) DESC - LIMIT 15; - `; - - const results = await prisma.$queryRawUnsafe(sql, escapedQuery); + WHERE "search_vector" @@ plainto_tsquery('simple', ${q}) + ORDER BY ts_rank("search_vector", plainto_tsquery('simple', ${q})) DESC + LIMIT 15 + `); res.json({ data: results }); - } catch (err) { - next(err); + } catch (error) { + next(error); } }); - export default router; diff --git a/src/types.ts b/src/types.ts index 9041026..9661c56 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,19 +1,52 @@ -import { z, ZodIssue } from 'zod'; +import { z } from 'zod'; +import type { ZodIssue } from 'zod'; + +const positiveInt = z.coerce.number().int().positive(); export const paginationSchema = z.object({ - page: z.coerce.number().int().positive().optional().default(1), - limit: z.coerce.number().int().positive().max(100).optional().default(10), - search: z.string().optional(), + page: positiveInt.optional().default(1), + limit: positiveInt.max(100).optional().default(10), + search: z.string().trim().min(1).optional(), }); export const idParamSchema = z.object({ - id: z.coerce.number().int().positive(), + id: positiveInt, }); export const codeParamSchema = z.object({ - regionCode: z.coerce.number().int().positive(), - districtCode: z.coerce.number().int().positive(), - wardCode: z.coerce.number().int().positive(), + regionCode: positiveInt, + districtCode: positiveInt, + wardCode: positiveInt, +}); + +export const countryCodeParamSchema = z.object({ + countryCode: positiveInt, +}); + +export const regionsQuerySchema = paginationSchema.extend({ + countryId: positiveInt.optional(), +}); + +export const districtsQuerySchema = paginationSchema.extend({ + countryId: positiveInt.optional(), + regionCode: positiveInt.optional(), +}); + +export const wardsQuerySchema = paginationSchema.extend({ + countryId: positiveInt.optional(), + districtCode: positiveInt.optional(), + regionCode: positiveInt.optional(), +}); + +export const placesQuerySchema = paginationSchema.extend({ + countryId: positiveInt.optional(), + districtCode: positiveInt.optional(), + regionCode: positiveInt.optional(), + wardCode: positiveInt.optional(), +}); + +export const searchQuerySchema = z.object({ + q: z.string().trim().min(2, 'Query must be at least 2 characters long'), }); export type IdParam = z.infer; @@ -38,7 +71,7 @@ export interface ErrorResponse { error: { message: string; stack?: string; - details?: any; + details?: unknown; validationErrors?: ZodIssue[]; }; } @@ -50,6 +83,7 @@ declare global { ReqBody = any, ReqQuery = any > { + requestId?: string; validatedQuery?: ReqQuery; validatedParams?: Params; validatedBody?: ReqBody; diff --git a/tests/locations-api.test.ts b/tests/locations-api.test.ts index 9f01531..fc79d1f 100644 --- a/tests/locations-api.test.ts +++ b/tests/locations-api.test.ts @@ -1,90 +1,121 @@ import request from 'supertest'; -import app from '../src/app'; - -describe('Tanzania Locations API', () => { - // Countries - describe('GET /api/countries', () => { - it('should return a list of countries', async () => { - const res = await request(app).get('/api/countries'); - expect(res.statusCode).toEqual(200); - expect(res.body).toHaveProperty('data'); - expect(Array.isArray(res.body.data)).toBeTruthy(); - }); +import app from '../src/app.js'; +import { disconnectPrisma, prisma } from '../src/db/prisma.js'; - it('should support pagination', async () => { - const res = await request(app).get('/api/countries?page=1&limit=5'); - expect(res.statusCode).toEqual(200); - expect(res.body.pagination).toMatchObject({ page: 1, limit: 5 }); - expect(res.body.data.length).toBeLessThanOrEqual(5); - }); +afterAll(async () => { + await disconnectPrisma(); +}); + +describe.each(['/v1', '/api'])('Tanzania Locations API (%s)', (basePath) => { + it('lists countries with pagination metadata', async () => { + const res = await request(app).get(`${basePath}/countries?page=1&limit=1`); + + expect(res.statusCode).toBe(200); + expect(res.body.pagination).toMatchObject({ page: 1, limit: 1, total: 2, pages: 2 }); + expect(res.body.data).toHaveLength(1); }); - // Regions - describe('GET /api/regions/:code', () => { - it('should return a specific region', async () => { - const res = await request(app).get('/api/regions/41'); - expect(res.statusCode).toBe(200); - expect(res.body.data).toHaveProperty('regionCode', 41); - }); + it('gets a specific region', async () => { + const res = await request(app).get(`${basePath}/regions/12`); - it('should return 404 for non-existent region', async () => { - const res = await request(app).get('/api/regions/40'); - expect(res.statusCode).toBe(404); + expect(res.statusCode).toBe(200); + expect(res.body.data).toMatchObject({ + regionCode: 12, + regionName: 'Dodoma', + countryId: 1, }); + }); + + it('supports collection filters without changing response envelopes', async () => { + const res = await request(app).get(`${basePath}/places?countryId=1®ionCode=12&districtCode=1201&wardCode=120101`); - it('should return 400 for invalid region code', async () => { - const res = await request(app).get('/api/regions/invalid'); - expect(res.statusCode).toBe(400); - expect(res.body).toHaveProperty('error'); + expect(res.statusCode).toBe(200); + expect(res.body.pagination.total).toBe(1); + expect(res.body.data[0]).toMatchObject({ + id: 2, + placeName: 'Nzuguni Center', }); }); - // Regions β†’ Districts - describe('GET /api/regions/:code/districts', () => { - it('should return districts of a specific region', async () => { - const res = await request(app).get('/api/regions/41/districts'); - expect(res.statusCode).toBe(200); - expect(Array.isArray(res.body.data)).toBeTruthy(); - }); + it('returns region districts with pagination', async () => { + const res = await request(app).get(`${basePath}/regions/12/districts?page=1&limit=10`); - it('should return 404 for region that doesn’t exist', async () => { - const res = await request(app).get('/api/regions/40/districts'); - expect(res.statusCode).toBe(404); + expect(res.statusCode).toBe(200); + expect(res.body.pagination).toMatchObject({ page: 1, limit: 10, total: 1, pages: 1 }); + expect(res.body.data[0]).toMatchObject({ + districtCode: 1201, + districtName: 'Dodoma Urban', }); }); - // Districts β†’ Wards - describe('GET /api/districts/:code/wards', () => { - it('should return wards for a district', async () => { - const res = await request(app).get('/api/districts/411/wards'); - expect(res.statusCode).toBe(200); - expect(Array.isArray(res.body.data)).toBe(true); - }); + it('returns district wards', async () => { + const res = await request(app).get(`${basePath}/districts/1201/wards`); - it('should return 404 if district not found', async () => { - const res = await request(app).get('/api/districts/00000/wards'); - expect(res.statusCode).toBe(404); - }); + expect(res.statusCode).toBe(200); + expect(res.body.data).toEqual([ + expect.objectContaining({ + wardCode: 120101, + wardName: 'Nzuguni', + }), + ]); }); - // Ward Details - describe('GET /api/wards/:code', () => { - it('should return a specific ward', async () => { - const res = await request(app).get('/api/wards/40000'); - if (res.statusCode === 200) { - expect(res.body.data).toHaveProperty('wardCode', 41115); - } else { - expect([404, 400]).toContain(res.statusCode); - } - }); + it('returns ward places', async () => { + const res = await request(app).get(`${basePath}/wards/120101/places`); + + expect(res.statusCode).toBe(200); + expect(res.body.data).toEqual([ + expect.objectContaining({ + id: 2, + placeName: 'Nzuguni Center', + }), + ]); + }); + + it('returns 400 for invalid region code', async () => { + const res = await request(app).get(`${basePath}/regions/invalid`); + + expect(res.statusCode).toBe(400); + expect(res.body.error).toBeDefined(); + }); +}); + +describe('Shared API behavior', () => { + it('keeps the /api alias active', async () => { + const res = await request(app).get('/api/countries'); + + expect(res.statusCode).toBe(200); + expect(res.headers['x-api-base-path']).toBe('compatibility'); + }); + + it('searches safely when the query contains quotes', async () => { + const res = await request(app).get('/v1/search').query({ q: "nzuguni's" }); + + expect(res.statusCode).toBe(200); + expect(Array.isArray(res.body.data)).toBe(true); }); - // 404 NOT FOUND - describe('Fallback Route', () => { - it('should return 404 for unknown route', async () => { - const res = await request(app).get('/api/unknown/route'); - expect(res.statusCode).toBe(404); - expect(res.body).toHaveProperty('error'); + it('returns seeded matches for a normal search query', async () => { + const res = await request(app).get('/v1/search').query({ q: 'nzuguni' }); + + expect(res.statusCode).toBe(200); + expect(res.body.data[0]).toMatchObject({ + ward: 'Nzuguni', + places: 'Nzuguni Center', + regioncode: 12, }); }); + + it('returns 404 for an unknown route', async () => { + const res = await request(app).get('/v1/unknown/route'); + + expect(res.statusCode).toBe(404); + expect(res.body.error).toBeDefined(); + }); + + it('reuses the Prisma singleton', async () => { + const imported = await import('../src/db/prisma.js'); + + expect(imported.prisma).toBe(prisma); + }); }); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..abdc4dc --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "outDir": "./dist" + }, + "include": ["server.ts", "src/**/*.ts"], + "exclude": ["dist", "node_modules", "tests", "scripts", "prisma", "src/generated"] +} diff --git a/tsconfig.json b/tsconfig.json index fcb8c8d..2be5142 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,18 +1,22 @@ { "compilerOptions": { - "target": "es2016", - "lib": ["es2020", "dom"], - "module": "commonjs", - "moduleResolution": "node", - "outDir": "./dist", + "target": "ES2022", + "lib": ["ES2023"], + "module": "NodeNext", + "moduleResolution": "NodeNext", "sourceMap": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, - "useDefineForClassFields": true + "useDefineForClassFields": true, + "types": ["node", "jest"], + "rootDir": ".", + "resolveJsonModule": true, + "verbatimModuleSyntax": true, + "noEmit": true }, - "include": ["src/**/*.ts", "server.ts"], - "exclude": ["node_modules"] + "include": ["server.ts", "src/**/*.ts", "tests/**/*.ts", "scripts/**/*.ts", "prisma/**/*.ts"], + "exclude": ["dist", "node_modules", "src/generated"] } diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..9c8b996 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "isolatedModules": true, + "verbatimModuleSyntax": false + } +} From 50e5da7efb78758c2f40cabfbb2150646d4f3c50 Mon Sep 17 00:00:00 2001 From: maotora Date: Mon, 16 Mar 2026 18:16:06 +0300 Subject: [PATCH 2/8] docs: refresh setup and API usage --- README.md | 164 ++++++++++++++++++++++++++---------------------------- 1 file changed, 78 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index 9385f68..11dfc5a 100644 --- a/README.md +++ b/README.md @@ -1,135 +1,127 @@ # Location Data API -A RESTful API for Tanzania location data including countries, regions, districts, wards, and places. +Compatibility-first REST API for Tanzania location data backed by PostgreSQL and Prisma ORM 7. -## Features +## What Changed -- Hierarchical location data with proper relationships -- RESTful API with clean structure -- Input validation and error handling -- Pagination and search support +- Prisma ORM 7 with a generated client in `src/generated/prisma` +- Dual API base paths: `/v1` is canonical and `/api` remains as a compatibility alias +- Reproducible Prisma migration + seed flow for local development and CI +- Request IDs, structured HTTP logs, cache headers, and safer full-text search +- Dependabot and GitHub Actions for ongoing dependency updates and verification -## Tech Stack +## Requirements -- Node.js / Express -- PostgreSQL -- Prisma ORM -- Jest & Supertest for testing +- Node.js `>=20.19.0` +- pnpm `10.7.0+` +- PostgreSQL `16+` recommended -## Getting Started +## Quick Start -### Prerequisites +1. Install dependencies. -- Node.js LTS -- [Tanzania Locations Database](https://github.com/HackEAC/tanzania-locations-db) running πŸƒπŸΏβ€β™‚οΈπŸƒπŸΏβ€β™€οΈ -- npm or yarn - -### Installation + ```bash + pnpm install + ``` -1. Clone the repository +2. Create your environment file. ```bash - git clone https://github.com/yourusername/locations-API.git - cd locations-API + cp .env.example .env ``` -2. Install dependencies +3. Start PostgreSQL and update `DATABASE_URL` if needed. + +4. Apply the checked-in schema and seed deterministic fixture data. ```bash - npm install + pnpm db:migrate + pnpm db:seed ``` -3. Create `.env` for your environment +5. Start the development server. ```bash - echo DATABASE_URL="postgresql://postgres:password@localhost:5433/locations" > .env + pnpm dev ``` - The above `DATABASE_URL` is for the [Tanzania-locations-database](https://github.com/HackEAC/tanzania-locations-db) Docker container provision. - -4. Sync up your API with the locations database: +## Useful Scripts - - **a.** Pull existing DB schema into your Prisma schema +```bash +pnpm lint +pnpm typecheck +pnpm build +pnpm test +pnpm openapi:json +``` - ```bash - pnpx prisma db pull - ``` +## API Base Paths - - **b.** Create migration init files +- `/v1`: canonical path for current integrations +- `/api`: compatibility alias for older consumers - ```bash - mkdir prisma/migrations/init - ``` +Both base paths return the same payload shapes. - - **c.** Mark the current schema as baseline +## Main Endpoints - ```bash - pnpx prisma migrate diff \ - --from-empty \ - --to-schema-datamodel prisma/schema.prisma \ - --script > prisma/migrations/init/migration.sql - ``` +### Collections - - **d.** Create migration history manually +- `GET /v1/countries` +- `GET /v1/regions` +- `GET /v1/districts` +- `GET /v1/wards` +- `GET /v1/places` - ```bash - pnpx prisma migrate resolve --applied init - ``` +### Detail Routes - βœ… Now you're synced! Future `prisma migrate dev` or `migrate deploy` will work cleanly. +- `GET /v1/countries/:id` +- `GET /v1/regions/:regionCode` +- `GET /v1/districts/:districtCode` +- `GET /v1/wards/:wardCode` +- `GET /v1/places/:id` -5. Start development server +### Nested Routes - ```bash - npm run dev - ``` +- `GET /v1/countries/:countryCode/regions` +- `GET /v1/regions/:regionCode/districts` +- `GET /v1/districts/:districtCode/wards` +- `GET /v1/wards/:wardCode/places` -6. Build application +### Search - ```bash - npm run build - ``` +- `GET /v1/search?q=nzuguni` -7. Start production server +## Collection Query Parameters - ```bash - npm run start - ``` +All collection endpoints support: -## API Endpoints +- `page` +- `limit` +- `search` -### Countries -- `GET /v1/countries` - Get all countries -- `GET /v1/countries/:id` - Get country by ID +Additional filters: -### Regions -- `GET /v1/regions` - Get all regions -- `GET /v1/regions/:regionCode` - Get region by code -- `GET /v1/regions/:regionCode/districts` - Get districts in a region +- `/regions`: `countryId` +- `/districts`: `countryId`, `regionCode` +- `/wards`: `countryId`, `regionCode`, `districtCode` +- `/places`: `countryId`, `regionCode`, `districtCode`, `wardCode` -### Districts -- `GET /v1/districts` - Get all districts -- `GET /v1/districts/:districtCode` - Get district by code -- `GET /v1/districts/:districtCode/wards` - Get wards in a district +## Docs -### Wards -- `GET /v1/wards` - Get all wards -- `GET /v1/wards/:wardCode` - Get ward by code -- `GET /v1/wards/:wardCode/places` - Get places in a ward +- Swagger UI: `http://localhost:8080/api-docs` +- OpenAPI JSON: `http://localhost:8080/openapi.json` -### Places -- `GET /v1/places` - Get all places -- `GET /v1/places/:id` - Get place by ID +## Database Notes -### Search -- `GET /v1/search?q=nzuguni` - Fulltext search for locations by name +- Prisma configuration lives in [prisma.config.ts](./prisma.config.ts) +- The baseline migration now creates the `general.search_vector` column and GIN index used by `/search` +- Seed data is intentionally small and deterministic so CI and tests can assert exact results -## Running Tests +## Dependency Automation -```bash -npm test -``` +- `.github/dependabot.yml` opens weekly update PRs for npm packages and GitHub Actions +- `.github/workflows/ci.yml` validates every PR against Postgres on Node `20.19.0` and `22` ## License -This project is licensed under the CopyLeft License – see the LICENSE file for details. +This project is licensed under the CopyLeft License. See [LICENSE](./LICENSE). From bc756b9795c13d4a8d97d067621ca56f3d423d75 Mon Sep 17 00:00:00 2001 From: maotora Date: Mon, 16 Mar 2026 18:16:16 +0300 Subject: [PATCH 3/8] ci: add dependency update and verification workflows --- .github/dependabot.yml | 33 +++++++++++++++++++++ .github/workflows/ci.yml | 64 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1bfd42d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,33 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + day: monday + time: '06:00' + timezone: Africa/Dar_es_Salaam + open-pull-requests-limit: 10 + groups: + npm-minor-patch: + patterns: + - '*' + update-types: + - minor + - patch + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + time: '06:15' + timezone: Africa/Dar_es_Salaam + open-pull-requests-limit: 5 + groups: + github-actions-minor-patch: + patterns: + - '*' + update-types: + - minor + - patch diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f79ac81 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,64 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + - master + +jobs: + verify: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: + - '20.19.0' + - '22' + services: + postgres: + image: postgres:16 + env: + POSTGRES_DB: locations_api + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + options: >- + --health-cmd="pg_isready -U postgres -d locations_api" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + ports: + - 5432:5432 + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/locations_api + NODE_ENV: test + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.7.0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint, typecheck, and build + run: pnpm build:ci + + - name: Run migrations + run: pnpm db:migrate + + - name: Seed database + run: pnpm db:seed + + - name: Run tests + run: pnpm test From 4a1e4844597fbcd6abeb7f8a310102415f8a3dae Mon Sep 17 00:00:00 2001 From: maotora Date: Mon, 16 Mar 2026 18:42:18 +0300 Subject: [PATCH 4/8] fix: preserve migration compatibility and seed safety --- README.md | 2 +- package.json | 8 +- .../20250411175910_cleanup/migration.sql | 32 +++ .../migration.sql | 24 ++ .../20260316190000_baseline/migration.sql | 169 -------------- prisma/migrations/init/migration.sql | 114 ++++++++++ prisma/seed.ts | 206 +++++++++--------- scripts/migrate.ts | 71 ++++++ src/db/prisma.ts | 72 ++++-- src/routes.ts | 10 +- tests/locations-api.test.ts | 6 + 11 files changed, 416 insertions(+), 298 deletions(-) create mode 100644 prisma/migrations/20250411175910_cleanup/migration.sql create mode 100644 prisma/migrations/20260316190000_add_general_search_vector/migration.sql delete mode 100644 prisma/migrations/20260316190000_baseline/migration.sql create mode 100644 prisma/migrations/init/migration.sql create mode 100644 scripts/migrate.ts diff --git a/README.md b/README.md index 11dfc5a..70237e5 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ Additional filters: ## Database Notes - Prisma configuration lives in [prisma.config.ts](./prisma.config.ts) -- The baseline migration now creates the `general.search_vector` column and GIN index used by `/search` +- The checked-in migration chain now creates the `general.search_vector` column, trigger, and GIN index used by `/search` - Seed data is intentionally small and deterministic so CI and tests can assert exact results ## Dependency Automation diff --git a/package.json b/package.json index 00b959b..c43fb38 100644 --- a/package.json +++ b/package.json @@ -10,16 +10,16 @@ "scripts": { "dev": "tsx watch server.ts", "generate": "prisma generate", - "db:migrate": "prisma migrate deploy", + "db:migrate": "tsx scripts/migrate.ts", "db:seed": "prisma db seed", "lint": "pnpm generate && eslint server.ts \"src/**/*.ts\" \"tests/**/*.ts\" \"scripts/**/*.ts\" \"prisma/**/*.ts\"", "typecheck": "pnpm generate && tsc --noEmit", "build:ci": "pnpm generate && pnpm lint && pnpm typecheck && pnpm build", "build": "pnpm generate && tsc -p tsconfig.build.json", "start": "node ./dist/server.js", - "test": "pnpm generate && NODE_OPTIONS=--experimental-vm-modules jest --runInBand", - "test:ci": "pnpm generate && pnpm db:migrate && pnpm db:seed && NODE_OPTIONS=--experimental-vm-modules jest --runInBand", - "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch", + "test": "pnpm generate && node --experimental-vm-modules ./node_modules/jest/bin/jest.js --runInBand", + "test:ci": "pnpm generate && pnpm db:migrate && pnpm db:seed && node --experimental-vm-modules ./node_modules/jest/bin/jest.js --runInBand", + "test:watch": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --watch", "openapi:json": "tsx scripts/export-openapi.ts" }, "keywords": [ diff --git a/prisma/migrations/20250411175910_cleanup/migration.sql b/prisma/migrations/20250411175910_cleanup/migration.sql new file mode 100644 index 0000000..7841308 --- /dev/null +++ b/prisma/migrations/20250411175910_cleanup/migration.sql @@ -0,0 +1,32 @@ +/* + Warnings: + + - You are about to drop the column `properties_count` on the `districts` table. All the data in the column will be lost. + - You are about to drop the column `view_count` on the `districts` table. All the data in the column will be lost. + - You are about to drop the column `watcher_count` on the `districts` table. All the data in the column will be lost. + - You are about to drop the column `properties_count` on the `places` table. All the data in the column will be lost. + - You are about to drop the column `view_count` on the `places` table. All the data in the column will be lost. + - You are about to drop the column `properties_count` on the `regions` table. All the data in the column will be lost. + - You are about to drop the column `view_count` on the `regions` table. All the data in the column will be lost. + - You are about to drop the column `watcher_count` on the `regions` table. All the data in the column will be lost. + - You are about to drop the column `properties_count` on the `wards` table. All the data in the column will be lost. + - You are about to drop the column `view_count` on the `wards` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "districts" DROP COLUMN "properties_count", +DROP COLUMN "view_count", +DROP COLUMN "watcher_count"; + +-- AlterTable +ALTER TABLE "places" DROP COLUMN "properties_count", +DROP COLUMN "view_count"; + +-- AlterTable +ALTER TABLE "regions" DROP COLUMN "properties_count", +DROP COLUMN "view_count", +DROP COLUMN "watcher_count"; + +-- AlterTable +ALTER TABLE "wards" DROP COLUMN "properties_count", +DROP COLUMN "view_count"; diff --git a/prisma/migrations/20260316190000_add_general_search_vector/migration.sql b/prisma/migrations/20260316190000_add_general_search_vector/migration.sql new file mode 100644 index 0000000..72d2bf9 --- /dev/null +++ b/prisma/migrations/20260316190000_add_general_search_vector/migration.sql @@ -0,0 +1,24 @@ +ALTER TABLE "general" +ADD COLUMN "search_vector" tsvector; + +UPDATE "general" +SET "search_vector" = to_tsvector( + 'simple'::regconfig, + concat_ws(' '::text, "region", "district", "ward", "street", "places") +); + +CREATE FUNCTION update_general_search_vector() RETURNS trigger +LANGUAGE plpgsql +AS $$ +BEGIN + NEW.search_vector := to_tsvector('simple'::regconfig, concat_ws(' '::text, NEW.region, NEW.district, NEW.ward, NEW.street, NEW.places)); + RETURN NEW; +END +$$; + +CREATE TRIGGER set_general_search_vector +BEFORE INSERT OR UPDATE ON "general" +FOR EACH ROW +EXECUTE FUNCTION update_general_search_vector(); + +CREATE INDEX "idx_general_search_vector" ON "general" USING GIN ("search_vector"); diff --git a/prisma/migrations/20260316190000_baseline/migration.sql b/prisma/migrations/20260316190000_baseline/migration.sql deleted file mode 100644 index 456cacf..0000000 --- a/prisma/migrations/20260316190000_baseline/migration.sql +++ /dev/null @@ -1,169 +0,0 @@ -CREATE TABLE "countries" ( - "id" SERIAL NOT NULL, - "iso" CHAR(2) NOT NULL, - "name" TEXT NOT NULL, - "nicename" TEXT NOT NULL, - "iso3" CHAR(3), - "numcode" INTEGER, - "phonecode" INTEGER NOT NULL, - CONSTRAINT "countries_pkey" PRIMARY KEY ("id") -); - -CREATE UNIQUE INDEX "idx_countries_iso" ON "countries"("iso"); - -CREATE TABLE "general" ( - "id" SERIAL NOT NULL, - "country_id" INTEGER, - "region" VARCHAR NOT NULL, - "regioncode" INTEGER NOT NULL, - "district" VARCHAR NOT NULL, - "districtcode" INTEGER NOT NULL, - "ward" VARCHAR NOT NULL, - "wardcode" INTEGER NOT NULL, - "street" VARCHAR, - "places" VARCHAR, - "search_vector" tsvector, - CONSTRAINT "general_pkey" PRIMARY KEY ("id") -); - -CREATE INDEX "idx_general_search_vector" ON "general" USING GIN ("search_vector"); - -CREATE FUNCTION update_general_search_vector() RETURNS trigger -LANGUAGE plpgsql -AS $$ -BEGIN - NEW.search_vector := to_tsvector('simple'::regconfig, concat_ws(' '::text, NEW.region, NEW.district, NEW.ward, NEW.street, NEW.places)); - RETURN NEW; -END -$$; - -CREATE TRIGGER set_general_search_vector -BEFORE INSERT OR UPDATE ON "general" -FOR EACH ROW -EXECUTE FUNCTION update_general_search_vector(); - -CREATE TABLE "regions" ( - "region_name" TEXT, - "region_code" INTEGER NOT NULL, - "general_locations_id" INTEGER, - "country_id" INTEGER, - CONSTRAINT "regions_pkey" PRIMARY KEY ("region_code") -); - -CREATE INDEX "idx_regions_country_id" ON "regions"("country_id"); -CREATE INDEX "idx_regions_general_id" ON "regions"("general_locations_id"); - -CREATE TABLE "districts" ( - "district_name" TEXT, - "district_code" INTEGER NOT NULL, - "general_locations_id" INTEGER, - "region_id" INTEGER, - "country_id" INTEGER, - CONSTRAINT "districts_pkey" PRIMARY KEY ("district_code") -); - -CREATE INDEX "idx_districts_country_id" ON "districts"("country_id"); -CREATE INDEX "idx_districts_general_id" ON "districts"("general_locations_id"); -CREATE INDEX "idx_districts_region_id" ON "districts"("region_id"); - -CREATE TABLE "wards" ( - "ward_name" TEXT, - "ward_code" INTEGER NOT NULL, - "district_id" INTEGER, - "region_id" INTEGER, - "country_id" INTEGER, - "general_locations_id" INTEGER, - CONSTRAINT "wards_pkey" PRIMARY KEY ("ward_code") -); - -CREATE INDEX "idx_wards_country_id" ON "wards"("country_id"); -CREATE INDEX "idx_wards_district_id" ON "wards"("district_id"); -CREATE INDEX "idx_wards_general_id" ON "wards"("general_locations_id"); -CREATE INDEX "idx_wards_region_id" ON "wards"("region_id"); - -CREATE TABLE "places" ( - "id" SERIAL NOT NULL, - "place_name" TEXT, - "ward_id" INTEGER, - "district_id" INTEGER, - "region_id" INTEGER, - "country_id" INTEGER, - "general_locations_id" INTEGER, - CONSTRAINT "places_pkey" PRIMARY KEY ("id") -); - -CREATE INDEX "idx_places_country_id" ON "places"("country_id"); -CREATE INDEX "idx_places_district_id" ON "places"("district_id"); -CREATE INDEX "idx_places_general_id" ON "places"("general_locations_id"); -CREATE INDEX "idx_places_region_id" ON "places"("region_id"); -CREATE INDEX "idx_places_ward_id" ON "places"("ward_id"); - -ALTER TABLE "regions" - ADD CONSTRAINT "regions_country_id_fkey" - FOREIGN KEY ("country_id") - REFERENCES "countries"("id") - ON DELETE NO ACTION - ON UPDATE NO ACTION; - -ALTER TABLE "regions" - ADD CONSTRAINT "regions_general_locations_id_fkey" - FOREIGN KEY ("general_locations_id") - REFERENCES "general"("id") - ON DELETE NO ACTION - ON UPDATE NO ACTION; - -ALTER TABLE "districts" - ADD CONSTRAINT "districts_country_id_fkey" - FOREIGN KEY ("country_id") - REFERENCES "countries"("id") - ON DELETE NO ACTION - ON UPDATE NO ACTION; - -ALTER TABLE "districts" - ADD CONSTRAINT "districts_general_locations_id_fkey" - FOREIGN KEY ("general_locations_id") - REFERENCES "general"("id") - ON DELETE NO ACTION - ON UPDATE NO ACTION; - -ALTER TABLE "districts" - ADD CONSTRAINT "districts_region_id_fkey" - FOREIGN KEY ("region_id") - REFERENCES "regions"("region_code") - ON DELETE NO ACTION - ON UPDATE NO ACTION; - -ALTER TABLE "wards" - ADD CONSTRAINT "wards_country_id_fkey" - FOREIGN KEY ("country_id") - REFERENCES "countries"("id") - ON DELETE NO ACTION - ON UPDATE NO ACTION; - -ALTER TABLE "wards" - ADD CONSTRAINT "wards_district_id_fkey" - FOREIGN KEY ("district_id") - REFERENCES "districts"("district_code") - ON DELETE NO ACTION - ON UPDATE NO ACTION; - -ALTER TABLE "wards" - ADD CONSTRAINT "wards_general_locations_id_fkey" - FOREIGN KEY ("general_locations_id") - REFERENCES "general"("id") - ON DELETE NO ACTION - ON UPDATE NO ACTION; - -ALTER TABLE "wards" - ADD CONSTRAINT "wards_region_id_fkey" - FOREIGN KEY ("region_id") - REFERENCES "regions"("region_code") - ON DELETE NO ACTION - ON UPDATE NO ACTION; - -ALTER TABLE "places" - ADD CONSTRAINT "places_general_locations_id_fkey" - FOREIGN KEY ("general_locations_id") - REFERENCES "general"("id") - ON DELETE NO ACTION - ON UPDATE NO ACTION; diff --git a/prisma/migrations/init/migration.sql b/prisma/migrations/init/migration.sql new file mode 100644 index 0000000..0c477af --- /dev/null +++ b/prisma/migrations/init/migration.sql @@ -0,0 +1,114 @@ +-- CreateTable +CREATE TABLE "countries" ( + "id" SERIAL NOT NULL, + "iso" CHAR(2) NOT NULL, + "name" TEXT NOT NULL, + "nicename" TEXT NOT NULL, + "iso3" CHAR(3), + "numcode" INTEGER, + "phonecode" INTEGER NOT NULL, + + CONSTRAINT "countries_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "districts" ( + "district_name" TEXT, + "district_code" INTEGER NOT NULL, + "watcher_count" INTEGER, + "view_count" INTEGER, + "general_locations_id" INTEGER, + "properties_count" INTEGER, + "region_id" INTEGER, + "country_id" INTEGER, + + CONSTRAINT "districts_pkey" PRIMARY KEY ("district_code") +); + +-- CreateTable +CREATE TABLE "places" ( + "id" SERIAL NOT NULL, + "place_name" TEXT, + "view_count" INTEGER, + "properties_count" INTEGER, + "ward_id" INTEGER, + "district_id" INTEGER, + "region_id" INTEGER, + "country_id" INTEGER, + "general_locations_id" INTEGER, + + CONSTRAINT "places_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "regions" ( + "region_name" TEXT, + "region_code" INTEGER NOT NULL, + "watcher_count" INTEGER, + "view_count" INTEGER, + "general_locations_id" INTEGER, + "properties_count" INTEGER, + "country_id" INTEGER, + + CONSTRAINT "regions_pkey" PRIMARY KEY ("region_code") +); + +-- CreateTable +CREATE TABLE "wards" ( + "ward_name" TEXT, + "ward_code" INTEGER NOT NULL, + "district_id" INTEGER, + "region_id" INTEGER, + "country_id" INTEGER, + "view_count" INTEGER, + "properties_count" INTEGER, + "general_locations_id" INTEGER, + + CONSTRAINT "wards_pkey" PRIMARY KEY ("ward_code") +); + +-- CreateTable +CREATE TABLE "general" ( + "id" SERIAL NOT NULL, + "country_id" INTEGER, + "region" VARCHAR NOT NULL, + "regioncode" INTEGER NOT NULL, + "district" VARCHAR NOT NULL, + "districtcode" INTEGER NOT NULL, + "ward" VARCHAR NOT NULL, + "wardcode" INTEGER NOT NULL, + "street" VARCHAR, + "places" VARCHAR, + + CONSTRAINT "general_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "districts" ADD CONSTRAINT "districts_country_id_fkey" FOREIGN KEY ("country_id") REFERENCES "countries"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "districts" ADD CONSTRAINT "districts_general_locations_id_fkey" FOREIGN KEY ("general_locations_id") REFERENCES "general"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "districts" ADD CONSTRAINT "districts_region_id_fkey" FOREIGN KEY ("region_id") REFERENCES "regions"("region_code") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "places" ADD CONSTRAINT "places_general_locations_id_fkey" FOREIGN KEY ("general_locations_id") REFERENCES "general"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "regions" ADD CONSTRAINT "regions_country_id_fkey" FOREIGN KEY ("country_id") REFERENCES "countries"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "regions" ADD CONSTRAINT "regions_general_locations_id_fkey" FOREIGN KEY ("general_locations_id") REFERENCES "general"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "wards" ADD CONSTRAINT "wards_country_id_fkey" FOREIGN KEY ("country_id") REFERENCES "countries"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "wards" ADD CONSTRAINT "wards_district_id_fkey" FOREIGN KEY ("district_id") REFERENCES "districts"("district_code") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "wards" ADD CONSTRAINT "wards_general_locations_id_fkey" FOREIGN KEY ("general_locations_id") REFERENCES "general"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "wards" ADD CONSTRAINT "wards_region_id_fkey" FOREIGN KEY ("region_id") REFERENCES "regions"("region_code") ON DELETE NO ACTION ON UPDATE NO ACTION; diff --git a/prisma/seed.ts b/prisma/seed.ts index 8055493..d0afa24 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,19 +1,13 @@ import { disconnectPrisma, prisma } from '../src/db/prisma.js'; async function seed() { - await prisma.$transaction([ - prisma.places.deleteMany(), - prisma.wards.deleteMany(), - prisma.districts.deleteMany(), - prisma.regions.deleteMany(), - prisma.general.deleteMany(), - prisma.countries.deleteMany(), - ]); + await prisma.$transaction(async (tx) => { + await tx.$executeRawUnsafe( + 'TRUNCATE TABLE "places", "wards", "districts", "regions", "general", "countries" RESTART IDENTITY CASCADE', + ); - await prisma.countries.createMany({ - data: [ - { - id: 1, + const tanzania = await tx.countries.create({ + data: { iso: 'TZ', iso3: 'TZA', name: 'Tanzania', @@ -21,8 +15,10 @@ async function seed() { numcode: 834, phonecode: 255, }, - { - id: 2, + }); + + const kenya = await tx.countries.create({ + data: { iso: 'KE', iso3: 'KEN', name: 'Kenya', @@ -30,14 +26,11 @@ async function seed() { numcode: 404, phonecode: 254, }, - ], - }); + }); - await prisma.general.createMany({ - data: [ - { - id: 1, - countryId: 1, + const dodomaGeneral = await tx.general.create({ + data: { + countryId: tanzania.id, region: 'Dodoma', regioncode: 12, district: 'Dodoma Urban', @@ -47,9 +40,11 @@ async function seed() { street: 'Nzuguni Road', places: 'Nzuguni Center', }, - { - id: 2, - countryId: 1, + }); + + const arushaGeneral = await tx.general.create({ + data: { + countryId: tanzania.id, region: 'Arusha', regioncode: 11, district: 'Arusha Urban', @@ -59,9 +54,11 @@ async function seed() { street: 'Clock Tower Avenue', places: 'Arusha Clock Tower', }, - { - id: 3, - countryId: 2, + }); + + const nairobiGeneral = await tx.general.create({ + data: { + countryId: kenya.id, region: 'Nairobi', regioncode: 21, district: 'Westlands', @@ -71,102 +68,103 @@ async function seed() { street: 'Westlands Road', places: 'Sarit Centre', }, - ], - }); + }); - await prisma.regions.createMany({ - data: [ - { countryId: 1, general_locations_id: 2, regionCode: 11, regionName: 'Arusha' }, - { countryId: 1, general_locations_id: 1, regionCode: 12, regionName: 'Dodoma' }, - { countryId: 2, general_locations_id: 3, regionCode: 21, regionName: 'Nairobi' }, - ], - }); + await tx.regions.createMany({ + data: [ + { countryId: tanzania.id, general_locations_id: arushaGeneral.id, regionCode: 11, regionName: 'Arusha' }, + { countryId: tanzania.id, general_locations_id: dodomaGeneral.id, regionCode: 12, regionName: 'Dodoma' }, + { countryId: kenya.id, general_locations_id: nairobiGeneral.id, regionCode: 21, regionName: 'Nairobi' }, + ], + }); - await prisma.districts.createMany({ - data: [ - { - country_id: 1, - districtCode: 1101, - districtName: 'Arusha Urban', - general_locations_id: 2, - regionId: 11, - }, - { - country_id: 1, - districtCode: 1201, - districtName: 'Dodoma Urban', - general_locations_id: 1, - regionId: 12, - }, - { - country_id: 2, - districtCode: 2101, - districtName: 'Westlands', - general_locations_id: 3, - regionId: 21, - }, - ], - }); + await tx.districts.createMany({ + data: [ + { + country_id: tanzania.id, + districtCode: 1101, + districtName: 'Arusha Urban', + general_locations_id: arushaGeneral.id, + regionId: 11, + }, + { + country_id: tanzania.id, + districtCode: 1201, + districtName: 'Dodoma Urban', + general_locations_id: dodomaGeneral.id, + regionId: 12, + }, + { + country_id: kenya.id, + districtCode: 2101, + districtName: 'Westlands', + general_locations_id: nairobiGeneral.id, + regionId: 21, + }, + ], + }); - await prisma.wards.createMany({ - data: [ - { - country_id: 1, - districtId: 1101, - general_locations_id: 2, - region_id: 11, - wardCode: 110101, - wardName: 'Kaloleni', - }, - { - country_id: 1, - districtId: 1201, - general_locations_id: 1, - region_id: 12, - wardCode: 120101, - wardName: 'Nzuguni', - }, - { - country_id: 2, - districtId: 2101, - general_locations_id: 3, - region_id: 21, - wardCode: 210101, - wardName: 'Parklands', - }, - ], - }); + await tx.wards.createMany({ + data: [ + { + country_id: tanzania.id, + districtId: 1101, + general_locations_id: arushaGeneral.id, + region_id: 11, + wardCode: 110101, + wardName: 'Kaloleni', + }, + { + country_id: tanzania.id, + districtId: 1201, + general_locations_id: dodomaGeneral.id, + region_id: 12, + wardCode: 120101, + wardName: 'Nzuguni', + }, + { + country_id: kenya.id, + districtId: 2101, + general_locations_id: nairobiGeneral.id, + region_id: 21, + wardCode: 210101, + wardName: 'Parklands', + }, + ], + }); - await prisma.places.createMany({ - data: [ - { - country_id: 1, + await tx.places.create({ + data: { + country_id: tanzania.id, district_id: 1101, - general_locations_id: 2, - id: 1, + general_locations_id: arushaGeneral.id, placeName: 'Arusha Clock Tower', region_id: 11, wardId: 110101, }, - { - country_id: 1, + }); + + await tx.places.create({ + data: { + country_id: tanzania.id, district_id: 1201, - general_locations_id: 1, - id: 2, + general_locations_id: dodomaGeneral.id, placeName: 'Nzuguni Center', region_id: 12, wardId: 120101, }, - { - country_id: 2, + }); + + await tx.places.create({ + data: { + country_id: kenya.id, district_id: 2101, - general_locations_id: 3, - id: 3, + general_locations_id: nairobiGeneral.id, placeName: 'Sarit Centre', region_id: 21, wardId: 210101, }, - ], + }); }); } diff --git a/scripts/migrate.ts b/scripts/migrate.ts new file mode 100644 index 0000000..a5a25c7 --- /dev/null +++ b/scripts/migrate.ts @@ -0,0 +1,71 @@ +import { spawnSync } from 'node:child_process'; +import { Pool } from 'pg'; +import config from '../src/config.js'; + +const pnpmCommand = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'; + +function runPrisma(args: string[]) { + const result = spawnSync( + pnpmCommand, + ['exec', 'prisma', ...args], + { + env: process.env, + stdio: 'inherit', + }, + ); + + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +async function bootstrapIfNeeded() { + const pool = new Pool({ + connectionString: config.databaseUrl, + }); + + try { + const [{ migrationsTableExists, countriesTableExists }] = ( + await pool.query<{ + migrationsTableExists: string | null; + countriesTableExists: string | null; + }>( + ` + SELECT + to_regclass('public._prisma_migrations') AS "migrationsTableExists", + to_regclass('public.countries') AS "countriesTableExists" + `, + ) + ).rows; + + const initApplied = migrationsTableExists + ? ( + await pool.query<{ exists: boolean }>( + `SELECT EXISTS( + SELECT 1 + FROM "_prisma_migrations" + WHERE migration_name = 'init' + ) AS "exists"`, + ) + ).rows[0]?.exists ?? false + : false; + + if (!initApplied) { + if (!countriesTableExists) { + runPrisma([ + 'db', + 'execute', + '--file', + 'prisma/migrations/init/migration.sql', + ]); + } + + runPrisma(['migrate', 'resolve', '--applied', 'init']); + } + } finally { + await pool.end(); + } +} + +await bootstrapIfNeeded(); +runPrisma(['migrate', 'deploy']); diff --git a/src/db/prisma.ts b/src/db/prisma.ts index 5809fef..5ca1c0f 100644 --- a/src/db/prisma.ts +++ b/src/db/prisma.ts @@ -5,29 +5,71 @@ import config from '../config.js'; const globalForPrisma = globalThis as typeof globalThis & { pgPool?: Pool; - prisma?: PrismaClient; + prismaClient?: PrismaClient; }; -const pool = - globalForPrisma.pgPool ?? - new Pool({ +let pool = globalForPrisma.pgPool; +let prismaClient = globalForPrisma.prismaClient; + +function createPool() { + return new Pool({ connectionString: config.databaseUrl, }); +} -const adapter = new PrismaPg(pool as unknown as ConstructorParameters[0]); - -export const prisma = - globalForPrisma.prisma ?? - new PrismaClient({ - adapter, +function createPrismaClient(nextPool: Pool) { + return new PrismaClient({ + adapter: new PrismaPg(nextPool as unknown as ConstructorParameters[0]), }); +} + +function cacheInstances() { + if (config.nodeEnv !== 'production') { + globalForPrisma.pgPool = pool; + globalForPrisma.prismaClient = prismaClient; + } +} + +function ensurePrismaClient(): PrismaClient { + if (!pool) { + pool = createPool(); + } -if (config.nodeEnv !== 'production') { - globalForPrisma.pgPool = pool; - globalForPrisma.prisma = prisma; + if (!prismaClient) { + prismaClient = createPrismaClient(pool); + cacheInstances(); + } + + return prismaClient; +} + +export const prisma = new Proxy({} as PrismaClient, { + get(_, property, receiver) { + return Reflect.get(ensurePrismaClient() as object, property, receiver) as unknown; + }, + set(_, property, value, receiver) { + return Reflect.set(ensurePrismaClient() as object, property, value, receiver); + }, +}); + +if (pool && prismaClient && config.nodeEnv !== 'production') { + cacheInstances(); } export async function disconnectPrisma() { - await prisma.$disconnect(); - await pool.end(); + if (prismaClient) { + await prismaClient.$disconnect(); + } + + if (pool) { + await pool.end(); + } + + pool = undefined; + prismaClient = undefined; + + if (config.nodeEnv !== 'production') { + delete globalForPrisma.pgPool; + delete globalForPrisma.prismaClient; + } } diff --git a/src/routes.ts b/src/routes.ts index a656eb9..7c4fafe 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -57,7 +57,7 @@ router.get('/countries', cacheControl(RESOURCE_CACHE), validate({ query: paginat try { const { limit, page, search } = req.validatedQuery; const skip = (page - 1) * limit; - const where: Prisma.CountriesWhereInput = search + const where = search ? { OR: [{ name: contains(search) }, { nicename: contains(search) }, { iso: contains(search) }], } @@ -154,7 +154,7 @@ router.get('/regions', cacheControl(RESOURCE_CACHE), validate({ query: regionsQu try { const { countryId, limit, page, search } = req.validatedQuery; const skip = (page - 1) * limit; - const where: Prisma.RegionsWhereInput = { + const where = { ...(countryId ? { countryId } : {}), ...(search ? { regionName: contains(search) } : {}), }; @@ -271,7 +271,7 @@ router.get('/districts', cacheControl(RESOURCE_CACHE), validate({ query: distric try { const { countryId, limit, page, regionCode, search } = req.validatedQuery; const skip = (page - 1) * limit; - const where: Prisma.DistrictsWhereInput = { + const where = { ...(countryId ? { country_id: countryId } : {}), ...(regionCode ? { regionId: regionCode } : {}), ...(search ? { districtName: contains(search) } : {}), @@ -384,7 +384,7 @@ router.get('/wards', cacheControl(RESOURCE_CACHE), validate({ query: wardsQueryS try { const { countryId, districtCode, limit, page, regionCode, search } = req.validatedQuery; const skip = (page - 1) * limit; - const where: Prisma.WardsWhereInput = { + const where = { ...(countryId ? { country_id: countryId } : {}), ...(districtCode ? { districtId: districtCode } : {}), ...(regionCode ? { region_id: regionCode } : {}), @@ -507,7 +507,7 @@ router.get('/places', cacheControl(RESOURCE_CACHE), validate({ query: placesQuer try { const { countryId, districtCode, limit, page, regionCode, search, wardCode } = req.validatedQuery; const skip = (page - 1) * limit; - const where: Prisma.PlacesWhereInput = { + const where = { ...(countryId ? { country_id: countryId } : {}), ...(districtCode ? { district_id: districtCode } : {}), ...(regionCode ? { region_id: regionCode } : {}), diff --git a/tests/locations-api.test.ts b/tests/locations-api.test.ts index fc79d1f..36554d7 100644 --- a/tests/locations-api.test.ts +++ b/tests/locations-api.test.ts @@ -118,4 +118,10 @@ describe('Shared API behavior', () => { expect(imported.prisma).toBe(prisma); }); + + it('recreates the Prisma client after disconnect', async () => { + await disconnectPrisma(); + + await expect(prisma.countries.count()).resolves.toBe(2); + }); }); From f0f6091d2a68e2bb7e7325c04504537b70d0eb99 Mon Sep 17 00:00:00 2001 From: maotora Date: Mon, 16 Mar 2026 18:54:41 +0300 Subject: [PATCH 5/8] docs: expand migration and testing guidance --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index 70237e5..f10888d 100644 --- a/README.md +++ b/README.md @@ -48,13 +48,35 @@ Compatibility-first REST API for Tanzania location data backed by PostgreSQL and ## Useful Scripts ```bash +pnpm db:migrate +pnpm db:seed pnpm lint pnpm typecheck pnpm build pnpm test +pnpm test:ci pnpm openapi:json ``` +## Migration Behavior + +- `pnpm db:migrate` is the supported entrypoint for schema changes in this repo +- On a fresh database it bootstraps the historical `init` migration, marks that baseline as applied, and then deploys later migrations +- On an existing database that already has the older Prisma migration history, it only applies the new additive migrations +- Prefer `pnpm db:migrate` over calling `prisma migrate deploy` directly + +## Testing + +- `pnpm test` expects a database that has already been migrated and seeded +- `pnpm test:ci` runs `generate`, `db:migrate`, `db:seed`, and the Jest suite in one command +- For a clean local verification flow, run: + + ```bash + pnpm db:migrate + pnpm db:seed + pnpm test + ``` + ## API Base Paths - `/v1`: canonical path for current integrations @@ -110,12 +132,14 @@ Additional filters: - Swagger UI: `http://localhost:8080/api-docs` - OpenAPI JSON: `http://localhost:8080/openapi.json` +- `pnpm openapi:json` exports the spec to `generated/openapi/openapi.json` ## Database Notes - Prisma configuration lives in [prisma.config.ts](./prisma.config.ts) - The checked-in migration chain now creates the `general.search_vector` column, trigger, and GIN index used by `/search` - Seed data is intentionally small and deterministic so CI and tests can assert exact results +- The seed is destructive by design for local/CI fixture setup; do not run it against a database you expect to preserve unchanged ## Dependency Automation From 39dbdbbb6a565cd2fc12404b81348aaea69a7582 Mon Sep 17 00:00:00 2001 From: Ano Rebel Date: Mon, 16 Mar 2026 23:06:46 +0300 Subject: [PATCH 6/8] fix: remove 'as const', improve search query safety and clarity - Remove unnecessary 'as const' assertion from mode: 'insensitive' in contains() - Strip null bytes from search input before passing to plainto_tsquery - Refactor search SQL to use a CTE so plainto_tsquery is evaluated once - Move seed destructiveness warning to step 4 where db:seed is first introduced --- README.md | 2 ++ src/routes.ts | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f10888d..a666012 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ Compatibility-first REST API for Tanzania location data backed by PostgreSQL and pnpm db:seed ``` + > ⚠️ **WARNING**: `pnpm db:seed` is destructive β€” it truncates all tables before inserting fixture data. Do not run it against a database you need to preserve. + 5. Start the development server. ```bash diff --git a/src/routes.ts b/src/routes.ts index 7c4fafe..d806bd9 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -49,7 +49,7 @@ function contains(search?: string) { return { contains: search, - mode: 'insensitive' as const, + mode: 'insensitive', }; } @@ -572,11 +572,18 @@ router.get('/search', cacheControl(SEARCH_CACHE), validate({ query: searchQueryS try { const { q } = req.validatedQuery; + // Strip null bytes that could bypass text processing + const sanitised = q.replace(/\0/g, ''); + const results = await prisma.$queryRaw(Prisma.sql` - SELECT id, region, district, ward, street, places, regioncode, districtcode, wardcode - FROM "general" - WHERE "search_vector" @@ plainto_tsquery('simple', ${q}) - ORDER BY ts_rank("search_vector", plainto_tsquery('simple', ${q})) DESC + WITH query AS ( + SELECT plainto_tsquery('simple', ${sanitised}::text) AS tsq + ) + SELECT g.id, g.region, g.district, g.ward, g.street, g.places, + g.regioncode, g.districtcode, g.wardcode + FROM "general" g, query + WHERE g."search_vector" @@ query.tsq + ORDER BY ts_rank(g."search_vector", query.tsq) DESC LIMIT 15 `); From eee3d4c68a9655fb2ab28c0590463bb5a4f460b8 Mon Sep 17 00:00:00 2001 From: Ano Rebel Date: Mon, 16 Mar 2026 23:24:12 +0300 Subject: [PATCH 7/8] fix: resolve no-unsafe-call lint error in search route String(q) avoids calling .replace() on an any-typed value from req.validatedQuery, satisfying @typescript-eslint/no-unsafe-call which is enabled via recommendedTypeChecked but was not disabled in eslint.config.js. --- src/routes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes.ts b/src/routes.ts index d806bd9..27ff1b4 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -573,7 +573,7 @@ router.get('/search', cacheControl(SEARCH_CACHE), validate({ query: searchQueryS const { q } = req.validatedQuery; // Strip null bytes that could bypass text processing - const sanitised = q.replace(/\0/g, ''); + const sanitised = String(q).replace(/\0/g, ''); const results = await prisma.$queryRaw(Prisma.sql` WITH query AS ( From 4a3eaba92ecfca5d95a00a7b86097281efc0f5d4 Mon Sep 17 00:00:00 2001 From: Ano Rebel Date: Mon, 16 Mar 2026 23:52:32 +0300 Subject: [PATCH 8/8] fix: restore 'as const' on mode: 'insensitive' for Prisma type compatibility The 'as const' assertion is required so TypeScript narrows the string literal to the QueryMode union expected by Prisma's StringNullableFilter. Without it, mode is typed as plain 'string' which is not assignable. --- src/routes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes.ts b/src/routes.ts index 27ff1b4..f7503e8 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -49,7 +49,7 @@ function contains(search?: string) { return { contains: search, - mode: 'insensitive', + mode: 'insensitive' as const, }; }