diff --git a/.dockerignore b/.dockerignore index 857e3cd3..75521b96 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,8 @@ -* -!apps/* -!packages/* +* +!apps/ +!apps/** +!lib/ +!lib/** !package.json !pnpm-lock.yaml -!pnpm-workspace.yaml \ No newline at end of file +!pnpm-workspace.yaml diff --git a/.github/workflows/backend-ci.yaml b/.github/workflows/backend-ci.yaml index a6f1e5fe..102aa814 100644 --- a/.github/workflows/backend-ci.yaml +++ b/.github/workflows/backend-ci.yaml @@ -28,14 +28,9 @@ jobs: run: | pnpm install - - name: Prisma generate - run: | - cd apps/backend - pnpm prisma:generate - - name: Build run: | - cd apps/backend + cd apps/backend pnpm build - name: Test @@ -110,20 +105,15 @@ jobs: cat ".env.local" echo "----------------------" - - name: Prisma generate - run: | - cd apps/backend - pnpm prisma:generate - - name: Run migrations run: | - cd apps/backend - pnpm prisma:migrate:deploy + cd apps/database + pnpm migrate:deploy - name: Run seeds run: | - cd apps/backend - pnpm prisma:seed + cd apps/database + pnpm seed - name: Test e2e run: | diff --git a/.github/workflows/check-format.yaml b/.github/workflows/check-format.yaml index a8a5e1d2..67507882 100644 --- a/.github/workflows/check-format.yaml +++ b/.github/workflows/check-format.yaml @@ -20,7 +20,7 @@ jobs: - uses: pnpm/action-setup@v3 - - name: Check JS format + - name: Check Biome run: | pnpm install - pnpm format:check + pnpm biome:check diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index b564ed20..00000000 --- a/.prettierignore +++ /dev/null @@ -1,11 +0,0 @@ -# app is formatted with swiftformat -./apps/mobile - -node_modules -.next -pnpm-lock.yaml - -schema.gql - -apps/backend/dist -apps/mobile/metro-now/build \ No newline at end of file diff --git a/README.md b/README.md index cd76b505..4697a2ce 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,10 @@ ```shell pnpm i + +# Start PostgreSQL and Redis for development +pnpm docker:up:dev + pnpm dev cd apps/backend diff --git a/apps/backend/.env.local.example b/apps/backend/.env.local.example index eb2a3c5d..888b4373 100644 --- a/apps/backend/.env.local.example +++ b/apps/backend/.env.local.example @@ -11,14 +11,6 @@ DB_HOST=localhost DB_PORT=5432 DB_SCHEMA=public -# This was inserted by `prisma init`: -# Environment variables declared in this file are automatically made available to Prisma. -# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema - -# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. -# See the documentation for all the connection string options: https://pris.ly/d/connection-strings - - DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DB_HOST}:${DB_PORT}/${POSTGRES_DB}?schema=${DB_SCHEMA}" # redis diff --git a/apps/backend/.eslintignore b/apps/backend/.eslintignore deleted file mode 100644 index a709d76e..00000000 --- a/apps/backend/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -**/*.generated* diff --git a/apps/backend/.eslintrc.js b/apps/backend/.eslintrc.js deleted file mode 100644 index dd8a61bc..00000000 --- a/apps/backend/.eslintrc.js +++ /dev/null @@ -1,68 +0,0 @@ -module.exports = { - parser: "@typescript-eslint/parser", - parserOptions: { - project: "tsconfig.json", - tsconfigRootDir: __dirname, - sourceType: "module", - }, - plugins: [], - extends: [ - "plugin:@typescript-eslint/recommended", - "plugin:prettier/recommended", - "plugin:import/recommended", - "plugin:import/typescript", - ], - root: true, - env: { - node: true, - jest: true, - }, - ignorePatterns: [".eslintrc.js"], - settings: { - "import/resolver": { - typescript: { - alwaysTryTypes: true, - project: "apps/backend/tsconfig.json", - }, - node: { - moduleDirectory: ["node_modules", "src/*"], - }, - }, - }, - 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", - "no-restricted-imports": [ - "error", - { - patterns: [ - { - group: [".*"], - message: - "Relative imports are not allowed, use absolute import instead.", - }, - ], - }, - ], - "sort-imports": [ - "error", - { - ignoreCase: true, - ignoreDeclarationSort: true, - }, - ], - "import/order": [ - "error", - { - alphabetize: { - order: "asc", - caseInsensitive: true, - }, - "newlines-between": "always", - }, - ], - "import/newline-after-import": ["error", { count: 1 }], - }, -}; diff --git a/apps/backend/e2e/tests/import.e2e-spec.ts b/apps/backend/e2e/tests/import.e2e-spec.ts deleted file mode 100644 index 82b990a4..00000000 --- a/apps/backend/e2e/tests/import.e2e-spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { INestApplication } from "@nestjs/common"; -import { ConfigModule } from "@nestjs/config"; -import { Test, TestingModule } from "@nestjs/testing"; - -import { configModuleConfig } from "src/config/config-module.config"; -import { ImportService } from "src/modules/import/import.service"; -import { PrismaModule } from "src/modules/prisma/prisma.module"; - -describe("Import Module (e2e)", () => { - let app: INestApplication; - - beforeAll(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [ConfigModule.forRoot(configModuleConfig), PrismaModule], - providers: [ImportService], - }).compile(); - - app = moduleFixture.createNestApplication(); - await app.init(); - }); - - afterAll(async () => { - await app.close(); - }); - - test( - "Run import", - async () => { - await app.get(ImportService).syncStops(); - }, - 10 * 60 * 1_000, - ); -}); diff --git a/apps/backend/e2e/tests/platform.e2e-spec.ts b/apps/backend/e2e/tests/platform.e2e-spec.ts index bf679c37..0e82ad64 100644 --- a/apps/backend/e2e/tests/platform.e2e-spec.ts +++ b/apps/backend/e2e/tests/platform.e2e-spec.ts @@ -11,8 +11,8 @@ import { } from "e2e/utils/generate-test-urls"; import { cacheModuleConfig } from "src/config/cache-module.config"; import { configModuleConfig } from "src/config/config-module.config"; +import { DatabaseModule } from "src/modules/database/database.module"; import { PlatformModule } from "src/modules/platform/platform.module"; -import { PrismaModule } from "src/modules/prisma/prisma.module"; describe("Platform Module (e2e)", () => { let app: INestApplication; @@ -22,7 +22,7 @@ describe("Platform Module (e2e)", () => { imports: [ CacheModule.register(cacheModuleConfig), ConfigModule.forRoot(configModuleConfig), - PrismaModule, + DatabaseModule, PlatformModule, ], }).compile(); diff --git a/apps/backend/e2e/tests/stop.e2e-spec.ts b/apps/backend/e2e/tests/stop.e2e-spec.ts index a12eac4e..213fcb96 100644 --- a/apps/backend/e2e/tests/stop.e2e-spec.ts +++ b/apps/backend/e2e/tests/stop.e2e-spec.ts @@ -11,7 +11,7 @@ import { } from "e2e/utils/generate-test-urls"; import { cacheModuleConfig } from "src/config/cache-module.config"; import { configModuleConfig } from "src/config/config-module.config"; -import { PrismaModule } from "src/modules/prisma/prisma.module"; +import { DatabaseModule } from "src/modules/database/database.module"; import { StopModule } from "src/modules/stop/stop.module"; describe("Stop Module (e2e)", () => { @@ -21,7 +21,7 @@ describe("Stop Module (e2e)", () => { imports: [ CacheModule.register(cacheModuleConfig), ConfigModule.forRoot(configModuleConfig), - PrismaModule, + DatabaseModule, StopModule, ], }).compile(); diff --git a/apps/backend/package.json b/apps/backend/package.json index a58b2f65..a4d08775 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -2,103 +2,76 @@ "name": "@metro-now/backend", "scripts": { "typegen": "ts-node ./src/scripts/generate-types.ts", - "prebuild": "rimraf dist && pnpm prisma:generate && pnpm typegen", + "prebuild": "rimraf dist && pnpm --filter @metro-now/shared build && pnpm --filter @metro-now/database build && pnpm typegen", "build": "nest build", "start": "nest start", "dev": "nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"", - "lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "types:check": "tsc --incremental --noEmit", - "test": "jest", + "lint": "biome check --config-path ../../biome.json ../../apps/backend/src ../../apps/backend/package.json ../../apps/backend/tsconfig.json", + "lint:fix": "biome check --config-path ../../biome.json ../../apps/backend/src ../../apps/backend/package.json ../../apps/backend/tsconfig.json --write", + "format": "biome format --config-path ../../biome.json ../../apps/backend/src ../../apps/backend/package.json ../../apps/backend/tsconfig.json --write", + "format:check": "biome check --config-path ../../biome.json ../../apps/backend/src ../../apps/backend/package.json ../../apps/backend/tsconfig.json", + "types:check": "pnpm --filter @metro-now/shared build && pnpm --filter @metro-now/database build && tsc --incremental --noEmit", + "test": "pnpm --filter @metro-now/shared build && pnpm --filter @metro-now/database build && jest", "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./e2e/jest-e2e.json", - "prisma:studio": "dotenv -e .env.local -- prisma studio", - "prisma:generate": "prisma generate", - "prisma:migrate:create": "dotenv -e .env.local -- prisma migrate dev", - "prisma:migrate:deploy": "dotenv -e .env.local -- prisma migrate deploy", - "prisma:push": "dotenv -e .env.local -- prisma db push", - "prisma:seed": "prisma db seed" + "test:e2e": "jest --config ./e2e/jest-e2e.json" }, "dependencies": { - "@apollo/server": "^4.11.3", - "@fast-csv/parse": "^5.0.2", - "@keyv/redis": "^4.3.3", - "@nestjs/apollo": "^13.0.4", - "@nestjs/cache-manager": "^3.0.1", - "@nestjs/common": "^11.0.13", - "@nestjs/config": "^4.0.2", - "@nestjs/core": "^11.0.13", - "@nestjs/graphql": "^13.0.4", - "@nestjs/platform-express": "^11.0.13", - "@nestjs/schedule": "^5.0.1", - "@nestjs/swagger": "^11.1.1", - "@prisma/client": "5.20.0", - "cache-manager": "^6.4.2", + "@apollo/server": "^5.5.0", + "@as-integrations/express5": "^1.1.2", + "@keyv/redis": "^5.1.6", + "@metro-now/database": "workspace:*", + "@metro-now/shared": "workspace:*", + "@nestjs/apollo": "^13.2.4", + "@nestjs/cache-manager": "^3.1.0", + "@nestjs/common": "^11.1.17", + "@nestjs/config": "^4.0.3", + "@nestjs/core": "^11.1.17", + "@nestjs/graphql": "^13.2.4", + "@nestjs/platform-express": "^11.1.17", + "@nestjs/swagger": "^11.2.6", + "cacheable": "^2.3.4", + "cache-manager": "^7.2.8", "dataloader": "^2.2.3", - "graphql": "^16.10.0", - "radash": "^12.1.0", + "graphql": "^16.13.2", + "keyv": "^5.6.0", + "radash": "^12.1.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.2", "ts-morph": "^25.0.1", - "unzipper": "^0.12.3", "zod": "^3.24.2" }, "devDependencies": { - "@nestjs/cli": "^11.0.6", - "@nestjs/schematics": "^11.0.4", - "@nestjs/testing": "^11.0.13", - "@types/express": "^5.0.1", + "@nestjs/cli": "^11.0.16", + "@nestjs/schematics": "^11.0.9", + "@nestjs/testing": "^11.1.17", + "@types/express": "^5.0.6", "@types/jest": "^29.5.14", "@types/node": "^22.14.0", "@types/supertest": "^6.0.3", - "@types/unzipper": "^0.10.11", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint": "^8.57.1", - "eslint-config-prettier": "^9.1.0", - "eslint-import-resolver-typescript": "^3.10.0", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-prettier": "^5.2.6", "jest": "^29.7.0", - "prettier": "^3.5.3", - "rimraf": "^6.0.1", + "rimraf": "^6.1.3", "source-map-support": "^0.5.21", - "supertest": "^7.1.0", - "ts-jest": "^29.3.1", - "ts-loader": "^9.5.2", + "supertest": "^7.2.2", + "ts-jest": "^29.4.6", + "ts-loader": "^9.5.4", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", "typescript": "^5.8.3" }, - "optionalDependencies": { - "dotenv-cli": "^8.0.0", - "prisma": "^5.20.0" - }, "jest": { - "moduleFileExtensions": [ - "js", - "json", - "ts" - ], + "moduleFileExtensions": ["js", "json", "ts"], "rootDir": "./", - "modulePaths": [ - "" - ], + "modulePaths": [""], "testRegex": ".*\\.spec\\.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" }, - "collectCoverageFrom": [ - "**/*.(t|j)s" - ], + "collectCoverageFrom": ["**/*.(t|j)s"], "coverageDirectory": "../coverage", "testEnvironment": "node" - }, - "prisma": { - "seed": "dotenv -e .env.local -- ts-node prisma/seed.ts" } } diff --git a/apps/backend/prisma/migrations/20240702100916_init/migration.sql b/apps/backend/prisma/migrations/20240702100916_init/migration.sql deleted file mode 100644 index 21182e99..00000000 --- a/apps/backend/prisma/migrations/20240702100916_init/migration.sql +++ /dev/null @@ -1,11 +0,0 @@ --- CreateTable -CREATE TABLE "Stop" ( - "id" TEXT NOT NULL, - "name" TEXT NOT NULL, - "latitude" DOUBLE PRECISION NOT NULL, - "longitude" DOUBLE PRECISION NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Stop_pkey" PRIMARY KEY ("id") -); diff --git a/apps/backend/prisma/migrations/20240702113112_routes/migration.sql b/apps/backend/prisma/migrations/20240702113112_routes/migration.sql deleted file mode 100644 index f25c8868..00000000 --- a/apps/backend/prisma/migrations/20240702113112_routes/migration.sql +++ /dev/null @@ -1,40 +0,0 @@ --- CreateTable -CREATE TABLE "Route" ( - "id" TEXT NOT NULL, - "name" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Route_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "StopsOnRoutes" ( - "stopId" TEXT NOT NULL, - "routeId" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "StopsOnRoutes_pkey" PRIMARY KEY ("stopId","routeId") -); - --- CreateIndex -CREATE INDEX "Route_name_idx" ON "Route"("name"); - --- CreateIndex -CREATE INDEX "Stop_name_idx" ON "Stop"("name"); - --- CreateIndex -CREATE INDEX "Stop_latitude_idx" ON "Stop"("latitude"); - --- CreateIndex -CREATE INDEX "Stop_longitude_idx" ON "Stop"("longitude"); - --- CreateIndex -CREATE INDEX "Stop_latitude_longitude_idx" ON "Stop"("latitude", "longitude"); - --- AddForeignKey -ALTER TABLE "StopsOnRoutes" ADD CONSTRAINT "StopsOnRoutes_stopId_fkey" FOREIGN KEY ("stopId") REFERENCES "Stop"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "StopsOnRoutes" ADD CONSTRAINT "StopsOnRoutes_routeId_fkey" FOREIGN KEY ("routeId") REFERENCES "Route"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/backend/prisma/migrations/20240922211708_add_geo_functions/migration.sql b/apps/backend/prisma/migrations/20240922211708_add_geo_functions/migration.sql deleted file mode 100644 index 9d417236..00000000 --- a/apps/backend/prisma/migrations/20240922211708_add_geo_functions/migration.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE extension IF NOT EXISTS cube; - -CREATE extension IF NOT EXISTS earthdistance; \ No newline at end of file diff --git a/apps/backend/prisma/migrations/20240922220942_is_metro_stop_property/migration.sql b/apps/backend/prisma/migrations/20240922220942_is_metro_stop_property/migration.sql deleted file mode 100644 index ac229f61..00000000 --- a/apps/backend/prisma/migrations/20240922220942_is_metro_stop_property/migration.sql +++ /dev/null @@ -1,9 +0,0 @@ -/* -Warnings: - -- Added the required column `isMetro` to the `Stop` table without a default value. This is not possible if the table is not empty. - - */ --- AlterTable -ALTER TABLE "Stop" -ADD COLUMN "isMetro" BOOLEAN NOT NULL; \ No newline at end of file diff --git a/apps/backend/prisma/migrations/20240922222733_route_vehicle_type/migration.sql b/apps/backend/prisma/migrations/20240922222733_route_vehicle_type/migration.sql deleted file mode 100644 index df4c8598..00000000 --- a/apps/backend/prisma/migrations/20240922222733_route_vehicle_type/migration.sql +++ /dev/null @@ -1,14 +0,0 @@ --- CreateEnum -CREATE TYPE "VehicleType" AS ENUM ( - 'METRO', - 'BUS', - 'TRAM', - 'TRAIN', - 'FERRY', - 'FUNICULAR' -); - --- AlterTable -ALTER TABLE "Route" -ADD COLUMN "isNight" BOOLEAN, -ADD COLUMN "vehicleType" "VehicleType"; \ No newline at end of file diff --git a/apps/backend/prisma/migrations/20240929183801_rename_stop_to_platform/migration.sql b/apps/backend/prisma/migrations/20240929183801_rename_stop_to_platform/migration.sql deleted file mode 100644 index 496277e9..00000000 --- a/apps/backend/prisma/migrations/20240929183801_rename_stop_to_platform/migration.sql +++ /dev/null @@ -1,59 +0,0 @@ -/* - Warnings: - - - You are about to drop the `Stop` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `StopsOnRoutes` table. If the table is not empty, all the data it contains will be lost. - -*/ --- DropForeignKey -ALTER TABLE "StopsOnRoutes" DROP CONSTRAINT "StopsOnRoutes_routeId_fkey"; - --- DropForeignKey -ALTER TABLE "StopsOnRoutes" DROP CONSTRAINT "StopsOnRoutes_stopId_fkey"; - --- DropTable -DROP TABLE "Stop"; - --- DropTable -DROP TABLE "StopsOnRoutes"; - --- CreateTable -CREATE TABLE "Platform" ( - "id" TEXT NOT NULL, - "name" TEXT NOT NULL, - "isMetro" BOOLEAN NOT NULL, - "latitude" DOUBLE PRECISION NOT NULL, - "longitude" DOUBLE PRECISION NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Platform_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "PlatformsOnRoutes" ( - "stopId" TEXT NOT NULL, - "routeId" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "PlatformsOnRoutes_pkey" PRIMARY KEY ("stopId","routeId") -); - --- CreateIndex -CREATE INDEX "Platform_name_idx" ON "Platform"("name"); - --- CreateIndex -CREATE INDEX "Platform_latitude_idx" ON "Platform"("latitude"); - --- CreateIndex -CREATE INDEX "Platform_longitude_idx" ON "Platform"("longitude"); - --- CreateIndex -CREATE INDEX "Platform_latitude_longitude_idx" ON "Platform"("latitude", "longitude"); - --- AddForeignKey -ALTER TABLE "PlatformsOnRoutes" ADD CONSTRAINT "PlatformsOnRoutes_stopId_fkey" FOREIGN KEY ("stopId") REFERENCES "Platform"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "PlatformsOnRoutes" ADD CONSTRAINT "PlatformsOnRoutes_routeId_fkey" FOREIGN KEY ("routeId") REFERENCES "Route"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/backend/prisma/migrations/20240929184258_add_stop/migration.sql b/apps/backend/prisma/migrations/20240929184258_add_stop/migration.sql deleted file mode 100644 index 65563d69..00000000 --- a/apps/backend/prisma/migrations/20240929184258_add_stop/migration.sql +++ /dev/null @@ -1,32 +0,0 @@ --- DropIndex -DROP INDEX "Platform_latitude_idx"; - --- DropIndex -DROP INDEX "Platform_latitude_longitude_idx"; - --- DropIndex -DROP INDEX "Platform_longitude_idx"; - --- DropIndex -DROP INDEX "Platform_name_idx"; - --- DropIndex -DROP INDEX "Route_name_idx"; - --- AlterTable -ALTER TABLE "Platform" ADD COLUMN "stopId" TEXT; - --- CreateTable -CREATE TABLE "Stop" ( - "id" TEXT NOT NULL, - "name" TEXT NOT NULL, - "avgLatitude" DOUBLE PRECISION NOT NULL, - "avgLongitude" DOUBLE PRECISION NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Stop_pkey" PRIMARY KEY ("id") -); - --- AddForeignKey -ALTER TABLE "Platform" ADD CONSTRAINT "Platform_stopId_fkey" FOREIGN KEY ("stopId") REFERENCES "Stop"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/backend/prisma/migrations/20240929202020_stop_integration/migration.sql b/apps/backend/prisma/migrations/20240929202020_stop_integration/migration.sql deleted file mode 100644 index 9ef6a60d..00000000 --- a/apps/backend/prisma/migrations/20240929202020_stop_integration/migration.sql +++ /dev/null @@ -1,22 +0,0 @@ -/* - Warnings: - - - The primary key for the `PlatformsOnRoutes` table will be changed. If it partially fails, the table could be left without primary key constraint. - - You are about to drop the column `stopId` on the `PlatformsOnRoutes` table. All the data in the column will be lost. - - A unique constraint covering the columns `[platformId,routeId]` on the table `PlatformsOnRoutes` will be added. If there are existing duplicate values, this will fail. - - Added the required column `platformId` to the `PlatformsOnRoutes` table without a default value. This is not possible if the table is not empty. - -*/ --- DropForeignKey -ALTER TABLE "PlatformsOnRoutes" DROP CONSTRAINT "PlatformsOnRoutes_stopId_fkey"; - --- AlterTable -ALTER TABLE "PlatformsOnRoutes" DROP CONSTRAINT "PlatformsOnRoutes_pkey", -DROP COLUMN "stopId", -ADD COLUMN "platformId" TEXT NOT NULL; - --- CreateIndex -CREATE UNIQUE INDEX "PlatformsOnRoutes_platformId_routeId_key" ON "PlatformsOnRoutes"("platformId", "routeId"); - --- AddForeignKey -ALTER TABLE "PlatformsOnRoutes" ADD CONSTRAINT "PlatformsOnRoutes_platformId_fkey" FOREIGN KEY ("platformId") REFERENCES "Platform"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/backend/prisma/migrations/20241018010649_logs/migration.sql b/apps/backend/prisma/migrations/20241018010649_logs/migration.sql deleted file mode 100644 index e35ab02c..00000000 --- a/apps/backend/prisma/migrations/20241018010649_logs/migration.sql +++ /dev/null @@ -1,23 +0,0 @@ -/* - Warnings: - - - The required column `id` was added to the `PlatformsOnRoutes` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required. - -*/ --- CreateEnum -CREATE TYPE "LogType" AS ENUM ('INFO', 'ERROR'); - --- AlterTable -ALTER TABLE "PlatformsOnRoutes" ADD COLUMN "id" TEXT NOT NULL, -ADD CONSTRAINT "PlatformsOnRoutes_pkey" PRIMARY KEY ("id"); - --- CreateTable -CREATE TABLE "Log" ( - "id" BIGSERIAL NOT NULL, - "type" "LogType" NOT NULL, - "message" TEXT NOT NULL, - "trace" JSONB, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "Log_pkey" PRIMARY KEY ("id") -); diff --git a/apps/backend/prisma/migrations/20241018203041_rename_log_type_to_log_level/migration.sql b/apps/backend/prisma/migrations/20241018203041_rename_log_type_to_log_level/migration.sql deleted file mode 100644 index 742437a5..00000000 --- a/apps/backend/prisma/migrations/20241018203041_rename_log_type_to_log_level/migration.sql +++ /dev/null @@ -1,16 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `type` on the `Log` table. All the data in the column will be lost. - - Added the required column `level` to the `Log` table without a default value. This is not possible if the table is not empty. - -*/ --- CreateEnum -CREATE TYPE "LogLevel" AS ENUM ('log', 'error', 'warn', 'debug', 'verbose', 'fatal'); - --- AlterTable -ALTER TABLE "Log" DROP COLUMN "type", -ADD COLUMN "level" "LogLevel" NOT NULL; - --- DropEnum -DROP TYPE "LogType"; diff --git a/apps/backend/prisma/migrations/20241115145117_platform_code/migration.sql b/apps/backend/prisma/migrations/20241115145117_platform_code/migration.sql deleted file mode 100644 index 7c75ffc0..00000000 --- a/apps/backend/prisma/migrations/20241115145117_platform_code/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "Platform" ADD COLUMN "code" TEXT; diff --git a/apps/backend/prisma/migrations/20241205005919_request_logs/migration.sql b/apps/backend/prisma/migrations/20241205005919_request_logs/migration.sql deleted file mode 100644 index 6057dc74..00000000 --- a/apps/backend/prisma/migrations/20241205005919_request_logs/migration.sql +++ /dev/null @@ -1,13 +0,0 @@ --- CreateTable -CREATE TABLE "RequestLog" ( - "id" BIGSERIAL NOT NULL, - "method" TEXT NOT NULL, - "path" TEXT NOT NULL, - "status" INTEGER NOT NULL, - "duration" INTEGER NOT NULL, - "response" TEXT, - "userAgent" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "RequestLog_pkey" PRIMARY KEY ("id") -); diff --git a/apps/backend/prisma/migrations/20241205183812_gtfs_routes/migration.sql b/apps/backend/prisma/migrations/20241205183812_gtfs_routes/migration.sql deleted file mode 100644 index c86881cf..00000000 --- a/apps/backend/prisma/migrations/20241205183812_gtfs_routes/migration.sql +++ /dev/null @@ -1,33 +0,0 @@ --- CreateTable -CREATE TABLE "GtfsRoute" ( - "id" TEXT NOT NULL, - "type" TEXT NOT NULL, - "shortName" TEXT NOT NULL, - "longName" TEXT, - "url" TEXT, - "color" TEXT, - "isNight" BOOLEAN, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "GtfsRoute_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "GtfsRouteStop" ( - "id" TEXT NOT NULL, - "routeId" TEXT NOT NULL, - "directionId" TEXT NOT NULL, - "stopId" TEXT NOT NULL, - "stopSequence" INTEGER NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "GtfsRouteStop_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "GtfsRouteStop_routeId_directionId_stopId_stopSequence_key" ON "GtfsRouteStop"("routeId", "directionId", "stopId", "stopSequence"); - --- AddForeignKey -ALTER TABLE "GtfsRouteStop" ADD CONSTRAINT "GtfsRouteStop_routeId_fkey" FOREIGN KEY ("routeId") REFERENCES "GtfsRoute"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/backend/prisma/migrations/20250405131055_remove_logs/migration.sql b/apps/backend/prisma/migrations/20250405131055_remove_logs/migration.sql deleted file mode 100644 index 6ad96d2a..00000000 --- a/apps/backend/prisma/migrations/20250405131055_remove_logs/migration.sql +++ /dev/null @@ -1,15 +0,0 @@ -/* - Warnings: - - - You are about to drop the `Log` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `RequestLog` table. If the table is not empty, all the data it contains will be lost. - -*/ --- DropTable -DROP TABLE "Log"; - --- DropTable -DROP TABLE "RequestLog"; - --- DropEnum -DROP TYPE "LogLevel"; diff --git a/apps/backend/prisma/migrations/20250413133924_gtfs_route_stop_platform_relation/migration.sql b/apps/backend/prisma/migrations/20250413133924_gtfs_route_stop_platform_relation/migration.sql deleted file mode 100644 index 063dcc39..00000000 --- a/apps/backend/prisma/migrations/20250413133924_gtfs_route_stop_platform_relation/migration.sql +++ /dev/null @@ -1,5 +0,0 @@ --- AlterTable -ALTER TABLE "GtfsRouteStop" ALTER COLUMN "stopId" DROP NOT NULL; - --- AddForeignKey -ALTER TABLE "GtfsRouteStop" ADD CONSTRAINT "GtfsRouteStop_stopId_fkey" FOREIGN KEY ("stopId") REFERENCES "Platform"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/backend/prisma/migrations/20250413162810_required_stop_id_on_gtfs_route_stop/migration.sql b/apps/backend/prisma/migrations/20250413162810_required_stop_id_on_gtfs_route_stop/migration.sql deleted file mode 100644 index 7e1988bb..00000000 --- a/apps/backend/prisma/migrations/20250413162810_required_stop_id_on_gtfs_route_stop/migration.sql +++ /dev/null @@ -1,14 +0,0 @@ -/* - Warnings: - - - Made the column `stopId` on table `GtfsRouteStop` required. This step will fail if there are existing NULL values in that column. - -*/ --- DropForeignKey -ALTER TABLE "GtfsRouteStop" DROP CONSTRAINT "GtfsRouteStop_stopId_fkey"; - --- AlterTable -ALTER TABLE "GtfsRouteStop" ALTER COLUMN "stopId" SET NOT NULL; - --- AddForeignKey -ALTER TABLE "GtfsRouteStop" ADD CONSTRAINT "GtfsRouteStop_stopId_fkey" FOREIGN KEY ("stopId") REFERENCES "Platform"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/backend/prisma/migrations/migration_lock.toml b/apps/backend/prisma/migrations/migration_lock.toml deleted file mode 100644 index fbffa92c..00000000 --- a/apps/backend/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "postgresql" \ No newline at end of file diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma deleted file mode 100644 index ee49c0cf..00000000 --- a/apps/backend/prisma/schema.prisma +++ /dev/null @@ -1,115 +0,0 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? -// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init - -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model Stop { - id String @id - name String - - avgLatitude Float - avgLongitude Float - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - platforms Platform[] -} - -model Platform { - id String @id - name String - code String? - isMetro Boolean - - latitude Float - longitude Float - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - routes PlatformsOnRoutes[] - - stop Stop? @relation(fields: [stopId], references: [id]) - stopId String? - GtfsRouteStop GtfsRouteStop[] -} - -model Route { - id String @id - name String - - vehicleType VehicleType? - isNight Boolean? - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - stops PlatformsOnRoutes[] -} - -model PlatformsOnRoutes { - id String @id @default(uuid()) - platform Platform @relation(fields: [platformId], references: [id]) - platformId String - - route Route @relation(fields: [routeId], references: [id]) - routeId String - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@unique([platformId, routeId]) -} - -model GtfsRoute { - id String @id - type String - shortName String - longName String? - url String? - color String? - isNight Boolean? - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - GtfsRouteStop GtfsRouteStop[] -} - -model GtfsRouteStop { - id String @id @default(uuid()) - - routeId String - route GtfsRoute @relation(fields: [routeId], references: [id]) - - directionId String - - stopId String // TODO: rename to platformId - platform Platform? @relation(fields: [stopId], references: [id]) - - stopSequence Int - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@unique([routeId, directionId, stopId, stopSequence]) -} - -enum VehicleType { - METRO - BUS - TRAM - TRAIN - FERRY - FUNICULAR -} diff --git a/apps/backend/prisma/seed.ts b/apps/backend/prisma/seed.ts deleted file mode 100644 index ada2af66..00000000 --- a/apps/backend/prisma/seed.ts +++ /dev/null @@ -1,84 +0,0 @@ -import * as fs from "fs"; - -import { PrismaClient } from "@prisma/client"; - -type Stop = { - id: string; - name: string; - avgLatitude: number; - avgLongitude: number; -}; - -type Route = { - id: string; - name: string; - isNight: boolean | null; - vehicleType: null; -}; - -type Platform = { - id: string; - name: string; - isMetro: boolean; - latitude: number; - longitude: number; - stopId: string; -}; - -const parseSeedFile = (path: string): T => { - const raw = fs.readFileSync(path).toString(); - - return JSON.parse(raw); -}; - -const prisma = new PrismaClient(); -async function main() { - const stops = parseSeedFile("./prisma/seeds/stops.json"); - const routes = parseSeedFile("./prisma/seeds/routes.json"); - const platforms = parseSeedFile( - "./prisma/seeds/platforms.json", - ); - - await prisma.$transaction(async (transaction) => { - await transaction.platformsOnRoutes.deleteMany(); - await transaction.route.deleteMany(); - await transaction.platform.deleteMany(); - await transaction.stop.deleteMany(); - - await transaction.stop.createMany({ - data: stops.map((stop) => ({ - id: stop.id, - name: stop.name, - avgLongitude: stop.avgLongitude, - avgLatitude: stop.avgLatitude, - })), - }); - - await transaction.platform.createMany({ - data: platforms.map((platform) => ({ - id: platform.id, - name: platform.name, - isMetro: platform.isMetro, - latitude: platform.latitude, - longitude: platform.longitude, - stopId: platform.stopId ?? null, - })), - }); - - await transaction.route.createMany({ - data: routes.map((platform) => ({ - id: platform.id, - name: platform.name, - })), - }); - }); -} -main() - .then(async () => { - await prisma.$disconnect(); - }) - .catch(async (e) => { - console.error(e); - await prisma.$disconnect(); - process.exit(1); - }); diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index 60a91a17..6bed589f 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -3,18 +3,17 @@ import { CacheModule } from "@nestjs/cache-manager"; import { Module } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { GraphQLModule } from "@nestjs/graphql"; -import { ScheduleModule } from "@nestjs/schedule"; import { formatGraphQLError } from "src/common/graphql-error"; import { cacheModuleConfig } from "src/config/cache-module.config"; import { configModuleConfig } from "src/config/config-module.config"; import { GRAPHQL_PATH } from "src/constants/api"; +import { DatabaseModule } from "src/modules/database/database.module"; import { DepartureModule } from "src/modules/departure/departure.module"; import { HelloModule } from "src/modules/hello/hello.module"; -import { ImportModule } from "src/modules/import/import.module"; import { InfotextsModule } from "src/modules/infotexts/infotexts.module"; +import { LogModule } from "src/modules/log/log.module"; import { PlatformModule } from "src/modules/platform/platform.module"; -import { PrismaModule } from "src/modules/prisma/prisma.module"; import { RouteModule } from "src/modules/route/route.module"; import { StatusModule } from "src/modules/status/status.module"; import { StopModule } from "src/modules/stop/stop.module"; @@ -23,31 +22,21 @@ import { StopModule } from "src/modules/stop/stop.module"; imports: [ PlatformModule, DepartureModule, - ImportModule, InfotextsModule, StopModule, - PrismaModule, + DatabaseModule, StatusModule, RouteModule, + LogModule, ConfigModule.forRoot(configModuleConfig), - ScheduleModule.forRoot(), CacheModule.registerAsync(cacheModuleConfig), GraphQLModule.forRoot({ driver: ApolloDriver, - playground: true, + graphiql: process.env.NODE_ENV !== "production", typePaths: ["./**/*.graphql"], path: GRAPHQL_PATH, autoTransformHttpErrors: true, formatError: formatGraphQLError, - - // useFactory: (dataloaderService: DataloaderService) => { - // return { - // autoSchemaFile: true, - // context: () => ({ - // loaders: dataloaderService.getLoaders(), - // }), - // }; - // }, }), HelloModule, ], diff --git a/apps/backend/src/common/graphql-error.ts b/apps/backend/src/common/graphql-error.ts index bf0d8d44..0d0dc447 100644 --- a/apps/backend/src/common/graphql-error.ts +++ b/apps/backend/src/common/graphql-error.ts @@ -1,5 +1,5 @@ import { - GraphQLFormattedError, + type GraphQLFormattedError, GraphQLError as OriginalGraphQLError, } from "graphql"; diff --git a/apps/backend/src/config/cache-module.config.ts b/apps/backend/src/config/cache-module.config.ts index 1e641e1b..86865f95 100644 --- a/apps/backend/src/config/cache-module.config.ts +++ b/apps/backend/src/config/cache-module.config.ts @@ -1,13 +1,16 @@ import KeyvRedis from "@keyv/redis"; import type { CacheModuleAsyncOptions } from "@nestjs/cache-manager"; +import Keyv from "keyv"; export const cacheModuleConfig: CacheModuleAsyncOptions = { isGlobal: true, useFactory: async () => { return { stores: [ - new KeyvRedis({ - url: `redis://${process.env.REDIS_HOST || "localhost"}:${parseInt(process.env.REDIS_PORT || "6379")}`, + new Keyv({ + store: new KeyvRedis( + `redis://${process.env.REDIS_HOST || "localhost"}:${Number.parseInt(process.env.REDIS_PORT || "6379")}`, + ), }), ], }; diff --git a/apps/backend/src/constants/cache.ts b/apps/backend/src/constants/cache.ts index ba75c600..0f601e65 100644 --- a/apps/backend/src/constants/cache.ts +++ b/apps/backend/src/constants/cache.ts @@ -1,9 +1,78 @@ +const stableStringify = (value: unknown): string => { + if (value === null || typeof value !== "object") { + return JSON.stringify(value); + } + + if (Array.isArray(value)) { + return `[${value.map((item) => stableStringify(item)).join(",")}]`; + } + + const entries = Object.entries(value as Record).sort( + ([left], [right]) => left.localeCompare(right), + ); + + return `{${entries + .map( + ([key, nestedValue]) => + `${JSON.stringify(key)}:${stableStringify(nestedValue)}`, + ) + .join(",")}}`; +}; + +const buildCacheKey = (prefix: string, value?: unknown): string => { + if (value === undefined) { + return prefix; + } + + return `${prefix}.${stableStringify(value)}`; +}; + +export const uniqueStrings = ( + values: readonly Value[], +): Value[] => { + return [...new Set(values)]; +}; + +export const uniqueSortedStrings = ( + values: readonly Value[], +): Value[] => { + return uniqueStrings(values).sort((left, right) => + left.localeCompare(right), + ); +}; + export const CACHE_KEYS = { infotexts: { getAll: "infotexts.getAll", }, golemio: { - getGolemioData: (url: string) => `golemio.getGolemioData.${url}`, + getGolemioData: (url: string) => + buildCacheKey("golemio.getGolemioData", url), + }, + platform: { + getAllGraphQL: (params: unknown) => + buildCacheKey("platform.getAllGraphQL", params), + getGraphQLById: (id: string) => + buildCacheKey("platform.getGraphQLById", id), + getOne: (params: unknown) => buildCacheKey("platform.getOne", params), + }, + route: { + getManyGraphQL: (params: unknown) => + buildCacheKey("route.getManyGraphQL", params), + getManyGraphQLByPlatformId: (platformId: string) => + buildCacheKey("route.getManyGraphQLByPlatformId", platformId), + getOneGraphQL: (id: string) => buildCacheKey("route.getOneGraphQL", id), + }, + status: { + getDbDataStatus: "status.getDbDataStatus", + getGeoFunctionsStatus: "status.getGeoFunctionsStatus", + }, + stop: { + getAllGraphQL: (params: unknown) => + buildCacheKey("stop.getAllGraphQL", params), + getGraphQLById: (id: string) => + buildCacheKey("stop.getGraphQLById", id), + getOne: (params: unknown) => buildCacheKey("stop.getOne", params), }, }; diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index 2707e1cd..2af3d329 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -1,6 +1,6 @@ import { VersioningType } from "@nestjs/common"; import { NestFactory } from "@nestjs/core"; -import { NestExpressApplication } from "@nestjs/platform-express"; +import type { NestExpressApplication } from "@nestjs/platform-express"; import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger"; import { AppModule } from "src/app.module"; diff --git a/apps/backend/src/modules/database/database.module.ts b/apps/backend/src/modules/database/database.module.ts new file mode 100644 index 00000000..ff98682f --- /dev/null +++ b/apps/backend/src/modules/database/database.module.ts @@ -0,0 +1,10 @@ +import { Global, Module } from "@nestjs/common"; + +import { DatabaseService } from "src/modules/database/database.service"; + +@Global() +@Module({ + providers: [DatabaseService], + exports: [DatabaseService], +}) +export class DatabaseModule {} diff --git a/apps/backend/src/modules/database/database.service.ts b/apps/backend/src/modules/database/database.service.ts new file mode 100644 index 00000000..4aa65cf9 --- /dev/null +++ b/apps/backend/src/modules/database/database.service.ts @@ -0,0 +1,36 @@ +import { type DatabaseClient, sql } from "@metro-now/database"; +import { createDatabaseClient } from "@metro-now/shared"; +import { + Injectable, + type OnModuleDestroy, + type OnModuleInit, +} from "@nestjs/common"; + +@Injectable() +export class DatabaseService implements OnModuleInit, OnModuleDestroy { + readonly db: DatabaseClient; + + constructor() { + this.db = createDatabaseClient({ + env: process.env, + }); + } + + async onModuleInit() { + await sql`SELECT 1`.execute(this.db); + } + + async onModuleDestroy() { + await this.db.destroy(); + } + + async getExtensionNames(): Promise { + const result = await sql<{ extname: string }>` + SELECT extname + FROM pg_extension + ORDER BY extname + `.execute(this.db); + + return result.rows.map(({ extname }) => extname); + } +} diff --git a/apps/backend/src/modules/dataloader/platforms-by-stop.loader.ts b/apps/backend/src/modules/dataloader/platforms-by-stop.loader.ts index a188b293..09b2d918 100644 --- a/apps/backend/src/modules/dataloader/platforms-by-stop.loader.ts +++ b/apps/backend/src/modules/dataloader/platforms-by-stop.loader.ts @@ -3,21 +3,22 @@ import * as DataLoader from "dataloader"; import { PlatformService } from "src/modules/platform/platform.service"; +type PlatformRecord = Awaited< + ReturnType +>[number]; + @Injectable({ scope: Scope.REQUEST }) -export class PlatformsByStopLoader extends DataLoader { +export class PlatformsByStopLoader extends DataLoader< + string, + PlatformRecord | null +> { constructor(private readonly platformService: PlatformService) { super((keys) => this.batchLoadFn(keys)); } private async batchLoadFn(platformIds: readonly string[]) { - const platforms = await this.platformService.getAllGraphQL({ - metroOnly: false, - where: { - id: { - in: [...platformIds], - }, - }, - }); + const platforms = + await this.platformService.getGraphQLByIds(platformIds); const platformMap = new Map(platforms.map((p) => [p.id, p])); diff --git a/apps/backend/src/modules/dataloader/routes-by-platform.loader.ts b/apps/backend/src/modules/dataloader/routes-by-platform.loader.ts index e42e1bbc..f8d865ad 100644 --- a/apps/backend/src/modules/dataloader/routes-by-platform.loader.ts +++ b/apps/backend/src/modules/dataloader/routes-by-platform.loader.ts @@ -3,7 +3,9 @@ import * as DataLoader from "dataloader"; import { RouteService } from "src/modules/route/route.service"; -type Route = Awaited>; +type Route = Awaited< + ReturnType +>[number][number]; @Injectable({ scope: Scope.REQUEST }) export class RoutesByPlatformIdLoader extends DataLoader { @@ -14,17 +16,6 @@ export class RoutesByPlatformIdLoader extends DataLoader { private async batchLoadFn( platformIds: readonly string[], ): Promise { - const routes = await this.routeService.getManyGraphQL({ - where: { - GtfsRouteStop: { - some: { - platform: { - id: { in: [...platformIds] }, - }, - }, - }, - }, - }); - return platformIds.map(() => routes); + return this.routeService.getManyGraphQLByPlatformIds(platformIds); } } diff --git a/apps/backend/src/modules/dataloader/stop-by-platform.loader.ts b/apps/backend/src/modules/dataloader/stop-by-platform.loader.ts index e2cb1d1f..b0209f21 100644 --- a/apps/backend/src/modules/dataloader/stop-by-platform.loader.ts +++ b/apps/backend/src/modules/dataloader/stop-by-platform.loader.ts @@ -3,20 +3,19 @@ import * as DataLoader from "dataloader"; import { StopService } from "src/modules/stop/stop.service"; +type StopRecord = Awaited>[number]; + @Injectable({ scope: Scope.REQUEST }) -export class StopByPlatformLoader extends DataLoader { +export class StopByPlatformLoader extends DataLoader< + string, + StopRecord | null +> { constructor(private readonly stopService: StopService) { super((keys) => this.batchLoadFn(keys)); } private async batchLoadFn(stopIds: readonly string[]) { - const stops = await this.stopService.getAllGraphQL({ - where: { - id: { - in: [...stopIds], - }, - }, - }); + const stops = await this.stopService.getGraphQLByIds(stopIds); const stopMap = new Map(stops.map((stop) => [stop.id, stop])); diff --git a/apps/backend/src/modules/departure/departure-board.service.ts b/apps/backend/src/modules/departure/departure-board.service.ts new file mode 100644 index 00000000..36d5049b --- /dev/null +++ b/apps/backend/src/modules/departure/departure-board.service.ts @@ -0,0 +1,99 @@ +import { Injectable } from "@nestjs/common"; + +import { uniqueSortedStrings } from "src/constants/cache"; +import { DatabaseService } from "src/modules/database/database.service"; +import { departureBoardsSchema } from "src/modules/departure/schema/departure-boards.schema"; +import { GolemioService } from "src/modules/golemio/golemio.service"; + +type DepartureBoardSearchParams = Record< + string, + string | number | boolean | null | undefined +>; + +@Injectable() +export class DepartureBoardService { + constructor( + private readonly database: DatabaseService, + private readonly golemioService: GolemioService, + ) {} + + async resolvePlatformIds({ + platformIds, + stopIds, + metroOnly, + limit = 100, + }: { + platformIds: readonly string[]; + stopIds: readonly string[]; + metroOnly?: boolean; + limit?: number; + }): Promise { + const directPlatforms = + platformIds.length === 0 + ? [] + : await this.database.db + .selectFrom("Platform") + .select("id") + .where("id", "in", [...platformIds]) + .$if(Boolean(metroOnly), (query) => + query.where("isMetro", "=", true), + ) + .execute(); + const stopPlatforms = + stopIds.length === 0 + ? [] + : await this.database.db + .selectFrom("Platform") + .select("id") + .where("stopId", "in", [...stopIds]) + .$if(Boolean(metroOnly), (query) => + query.where("isMetro", "=", true), + ) + .execute(); + + return uniqueSortedStrings([ + ...directPlatforms.map((platform) => platform.id), + ...stopPlatforms.map((platform) => platform.id), + ]).slice(0, limit); + } + + async fetchDepartureBoard({ + platformIds, + params, + }: { + platformIds: readonly string[]; + params: DepartureBoardSearchParams; + }) { + const resolvedPlatformIds = uniqueSortedStrings(platformIds).slice( + 0, + 100, + ); + + if (resolvedPlatformIds.length === 0) { + return departureBoardsSchema.parse({ + departures: [], + infotexts: [], + stops: [], + }); + } + + const searchParams = new URLSearchParams( + resolvedPlatformIds + .map((id) => ["ids", id]) + .concat( + Object.entries(params) + .filter( + ([, value]) => + value !== null && value !== undefined, + ) + .map(([key, value]) => [key, String(value)]), + ), + ); + + const data = await this.golemioService.getGolemioData( + `/v2/pid/departureboards?${searchParams.toString()}`, + ); + + return departureBoardsSchema.parse(data); + } +} diff --git a/apps/backend/src/modules/departure/departure-v1.service.ts b/apps/backend/src/modules/departure/departure-v1.service.ts index f3879238..4282b3ae 100644 --- a/apps/backend/src/modules/departure/departure-v1.service.ts +++ b/apps/backend/src/modules/departure/departure-v1.service.ts @@ -1,17 +1,13 @@ import { Injectable } from "@nestjs/common"; -import { unique } from "radash"; -import { departureBoardsSchema } from "src/modules/departure/schema/departure-boards.schema"; +import { DepartureBoardService } from "src/modules/departure/departure-board.service"; import type { DepartureSchema } from "src/modules/departure/schema/departure.schema"; -import { GolemioService } from "src/modules/golemio/golemio.service"; -import { PrismaService } from "src/modules/prisma/prisma.service"; import { getDelayInSeconds } from "src/utils/delay"; @Injectable() export class DepartureServiceV1 { constructor( - private prisma: PrismaService, - private golemioService: GolemioService, + private readonly departureBoardService: DepartureBoardService, ) {} async getDepartures(args: { @@ -19,68 +15,29 @@ export class DepartureServiceV1 { platformIds: string[]; metroOnly: boolean; }): Promise { - const dbPlatforms = ( - await this.prisma.platform.findMany({ - select: { id: true }, - where: { - id: { in: args.platformIds }, - ...(args.metroOnly ? { isMetro: true } : {}), - }, - }) - ).map((platform) => platform.id); - - const stopPlatforms = ( - await this.prisma.stop.findMany({ - select: { - platforms: { - select: { id: true }, - where: { ...(args.metroOnly ? { isMetro: true } : {}) }, - }, - }, - where: { id: { in: args.stopIds } }, - }) - ).flatMap((stop) => stop.platforms.map((platform) => platform.id)); - - const allPlatformIds = unique([...dbPlatforms, ...stopPlatforms]).slice( - 0, - 100, - ); + const allPlatformIds = + await this.departureBoardService.resolvePlatformIds({ + platformIds: args.platformIds, + stopIds: args.stopIds, + ...(args.metroOnly ? { metroOnly: true } : {}), + }); if (allPlatformIds.length === 0) { return []; } - const searchParams = new URLSearchParams( - allPlatformIds - .map((id) => ["ids", id]) - .concat( - Object.entries({ - skip: "canceled", - mode: "departures", - order: "real", - minutesAfter: String(24 * 60), - }), - ), - ); - - const res = await this.golemioService.getGolemioData( - `/v2/pid/departureboards?${searchParams}`, - ); - - if (!res.ok) { - throw new Error( - `Failed to fetch departure data: ${res.status} ${res.statusText}`, - ); - } - - const json = await res.json(); - const parsed = departureBoardsSchema.safeParse(json); - - if (!parsed.success) { - throw new Error(parsed.error.message); - } + const departureBoard = + await this.departureBoardService.fetchDepartureBoard({ + platformIds: allPlatformIds, + params: { + minutesAfter: 24 * 60, + mode: "departures", + order: "real", + skip: "canceled", + }, + }); - const parsedDepartures = parsed.data.departures.map((departure) => { + const parsedDepartures = departureBoard.departures.map((departure) => { return { departure: departure.departure_timestamp, delay: getDelayInSeconds(departure.delay), diff --git a/apps/backend/src/modules/departure/departure-v2.service.ts b/apps/backend/src/modules/departure/departure-v2.service.ts index 791e303f..3cefaab4 100644 --- a/apps/backend/src/modules/departure/departure-v2.service.ts +++ b/apps/backend/src/modules/departure/departure-v2.service.ts @@ -1,18 +1,17 @@ import { Injectable } from "@nestjs/common"; -import { group, unique } from "radash"; +import { group } from "radash"; -import { departureBoardsSchema } from "src/modules/departure/schema/departure-boards.schema"; +import { DatabaseService } from "src/modules/database/database.service"; +import { DepartureBoardService } from "src/modules/departure/departure-board.service"; import type { DepartureSchema } from "src/modules/departure/schema/departure.schema"; -import { GolemioService } from "src/modules/golemio/golemio.service"; -import { PrismaService } from "src/modules/prisma/prisma.service"; -import { VehicleTypeSchema } from "src/schema/metro-only.schema"; +import type { VehicleTypeSchema } from "src/schema/metro-only.schema"; import { getDelayInSeconds } from "src/utils/delay"; @Injectable() export class DepartureServiceV2 { constructor( - private prisma: PrismaService, - private golemioService: GolemioService, + private readonly departureBoardService: DepartureBoardService, + private readonly database: DatabaseService, ) {} async getDepartures(args: { @@ -32,80 +31,53 @@ export class DepartureServiceV2 { ? { isMetro: false } : undefined; - const dbPlatforms = ( - await this.prisma.platform.findMany({ - select: { id: true }, - where: { - id: { in: args.platformIds }, - ...vehicleTypeWhere, - }, - }) - ).map((platform) => platform.id); - - const stopPlatforms = ( - await this.prisma.stop.findMany({ - select: { - platforms: { - select: { id: true }, - where: vehicleTypeWhere ?? {}, - }, - }, - where: { id: { in: args.stopIds } }, - }) - ).flatMap((stop) => stop.platforms.map((platform) => platform.id)); - - const allPlatformIds = unique([...dbPlatforms, ...stopPlatforms]).slice( - 0, - 100, - ); + const allPlatformIds = + await this.departureBoardService.resolvePlatformIds({ + platformIds: args.platformIds, + stopIds: args.stopIds, + ...(vehicleTypeWhere?.isMetro !== undefined + ? { metroOnly: vehicleTypeWhere.isMetro } + : {}), + }); if (allPlatformIds.length === 0) { return []; } - const searchParams = new URLSearchParams( - allPlatformIds - .map((id) => ["ids", id]) - .concat( - Object.entries({ - skip: "canceled", - mode: "departures", - order: "real", - minutesBefore: String(args.minutesBefore), - minutesAfter: String(args.minutesAfter), - limit: String(args.totalLimit ?? 1_000), - }), - ), - ); + const departureBoard = + await this.departureBoardService.fetchDepartureBoard({ + platformIds: allPlatformIds, + params: { + limit: args.totalLimit ?? 1_000, + minutesAfter: args.minutesAfter, + minutesBefore: args.minutesBefore, + mode: "departures", + order: "real", + skip: "canceled", + }, + }); - const res = await this.golemioService.getGolemioData( - `/v2/pid/departureboards?${searchParams.toString()}`, + const routeShortNames = [ + ...new Set( + departureBoard.departures.map( + (departure) => departure.route.short_name, + ), + ), + ]; + const gtfsRoutes = + routeShortNames.length === 0 + ? [] + : await this.database.db + .selectFrom("GtfsRoute") + .select(["id", "shortName"]) + .where("shortName", "in", routeShortNames) + .execute(); + + const gtfsRouteIdByShortName = new Map( + gtfsRoutes.map((route) => [route.shortName, route.id]), ); - if (!res.ok) { - throw new Error( - `Failed to fetch departure data: ${res.status} ${res.statusText}`, - ); - } - - const json = await res.json(); - const parsed = departureBoardsSchema.safeParse(json); - - if (!parsed.success) { - throw new Error(parsed.error.message); - } - - const gtfsRoutes = await this.prisma.gtfsRoute.findMany({ - where: { - shortName: { - in: parsed.data.departures.map( - (departure) => departure.route.short_name, - ), - }, - }, - }); - - const parsedDepartures = parsed.data.departures.map((departure) => { + const parsedDepartures = departureBoard.departures.map((departure) => { return { id: departure.trip.id, departure: departure.departure_timestamp, @@ -113,10 +85,8 @@ export class DepartureServiceV2 { headsign: departure.trip.headsign, route: departure.route.short_name, routeId: - gtfsRoutes.find( - (gtfsRoute) => - gtfsRoute.shortName === departure.route.short_name, - )?.id ?? null, + gtfsRouteIdByShortName.get(departure.route.short_name) ?? + null, platformId: departure.stop.id, platformCode: departure.stop.platform_code, }; @@ -125,24 +95,18 @@ export class DepartureServiceV2 { const limit = args.limit; const totalLimit = args.totalLimit ?? 1000; - if (limit === null && totalLimit === null) { - return parsedDepartures; - } - const limitedByPlatformAndRoute = limit !== null && limit < totalLimit ? getLimitedRes(parsedDepartures, limit) : parsedDepartures; - const resss = limitedByPlatformAndRoute + return limitedByPlatformAndRoute .sort( (a, b) => +new Date(a.departure.predicted) - +new Date(b.departure.predicted), ) - .slice(0, totalLimit ?? limitedByPlatformAndRoute.length); - - return resss; + .slice(0, totalLimit); } } diff --git a/apps/backend/src/modules/departure/departure.controller.ts b/apps/backend/src/modules/departure/departure.controller.ts index 59cacf56..4a9c3020 100644 --- a/apps/backend/src/modules/departure/departure.controller.ts +++ b/apps/backend/src/modules/departure/departure.controller.ts @@ -16,8 +16,8 @@ import { EndpointVersion } from "src/enums/endpoint-version"; import { DepartureServiceV1 } from "src/modules/departure/departure-v1.service"; import { DepartureServiceV2 } from "src/modules/departure/departure-v2.service"; import { - departureSchema, type DepartureSchema, + departureSchema, } from "src/modules/departure/schema/departure.schema"; import { metroOnlySchema, diff --git a/apps/backend/src/modules/departure/departure.module.ts b/apps/backend/src/modules/departure/departure.module.ts index 99bb0f77..4b628df2 100644 --- a/apps/backend/src/modules/departure/departure.module.ts +++ b/apps/backend/src/modules/departure/departure.module.ts @@ -1,23 +1,22 @@ import { Module } from "@nestjs/common"; +import { DepartureBoardService } from "src/modules/departure/departure-board.service"; import { DepartureServiceV1 } from "src/modules/departure/departure-v1.service"; import { DepartureServiceV2 } from "src/modules/departure/departure-v2.service"; import { DepartureController } from "src/modules/departure/departure.controller"; import { DepartureResolver } from "src/modules/departure/departure.resolver"; -import { GolemioService } from "src/modules/golemio/golemio.service"; -import { PlatformService } from "src/modules/platform/platform.service"; -import { RouteService } from "src/modules/route/route.service"; +import { GolemioModule } from "src/modules/golemio/golemio.module"; +import { PlatformModule } from "src/modules/platform/platform.module"; +import { RouteModule } from "src/modules/route/route.module"; @Module({ controllers: [DepartureController], providers: [ + DepartureBoardService, DepartureServiceV1, DepartureServiceV2, DepartureResolver, - PlatformService, - GolemioService, - RouteService, ], - imports: [], + imports: [GolemioModule, PlatformModule, RouteModule], }) export class DepartureModule {} diff --git a/apps/backend/src/modules/departure/departure.resolver.ts b/apps/backend/src/modules/departure/departure.resolver.ts index 0fbf934b..1d0fdb59 100644 --- a/apps/backend/src/modules/departure/departure.resolver.ts +++ b/apps/backend/src/modules/departure/departure.resolver.ts @@ -1,11 +1,10 @@ import { Args, Parent, Query, ResolveField, Resolver } from "@nestjs/graphql"; import { GraphQLError } from "src/common/graphql-error"; -import { GolemioService } from "src/modules/golemio/golemio.service"; +import { DepartureBoardService } from "src/modules/departure/departure-board.service"; import { PlatformService } from "src/modules/platform/platform.service"; -import { PrismaService } from "src/modules/prisma/prisma.service"; import { RouteService } from "src/modules/route/route.service"; -import { ParentType } from "src/types/parent"; +import type { ParentType } from "src/types/parent"; const ROUTE_ID_BY_NAME = { A: "L991", @@ -16,9 +15,8 @@ const ROUTE_ID_BY_NAME = { @Resolver("Departure") export class DepartureResolver { constructor( - private golemioService: GolemioService, - private prismaService: PrismaService, - private platformService: PlatformService, + private readonly departureBoardService: DepartureBoardService, + private readonly platformService: PlatformService, private readonly routeService: RouteService, ) {} @@ -26,48 +24,32 @@ export class DepartureResolver { async getMultiple( @Args("platformIds") platformIds: string[] = [], @Args("stopIds") stopIds: string[] = [], - @Args("limit") limit: number = 100, + @Args("limit") limit = 100, ) { if (platformIds.length === 0 && stopIds.length === 0) { - return GraphQLError({ + throw GraphQLError({ message: "At least one `platformId` or `stopId` is required", code: "BAD_USER_INPUT", }); } - const stopPlatforms = await this.prismaService.stop.findMany({ - select: { platforms: { select: { id: true } } }, - where: { id: { in: stopIds } }, + const resolvedPlatformIds = + await this.departureBoardService.resolvePlatformIds({ + platformIds, + stopIds, + }); + const normalizedLimit = Math.max(1, Math.min(limit, 100)); + const json = await this.departureBoardService.fetchDepartureBoard({ + platformIds: resolvedPlatformIds, + params: { + includeMetroTrains: true, + limit: normalizedLimit, + minutesBefore: 1, + mode: "departures", + order: "real", + skip: "canceled", + }, }); - const stopPlatformIds = stopPlatforms.flatMap(({ platforms }) => - platforms.map(({ id }) => id), - ); - - const searchParams = new URLSearchParams( - platformIds - .concat(stopPlatformIds) - .map((id) => ["ids", id]) - .concat([ - ["skip", "canceled"], - ["mode", "departures"], - ["order", "real"], - ["includeMetroTrains", "true"], - ["limit", limit.toString()], - ["minutesBefore", "1"], - ]), - ); - - const res = await this.golemioService.getGolemioData( - `/v2/pid/departureboards?${searchParams.toString()}`, - ); - - if (!res.ok) { - throw new Error( - `Failed to fetch departure data: ${res.status} ${res.statusText}`, - ); - } - - const json = await res.json(); return json.departures.map((departure) => ({ ...departure, @@ -91,9 +73,7 @@ export class DepartureResolver { @ResolveField("platform") getPlatformField(@Parent() departure: ParentType) { - return this.platformService.getOne({ - where: { id: departure.platform.id }, - }); + return this.platformService.getOneById(departure.platform.id); } @ResolveField("route") diff --git a/apps/backend/src/modules/golemio/golemio.module.ts b/apps/backend/src/modules/golemio/golemio.module.ts new file mode 100644 index 00000000..14ffd5f8 --- /dev/null +++ b/apps/backend/src/modules/golemio/golemio.module.ts @@ -0,0 +1,9 @@ +import { Module } from "@nestjs/common"; + +import { GolemioService } from "src/modules/golemio/golemio.service"; + +@Module({ + providers: [GolemioService], + exports: [GolemioService], +}) +export class GolemioModule {} diff --git a/apps/backend/src/modules/golemio/golemio.service.ts b/apps/backend/src/modules/golemio/golemio.service.ts index 43765faa..30051ba0 100644 --- a/apps/backend/src/modules/golemio/golemio.service.ts +++ b/apps/backend/src/modules/golemio/golemio.service.ts @@ -1,39 +1,46 @@ import { CACHE_MANAGER } from "@nestjs/cache-manager"; import { Inject, Injectable } from "@nestjs/common"; -import { Cache } from "cache-manager"; +import type { Cache } from "cache-manager"; -import { CACHE_KEYS } from "src/constants/cache"; +import { CACHE_KEYS, ttl } from "src/constants/cache"; -const TTL = 4 * 1_000; const GOLEMIO_API = "https://api.golemio.cz"; +const getTtlForPath = (path: string): number => { + if (path.startsWith("/v2/pid/departureboards")) { + return ttl({ seconds: 10 }); + } + + return ttl({ seconds: 30 }); +}; + @Injectable() export class GolemioService { constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {} - async getGolemioData(path: string): Promise { + async getGolemioData(path: string): Promise { const url = `${GOLEMIO_API}${path}`; - const CACHE_KEY = CACHE_KEYS.golemio.getGolemioData(path); - - const cached = await this.cacheManager.get(CACHE_KEY); - if (cached) { - return new Response(JSON.stringify(cached)); - } - - const res = await fetch(url, { - method: "GET", - headers: { - "Content-Type": "application/json", - "X-Access-Token": process.env.GOLEMIO_API_KEY ?? "", + return this.cacheManager.wrap( + CACHE_KEYS.golemio.getGolemioData(path), + async () => { + const res = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-Access-Token": process.env.GOLEMIO_API_KEY ?? "", + }, + }); + + if (!res.ok) { + throw new Error( + `Failed to fetch Golemio data: ${res.status} ${res.statusText}`, + ); + } + + return await res.json(); }, - }); - - if (res.ok) { - const parsed = await res.clone().json(); - await this.cacheManager.set(CACHE_KEY, parsed, TTL); - } - - return res; + getTtlForPath(path), + ); } } diff --git a/apps/backend/src/modules/hello/__test__/hello.resolver.spec.ts b/apps/backend/src/modules/hello/__test__/hello.resolver.spec.ts index 5a050ca8..1ac28398 100644 --- a/apps/backend/src/modules/hello/__test__/hello.resolver.spec.ts +++ b/apps/backend/src/modules/hello/__test__/hello.resolver.spec.ts @@ -1,4 +1,4 @@ -import { Test, TestingModule } from "@nestjs/testing"; +import { Test, type TestingModule } from "@nestjs/testing"; import { HelloResolver } from "src/modules/hello/hello.resolver"; diff --git a/apps/backend/src/modules/hello/hello.resolver.ts b/apps/backend/src/modules/hello/hello.resolver.ts index 0cc899ab..01253d9b 100644 --- a/apps/backend/src/modules/hello/hello.resolver.ts +++ b/apps/backend/src/modules/hello/hello.resolver.ts @@ -1,6 +1,6 @@ import { Query, Resolver } from "@nestjs/graphql"; -import { IQuery } from "src/types/graphql.generated"; +import type { IQuery } from "src/types/graphql.generated"; @Resolver() export class HelloResolver { diff --git a/apps/backend/src/modules/import/gtfs.service.ts b/apps/backend/src/modules/import/gtfs.service.ts deleted file mode 100644 index 50715573..00000000 --- a/apps/backend/src/modules/import/gtfs.service.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { Open as unzipperOpen } from "unzipper"; - -import { PrismaService } from "src/modules/prisma/prisma.service"; -import { parseCsvString } from "src/utils/csv.utils"; - -@Injectable() -export class GtfsService { - constructor(private readonly prisma: PrismaService) {} - - async syncGtfsData() { - const response = await fetch("https://data.pid.cz/PID_GTFS.zip"); - const arrayBuffer = await response.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - const directory = await unzipperOpen.buffer(buffer); - - const routes = directory.files.find( - (file) => file.path === "routes.txt", - ); - if (!routes) { - console.log("routes.txt not found"); - return; - } - const routeStops = directory.files.find( - (file) => file.path === "route_stops.txt", - ); - if (!routeStops) { - console.log("route_stops.txt not found"); - return; - } - - const routesBuffer = await routes.buffer(); - const routeStopsBuffer = await routeStops.buffer(); - - type RouteRecord = { - route_id: string; - route_short_name: string; - route_long_name: string; - route_type: string; - route_color?: string | undefined; - is_night: string; - route_url?: string | undefined; - }; - // FIXME: validate with zod - const routesData = await parseCsvString( - routesBuffer.toString(), - ); - - type RouteStopRecord = { - route_id: string; - direction_id: string; - stop_id: string; - stop_sequence: string; - }; - // FIXME: validate with zod - const routeStopsData = await parseCsvString( - routeStopsBuffer.toString(), - ); - - await this.prisma.$transaction(async (transaction) => { - await transaction.gtfsRouteStop.deleteMany(); - await transaction.gtfsRoute.deleteMany(); - - await transaction.gtfsRoute.createMany({ - data: routesData.map((route) => ({ - id: route.route_id, - shortName: route.route_short_name, - longName: route.route_long_name ?? null, - type: route.route_type, - isNight: Boolean(route.is_night), - color: route.route_color ?? null, - url: route.route_url ?? null, - })), - }); - - const platforms = await this.prisma.platform.findMany({ - select: { id: true }, - }); - const platformIds = platforms.map((platform) => platform.id); - - await transaction.gtfsRouteStop.createMany({ - data: routeStopsData - .map((routeStop) => { - let stopId = routeStop.stop_id; - - if (stopId.includes("_")) { - stopId = stopId.split("_")[0]; - } - - return { - routeId: routeStop.route_id, - directionId: routeStop.direction_id, - stopId: stopId, - stopSequence: Number(routeStop.stop_sequence), - }; - }) - .filter((routeStop) => - platformIds.includes(routeStop.stopId), - ), - }); - }); - - console.log("Finished GTFS sync"); - } -} diff --git a/apps/backend/src/modules/import/import.controller.ts b/apps/backend/src/modules/import/import.controller.ts deleted file mode 100644 index 8c46bb73..00000000 --- a/apps/backend/src/modules/import/import.controller.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Controller, OnModuleInit } from "@nestjs/common"; -import { Cron, CronExpression } from "@nestjs/schedule"; - -import { Environment } from "src/enums/environment.enum"; -import { GtfsService } from "src/modules/import/gtfs.service"; -import { ImportService } from "src/modules/import/import.service"; - -@Controller("import") -export class ImportController implements OnModuleInit { - constructor( - private readonly importService: ImportService, - private readonly gtfsService: GtfsService, - ) {} - - async syncEverything(): Promise { - console.log("syncing everything"); - await this.importService.syncStops(); - await this.gtfsService.syncGtfsData(); - console.log("syncing everything done"); - } - - async onModuleInit(): Promise { - const importPromise = this.syncEverything(); - - if (process.env.NODE_ENV === Environment.TEST) { - await importPromise; - } - } - - @Cron(CronExpression.EVERY_7_HOURS) - async cronSyncStops(): Promise { - this.syncEverything(); - } -} diff --git a/apps/backend/src/modules/import/import.module.ts b/apps/backend/src/modules/import/import.module.ts deleted file mode 100644 index 6c36ab2d..00000000 --- a/apps/backend/src/modules/import/import.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Module } from "@nestjs/common"; - -import { GtfsService } from "src/modules/import/gtfs.service"; -import { ImportController } from "src/modules/import/import.controller"; -import { ImportService } from "src/modules/import/import.service"; - -@Module({ - controllers: [ImportController], - providers: [ImportService, GtfsService], - imports: [], -}) -export class ImportModule {} diff --git a/apps/backend/src/modules/import/import.service.ts b/apps/backend/src/modules/import/import.service.ts deleted file mode 100644 index 3b463ff6..00000000 --- a/apps/backend/src/modules/import/import.service.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { VehicleType } from "@prisma/client"; -import { unique } from "radash"; - -import { - pidStopsSchema, - type PidStopsSchema, -} from "src/modules/import/schema/pid-stops.schema"; -import { PrismaService } from "src/modules/prisma/prisma.service"; - -@Injectable() -export class ImportService { - constructor(private readonly prisma: PrismaService) {} - - async getStops(): Promise { - const url = new URL("https://data.pid.cz/stops/json/stops.json"); - const res = await fetch(url, { method: "GET" }); - if (!res.ok) { - throw new Error( - `Failed to fetch stops data: ${res.status} ${res.statusText}`, - ); - } - const raw = await res.json(); - const parsed = pidStopsSchema.safeParse(raw); - - if (parsed.error) { - throw new Error( - `Couldn't parse data from '${url.toString()}': ${parsed.error}`, - ); - } - - return parsed.data; - } - - async updateDB({ - stops, - platforms, - }: { - stops: { - id: string; - name: string; - avgLatitude: number; - avgLongitude: number; - }[]; - platforms: { - id: string; - name: string; - isMetro: boolean; - latitude: number; - longitude: number; - stopId: string | null; - code: string | null; - routes: { - id: string; - name: string; - vehicleType: string; - }[]; - }[]; - }): Promise { - const uniqueStops = unique(stops, (stop) => stop.id); - const uniqueRoutes = unique( - platforms.flatMap((platform) => platform.routes), - (route) => route.id, - ); - - await this.prisma.$transaction( - async (transaction) => { - await transaction.platformsOnRoutes.deleteMany(); - await transaction.gtfsRouteStop.deleteMany(); - await transaction.platform.deleteMany(); - await transaction.route.deleteMany(); - await transaction.stop.deleteMany(); - - // Create stops - await transaction.stop.createMany({ - data: uniqueStops.map((stop) => ({ - id: stop.id, - name: stop.name, - avgLatitude: stop.avgLatitude, - avgLongitude: stop.avgLongitude, - })), - }); - - await transaction.platform.createMany({ - data: unique( - platforms.map((platform) => { - const stopIdExists = - platform.stopId !== null && - stops.some( - (stop) => stop.id === platform.stopId, - ); - - return { - id: platform.id, - name: platform.name, - code: platform.code, - isMetro: platform.isMetro, - latitude: platform.latitude, - longitude: platform.longitude, - stopId: stopIdExists ? platform.stopId : null, - }; - }), - (platform) => platform.id, - ), - }); - - // Create routes - await transaction.route.createMany({ - data: uniqueRoutes.map((route) => ({ - id: route.id, - name: route.name, - vehicleType: - VehicleType?.[route.vehicleType.toUpperCase()] ?? - null, - })), - }); - - // Create relations - await transaction.platformsOnRoutes.createMany({ - data: unique( - platforms.flatMap((platform) => - platform.routes.map((route) => ({ - platformId: platform.id, - routeId: route.id, - })), - ), - (relation) => - `${relation.platformId}${relation.routeId}`, - ), - }); - }, - { - maxWait: 10 * 1_000, - timeout: 20 * 60 * 1_000, - }, - ); - } - - async syncStops(): Promise { - try { - const stopsData = await this.getStops(); - - const platforms = stopsData.stopGroups - .flatMap((stop) => - stop.stops.map((platform) => { - return { - latitude: platform.lat, - longitude: platform.lon, - id: platform.gtfsIds?.[0], - name: platform.altIdosName, - isMetro: platform.isMetro === true, - code: platform.platform ?? null, - routes: platform.lines.map((line) => ({ - id: line.id, - name: line.name, - vehicleType: line.type, - })), - }; - }), - ) - .filter( - (stop) => - !!stop.latitude && - !!stop.longitude && - !!stop.id && - !!stop.name, - ); - - await this.updateDB({ - stops: stopsData.stopGroups.map((stop) => ({ - id: `U${stop.node}`, - name: stop.name, - avgLatitude: stop.avgLat, - avgLongitude: stop.avgLon, - })), - platforms: platforms.map((platform) => ({ - id: platform.id, - name: platform.name, - code: platform.code, - isMetro: platform.isMetro, - latitude: platform.latitude, - longitude: platform.longitude, - stopId: platform.id.split("Z")[0], - routes: platform.routes, - })), - }); - } catch (error) { - console.error(error); - } finally { - console.log("Finished stop sync"); - } - } -} diff --git a/apps/backend/src/modules/infotexts/__test__/infotext.resolver.spec.ts b/apps/backend/src/modules/infotexts/__test__/infotext.resolver.spec.ts index 8ecaa1d1..d4c5e4f4 100644 --- a/apps/backend/src/modules/infotexts/__test__/infotext.resolver.spec.ts +++ b/apps/backend/src/modules/infotexts/__test__/infotext.resolver.spec.ts @@ -1,11 +1,11 @@ import { CACHE_MANAGER } from "@nestjs/cache-manager"; -import { Test, TestingModule } from "@nestjs/testing"; +import { Test, type TestingModule } from "@nestjs/testing"; +import { DatabaseService } from "src/modules/database/database.service"; import { GolemioService } from "src/modules/golemio/golemio.service"; import { InfotextsResolver } from "src/modules/infotexts/infotexts.resolver"; import { InfotextsService } from "src/modules/infotexts/infotexts.service"; import { PlatformService } from "src/modules/platform/platform.service"; -import { PrismaService } from "src/modules/prisma/prisma.service"; describe("InfotextResolver", () => { let resolver: InfotextsResolver; @@ -18,10 +18,10 @@ describe("InfotextResolver", () => { GolemioService, PlatformService, { - provide: PrismaService, + provide: DatabaseService, useValue: { - platform: { - findMany: jest.fn(), + db: { + selectFrom: jest.fn(), }, }, }, diff --git a/apps/backend/src/modules/infotexts/infotexts.module.ts b/apps/backend/src/modules/infotexts/infotexts.module.ts index 09db33f2..93d7d75b 100644 --- a/apps/backend/src/modules/infotexts/infotexts.module.ts +++ b/apps/backend/src/modules/infotexts/infotexts.module.ts @@ -1,19 +1,14 @@ import { Module } from "@nestjs/common"; -import { GolemioService } from "src/modules/golemio/golemio.service"; +import { GolemioModule } from "src/modules/golemio/golemio.module"; import { InfotextsController } from "src/modules/infotexts/infotexts.controller"; import { InfotextsResolver } from "src/modules/infotexts/infotexts.resolver"; import { InfotextsService } from "src/modules/infotexts/infotexts.service"; -import { PlatformService } from "src/modules/platform/platform.service"; +import { PlatformModule } from "src/modules/platform/platform.module"; @Module({ controllers: [InfotextsController], - providers: [ - InfotextsResolver, - InfotextsService, - GolemioService, - PlatformService, - ], - imports: [], + providers: [InfotextsResolver, InfotextsService], + imports: [GolemioModule, PlatformModule], }) export class InfotextsModule {} diff --git a/apps/backend/src/modules/infotexts/infotexts.resolver.ts b/apps/backend/src/modules/infotexts/infotexts.resolver.ts index 8e0d4fb3..bfdb66f7 100644 --- a/apps/backend/src/modules/infotexts/infotexts.resolver.ts +++ b/apps/backend/src/modules/infotexts/infotexts.resolver.ts @@ -2,7 +2,7 @@ import { Parent, Query, ResolveField, Resolver } from "@nestjs/graphql"; import { InfotextsService } from "src/modules/infotexts/infotexts.service"; import { PlatformService } from "src/modules/platform/platform.service"; -import { ParentType } from "src/types/parent"; +import type { ParentType } from "src/types/parent"; @Resolver("Infotext") export class InfotextsResolver { @@ -23,9 +23,6 @@ export class InfotextsResolver { ) { const ids = infotext.relatedPlatforms.map(({ id }) => id); - return this.platformService.getAll({ - metroOnly: false, - where: { id: { in: ids } }, - }); + return this.platformService.getGraphQLByIds(ids); } } diff --git a/apps/backend/src/modules/infotexts/infotexts.service.ts b/apps/backend/src/modules/infotexts/infotexts.service.ts index 56cdf02e..ddb84fdc 100644 --- a/apps/backend/src/modules/infotexts/infotexts.service.ts +++ b/apps/backend/src/modules/infotexts/infotexts.service.ts @@ -1,4 +1,4 @@ -import { Cache, CACHE_MANAGER } from "@nestjs/cache-manager"; +import { CACHE_MANAGER, type Cache } from "@nestjs/cache-manager"; import { Inject, Injectable } from "@nestjs/common"; import { CACHE_KEYS, ttl } from "src/constants/cache"; @@ -14,17 +14,10 @@ export class InfotextsService { ) {} private async _getAll() { - const res = - await this.golemioService.getGolemioData(`/v3/pid/infotexts`); + const data = + await this.golemioService.getGolemioData("/v3/pid/infotexts"); - if (!res.ok) { - throw new Error( - `Failed to fetch infotexts: ${res.status} ${res.statusText}`, - ); - } - - const json = await res.json(); - const parsed = golemioResponseSchema.safeParse(json); + const parsed = golemioResponseSchema.safeParse(data); if (!parsed.success) { throw new Error(parsed.error.message); diff --git a/apps/backend/src/modules/log/graphql-query-logging.plugin.ts b/apps/backend/src/modules/log/graphql-query-logging.plugin.ts new file mode 100644 index 00000000..2e6273e3 --- /dev/null +++ b/apps/backend/src/modules/log/graphql-query-logging.plugin.ts @@ -0,0 +1,40 @@ +import { + type ApolloServerPlugin, + type BaseContext, + type GraphQLRequestContext, + type GraphQLRequestListener, +} from "@apollo/server"; +import { Plugin } from "@nestjs/apollo"; +import { Injectable } from "@nestjs/common"; +import type { GraphQLError } from "graphql"; + +import { LogService } from "src/modules/log/log.service"; + +@Plugin() +@Injectable() +export class GraphQLQueryLoggingPlugin + implements ApolloServerPlugin +{ + constructor(private readonly logService: LogService) {} + + async requestDidStart( + _requestContext: GraphQLRequestContext, + ): Promise> { + const startedAt = Date.now(); + const logService = this.logService; + let encounteredErrors: readonly GraphQLError[] = []; + + return { + async didEncounterErrors(requestContext) { + encounteredErrors = requestContext.errors; + }, + async willSendResponse(requestContext) { + logService.logGraphQLRequest({ + durationMs: Date.now() - startedAt, + errors: encounteredErrors, + requestContext, + }); + }, + }; + } +} diff --git a/apps/backend/src/modules/log/log.module.ts b/apps/backend/src/modules/log/log.module.ts new file mode 100644 index 00000000..80ad5037 --- /dev/null +++ b/apps/backend/src/modules/log/log.module.ts @@ -0,0 +1,9 @@ +import { Module } from "@nestjs/common"; + +import { GraphQLQueryLoggingPlugin } from "src/modules/log/graphql-query-logging.plugin"; +import { LogService } from "src/modules/log/log.service"; + +@Module({ + providers: [LogService, GraphQLQueryLoggingPlugin], +}) +export class LogModule {} diff --git a/apps/backend/src/modules/log/log.service.spec.ts b/apps/backend/src/modules/log/log.service.spec.ts new file mode 100644 index 00000000..e31e5f4d --- /dev/null +++ b/apps/backend/src/modules/log/log.service.spec.ts @@ -0,0 +1,72 @@ +import type { GraphQLRequestContextWillSendResponse } from "@apollo/server"; + +import { LogService } from "src/modules/log/log.service"; + +describe("LogService", () => { + it("stores GraphQL request logs in the database log table", async () => { + const execute = jest.fn().mockResolvedValue(undefined); + const values = jest.fn().mockReturnValue({ execute }); + const insertInto = jest.fn().mockReturnValue({ values }); + const logService = new LogService({ + db: { + insertInto, + }, + } as never); + + logService.logGraphQLRequest({ + durationMs: 42, + requestContext: { + operationName: "Stops", + operation: { + operation: "query", + }, + queryHash: "abc123", + request: { + query: "query Stops { stops { id } }", + operationName: "Stops", + variables: { + limit: 5, + }, + http: { + method: "POST", + search: "", + headers: new Headers({ + "user-agent": "jest", + }) as never, + }, + }, + requestIsBatched: false, + response: { + body: { + kind: "single", + singleResult: { + data: { + stops: [], + }, + }, + }, + http: {}, + }, + source: "query Stops { stops { id } }", + } as unknown as GraphQLRequestContextWillSendResponse, + }); + + await logService.flush(); + + expect(insertInto).toHaveBeenCalledWith("Log"); + expect(values).toHaveBeenCalledWith( + expect.objectContaining({ + service: "backend", + level: "info", + message: "GraphQL request completed", + context: expect.objectContaining({ + operationName: "Stops", + operationType: "query", + durationMs: 42, + queryHash: "abc123", + }), + }), + ); + expect(execute).toHaveBeenCalledTimes(1); + }); +}); diff --git a/apps/backend/src/modules/log/log.service.ts b/apps/backend/src/modules/log/log.service.ts new file mode 100644 index 00000000..77b2e6c2 --- /dev/null +++ b/apps/backend/src/modules/log/log.service.ts @@ -0,0 +1,121 @@ +import { randomUUID } from "node:crypto"; +import { + type BaseContext, + type GraphQLRequestContextWillSendResponse, +} from "@apollo/server"; +import { type NewLog } from "@metro-now/database"; +import { Injectable, type OnModuleDestroy } from "@nestjs/common"; +import type { GraphQLError } from "graphql"; + +import { DatabaseService } from "src/modules/database/database.service"; + +const SERVICE_NAME = "backend"; + +const serializeGraphQLError = (error: GraphQLError) => { + return { + message: error.message, + code: + typeof error.extensions?.code === "string" + ? error.extensions.code + : null, + path: error.path ?? null, + }; +}; + +const sanitizeContext = ( + context: Record, +): Record => { + return Object.fromEntries( + Object.entries(context).filter(([, value]) => value !== undefined), + ); +}; + +const getResultKeys = ( + body: GraphQLRequestContextWillSendResponse["response"]["body"], +): string[] | null => { + const singleResult = + body.kind === "single" ? body.singleResult : body.initialResult; + const data = "data" in singleResult ? singleResult.data : undefined; + + if (!data || typeof data !== "object" || Array.isArray(data)) { + return null; + } + + return Object.keys(data as Record); +}; + +@Injectable() +export class LogService implements OnModuleDestroy { + private pendingWrite: Promise = Promise.resolve(); + + constructor(private readonly database: DatabaseService) {} + + logGraphQLRequest({ + durationMs, + errors = [], + requestContext, + }: { + durationMs: number; + errors?: readonly GraphQLError[]; + requestContext: GraphQLRequestContextWillSendResponse; + }): void { + const level = errors.length > 0 ? "error" : "info"; + const statusCode = requestContext.response.http.status ?? 200; + const logEntry: NewLog = { + id: randomUUID(), + service: SERVICE_NAME, + level, + message: "GraphQL request completed", + context: sanitizeContext({ + operationName: + requestContext.operationName ?? + requestContext.request.operationName ?? + null, + operationType: requestContext.operation?.operation ?? null, + query: + requestContext.source ?? + requestContext.request.query ?? + null, + variables: requestContext.request.variables ?? null, + extensions: requestContext.request.extensions ?? null, + queryHash: requestContext.queryHash ?? null, + durationMs, + statusCode, + requestIsBatched: requestContext.requestIsBatched, + httpMethod: requestContext.request.http?.method ?? null, + search: requestContext.request.http?.search ?? null, + userAgent: + requestContext.request.http?.headers.get("user-agent") ?? + null, + responseKind: requestContext.response.body.kind, + resultKeys: getResultKeys(requestContext.response.body), + hasErrors: errors.length > 0, + errorCount: errors.length, + errors: errors.map(serializeGraphQLError), + }), + createdAt: new Date(), + }; + + this.pendingWrite = this.pendingWrite + .then(async () => { + await this.database.db + .insertInto("Log") + .values(logEntry) + .execute(); + }) + .catch((error) => { + console.error("Failed to write backend GraphQL log", { + error: + error instanceof Error ? error.message : String(error), + }); + }); + } + + async flush(): Promise { + await this.pendingWrite; + } + + async onModuleDestroy() { + await this.flush(); + } +} diff --git a/apps/backend/src/modules/platform/platform.controller.ts b/apps/backend/src/modules/platform/platform.controller.ts index c18ea669..3bcf7a12 100644 --- a/apps/backend/src/modules/platform/platform.controller.ts +++ b/apps/backend/src/modules/platform/platform.controller.ts @@ -15,12 +15,12 @@ import { ApiDescription, ApiQueries } from "src/decorators/swagger.decorator"; import { EndpointVersion } from "src/enums/endpoint-version"; import { PlatformService } from "src/modules/platform/platform.service"; import { + type PlatformWithDistanceSchema, platformWithDistanceSchema, - PlatformWithDistanceSchema, } from "src/modules/platform/schema/platform-with-distance.schema"; import { - platformSchema, type PlatformSchema, + platformSchema, } from "src/modules/platform/schema/platform.schema"; import { boundingBoxSchema } from "src/schema/bounding-box.schema"; import { metroOnlySchema } from "src/schema/metro-only.schema"; diff --git a/apps/backend/src/modules/platform/platform.module.ts b/apps/backend/src/modules/platform/platform.module.ts index ea1d84bf..56b483ff 100644 --- a/apps/backend/src/modules/platform/platform.module.ts +++ b/apps/backend/src/modules/platform/platform.module.ts @@ -5,19 +5,12 @@ import { StopByPlatformLoader } from "src/modules/dataloader/stop-by-platform.lo import { PlatformController } from "src/modules/platform/platform.controller"; import { PlatformResolver } from "src/modules/platform/platform.resolver"; import { PlatformService } from "src/modules/platform/platform.service"; -import { RouteService } from "src/modules/route/route.service"; -import { StopService } from "src/modules/stop/stop.service"; +import { StopDataModule } from "src/modules/stop/stop-data.module"; @Module({ controllers: [PlatformController], - providers: [ - PlatformResolver, - PlatformService, - StopService, - RouteService, - StopByPlatformLoader, - ], + providers: [PlatformResolver, PlatformService, StopByPlatformLoader], exports: [PlatformService, StopByPlatformLoader], - imports: [RoutesByPlatformIdLoaderModule], + imports: [RoutesByPlatformIdLoaderModule, StopDataModule], }) export class PlatformModule {} diff --git a/apps/backend/src/modules/platform/platform.resolver.ts b/apps/backend/src/modules/platform/platform.resolver.ts index 59b717f7..b46db65a 100644 --- a/apps/backend/src/modules/platform/platform.resolver.ts +++ b/apps/backend/src/modules/platform/platform.resolver.ts @@ -3,7 +3,7 @@ import { Args, Parent, Query, ResolveField, Resolver } from "@nestjs/graphql"; import { RoutesByPlatformIdLoader } from "src/modules/dataloader/routes-by-platform.loader"; import { StopByPlatformLoader } from "src/modules/dataloader/stop-by-platform.loader"; import { PlatformService } from "src/modules/platform/platform.service"; -import { ParentType } from "src/types/parent"; +import type { ParentType } from "src/types/parent"; @Resolver("Platform") export class PlatformResolver { @@ -15,15 +15,18 @@ export class PlatformResolver { @Query("platform") getOne(@Args("id") id: string) { - return this.platformService.getOne({ where: { id } }); + return this.platformService.getOneById(id); } @Query("platforms") - getMultiple(@Args("ids") ids: string[]) { - return this.platformService.getAllGraphQL({ - metroOnly: false, - where: { id: { in: ids } }, - }); + getMultiple(@Args("ids") ids: string[] | undefined) { + if (!ids || ids.length === 0) { + return this.platformService.getAllGraphQL({ + metroOnly: false, + }); + } + + return this.platformService.getGraphQLByIds(ids); } @ResolveField("stop") diff --git a/apps/backend/src/modules/platform/platform.service.ts b/apps/backend/src/modules/platform/platform.service.ts index 240af1f2..34bccea7 100644 --- a/apps/backend/src/modules/platform/platform.service.ts +++ b/apps/backend/src/modules/platform/platform.service.ts @@ -1,35 +1,161 @@ -import { Injectable } from "@nestjs/common"; -import { Prisma } from "@prisma/client"; +import type { Platform, Route } from "@metro-now/database"; +import { sql } from "@metro-now/database"; +import { CACHE_MANAGER, type Cache } from "@nestjs/cache-manager"; +import { Inject, Injectable } from "@nestjs/common"; -import { PlatformWithDistanceSchema } from "src/modules/platform/schema/platform-with-distance.schema"; -import { PlatformSchema } from "src/modules/platform/schema/platform.schema"; -import { PrismaService } from "src/modules/prisma/prisma.service"; +import { CACHE_KEYS, ttl } from "src/constants/cache"; +import { DatabaseService } from "src/modules/database/database.service"; +import type { PlatformWithDistanceSchema } from "src/modules/platform/schema/platform-with-distance.schema"; +import type { PlatformSchema } from "src/modules/platform/schema/platform.schema"; import type { BoundingBox } from "src/schema/bounding-box.schema"; +import { loadCachedBatch } from "src/utils/cache-batch"; import { minMax } from "src/utils/math"; -export const platformSelect = { - id: true, - latitude: true, - longitude: true, - name: true, - isMetro: true, - stopId: true, - code: true, - routes: { - select: { - route: { - select: { - id: true, - name: true, - }, - }, - }, - }, -} satisfies Prisma.PlatformSelect; +type PlatformRouteRecord = Pick; + +type PlatformRecordBase = Pick< + Platform, + "code" | "id" | "isMetro" | "latitude" | "longitude" | "name" | "stopId" +>; + +type PlatformRecord = PlatformRecordBase & { + routes: PlatformRouteRecord[]; +}; + +type PlatformGraphQLRecord = PlatformRecordBase; + +const GRAPHQL_CACHE_TTL_MS = ttl({ minutes: 5 }); @Injectable() export class PlatformService { - constructor(private prisma: PrismaService) {} + constructor( + private readonly database: DatabaseService, + @Inject(CACHE_MANAGER) private readonly cacheManager: Cache, + ) {} + + private async loadPlatformRows({ + ids, + metroOnly, + boundingBox, + }: { + ids?: readonly string[]; + metroOnly?: boolean; + boundingBox?: BoundingBox; + }): Promise { + if (ids && ids.length === 0) { + return []; + } + + let query = this.database.db + .selectFrom("Platform") + .select([ + "id", + "latitude", + "longitude", + "name", + "isMetro", + "stopId", + "code", + ]); + + if (ids) { + query = query.where("id", "in", [...ids]); + } + + if (metroOnly) { + query = query.where("isMetro", "=", true); + } + + if (boundingBox) { + const latitude = minMax(boundingBox.latitude); + const longitude = minMax(boundingBox.longitude); + + query = query + .where("latitude", ">=", latitude.min) + .where("latitude", "<=", latitude.max) + .where("longitude", ">=", longitude.min) + .where("longitude", "<=", longitude.max); + } + + return query.orderBy("id", "asc").execute(); + } + + private async loadRoutesByPlatformIds( + platformIds: readonly string[], + ): Promise> { + const routesByPlatformId = new Map( + platformIds.map((platformId) => [platformId, []]), + ); + + if (platformIds.length === 0) { + return routesByPlatformId; + } + + const rows = await this.database.db + .selectFrom("PlatformsOnRoutes") + .innerJoin("Route", "Route.id", "PlatformsOnRoutes.routeId") + .select([ + "PlatformsOnRoutes.platformId as platformId", + "Route.id as routeId", + "Route.name as routeName", + ]) + .where("PlatformsOnRoutes.platformId", "in", [...platformIds]) + .orderBy("PlatformsOnRoutes.platformId", "asc") + .orderBy("Route.id", "asc") + .execute(); + + for (const row of rows) { + routesByPlatformId.get(row.platformId)?.push({ + id: row.routeId, + name: row.routeName, + }); + } + + return routesByPlatformId; + } + + private async loadPlatformsWithRoutes({ + ids, + metroOnly, + boundingBox, + }: { + ids?: readonly string[]; + metroOnly?: boolean; + boundingBox?: BoundingBox; + }): Promise { + const platforms = await this.loadPlatformRows({ + ...(ids ? { ids } : {}), + ...(metroOnly ? { metroOnly } : {}), + ...(boundingBox ? { boundingBox } : {}), + }); + const routesByPlatformId = await this.loadRoutesByPlatformIds( + platforms.map((platform) => platform.id), + ); + + return platforms.map((platform) => ({ + ...platform, + routes: routesByPlatformId.get(platform.id) ?? [], + })); + } + + private async loadGraphQLPlatformsByIds( + ids: readonly string[], + ): Promise> { + const platforms = await this.loadPlatformRows({ + ids, + }); + const platformsById = new Map( + platforms.map((platform) => [platform.id, platform]), + ); + + for (const id of ids) { + if (!platformsById.has(id)) { + platformsById.set(id, null); + } + } + + return platformsById; + } async getPlatformsByDistance({ latitude, @@ -42,49 +168,53 @@ export class PlatformService { count: number; metroOnly: boolean; }): Promise { - const res = await this.prisma.$transaction(async (transaction) => { - const platformsWithDistance = await this.prisma.$queryRaw< - { id: string; distance: number }[] - >` - SELECT - "Platform"."id", - earth_distance( - ll_to_earth("Platform"."latitude", "Platform"."longitude"), - ll_to_earth(${latitude}, ${longitude}) - ) AS "distance" - FROM "Platform" - ${metroOnly ? Prisma.sql`WHERE "Platform"."isMetro" = true` : Prisma.empty} - ORDER BY "distance" - ${count ? Prisma.sql`LIMIT ${count}` : Prisma.empty} - `; - - const distanceByPlatformID = Object.fromEntries( - platformsWithDistance.map(({ id, distance }) => [id, distance]), - ); - - const platforms = await transaction.platform.findMany({ - select: platformSelect, - where: { - id: { - in: platformsWithDistance.map( - (platform) => platform.id, - ), - }, - }, - }); + const whereClause = metroOnly + ? sql`WHERE "Platform"."isMetro" = true` + : sql``; + const limitClause = count > 0 ? sql`LIMIT ${count}` : sql``; + const result = await sql<{ distance: number; id: string }>` + SELECT + "Platform"."id", + earth_distance( + ll_to_earth("Platform"."latitude", "Platform"."longitude"), + ll_to_earth(${latitude}, ${longitude}) + ) AS "distance" + FROM "Platform" + ${whereClause} + ORDER BY "distance" + ${limitClause} + `.execute(this.database.db); - return platforms - .map((platform) => ({ - ...platform, - distance: distanceByPlatformID[platform.id], - })) - .sort((a, b) => a.distance - b.distance); + const orderedPlatformIds = result.rows.map(({ id }) => id); + const distanceByPlatformId = new Map( + result.rows.map(({ id, distance }) => [id, distance]), + ); + const platforms = await this.loadPlatformsWithRoutes({ + ids: orderedPlatformIds, }); + const platformsById = new Map( + platforms.map((platform) => [platform.id, platform]), + ); - return res.map((platform) => ({ - ...platform, - routes: platform.routes.map(({ route }) => route), - })); + return orderedPlatformIds + .map((id) => { + const platform = platformsById.get(id); + + if (!platform) { + return null; + } + + return { + ...platform, + distance: distanceByPlatformId.get(id) ?? 0, + }; + }) + .filter( + ( + platform, + ): platform is PlatformWithDistanceSchema & PlatformRecord => + platform !== null, + ); } async getPlatformsInBoundingBox({ @@ -94,97 +224,70 @@ export class PlatformService { boundingBox: BoundingBox; metroOnly: boolean; }): Promise { - const latitude = minMax(boundingBox.latitude); - const longitude = minMax(boundingBox.longitude); - - const platforms = await this.prisma.platform.findMany({ - select: platformSelect, - where: { - latitude: { - gte: latitude.min, - lte: latitude.max, - }, - longitude: { - gte: longitude.min, - lte: longitude.max, - }, - ...(metroOnly ? { isMetro: true } : {}), - }, + return this.loadPlatformsWithRoutes({ + boundingBox, + metroOnly, }); - - return platforms.map((platform) => ({ - ...platform, - routes: platform.routes.map(({ route }) => route), - })); } - /** - * @deprecated Use getAllGraphQL instead - */ async getAll({ metroOnly, - where, }: { metroOnly: boolean; - where?: Prisma.PlatformWhereInput; - }) { - const platforms = await this.prisma.platform.findMany({ - select: platformSelect, - where: { - ...where, - ...(metroOnly - ? { - isMetro: true, - } - : {}), - }, + }): Promise { + return this.loadPlatformsWithRoutes({ + metroOnly, }); - - return platforms.map((platform) => ({ - ...platform, - routes: platform.routes.map(({ route }) => route), - })); } async getAllGraphQL({ metroOnly, - where, }: { metroOnly: boolean; - where?: Prisma.PlatformWhereInput; - }) { - const platforms = await this.prisma.platform.findMany({ - select: { - id: true, - latitude: true, - longitude: true, - code: true, - name: true, - isMetro: true, - stopId: true, - }, - where: { - ...where, - ...(metroOnly ? { isMetro: true } : {}), - }, - }); - - return platforms; + }): Promise { + return this.cacheManager.wrap( + CACHE_KEYS.platform.getAllGraphQL({ + metroOnly, + }), + async () => + await this.loadPlatformRows({ + metroOnly, + }), + GRAPHQL_CACHE_TTL_MS, + ); } - async getOne({ where }: { where: Prisma.PlatformWhereInput }) { - const platform = await this.prisma.platform.findFirst({ - select: platformSelect, - where, + async getGraphQLByIds( + ids: readonly string[], + ): Promise { + const platformsById = await loadCachedBatch({ + cacheManager: this.cacheManager, + getCacheKey: CACHE_KEYS.platform.getGraphQLById, + keys: ids, + loadMissing: async (missingIds) => + this.loadGraphQLPlatformsByIds(missingIds), + ttlMs: GRAPHQL_CACHE_TTL_MS, }); - if (!platform) { - return null; - } + return Array.from(new Set(ids)) + .map((id) => platformsById.get(id)) + .filter( + (platform): platform is PlatformGraphQLRecord => + platform !== null, + ); + } - return { - ...platform, - routes: platform.routes.map(({ route }) => route), - }; + async getOneById(id: string): Promise { + return this.cacheManager.wrap( + CACHE_KEYS.platform.getOne({ id }), + async () => { + const [platform] = await this.loadPlatformsWithRoutes({ + ids: [id], + }); + + return platform ?? null; + }, + GRAPHQL_CACHE_TTL_MS, + ); } } diff --git a/apps/backend/src/modules/platform/schema.graphql b/apps/backend/src/modules/platform/schema.graphql index 96fa3378..4f98c0e9 100644 --- a/apps/backend/src/modules/platform/schema.graphql +++ b/apps/backend/src/modules/platform/schema.graphql @@ -5,7 +5,7 @@ type Platform { longitude: Float! isMetro: Boolean! code: String - stop: Stop! + stop: Stop routes: [Route!]! } diff --git a/apps/backend/src/modules/prisma/prisma.module.ts b/apps/backend/src/modules/prisma/prisma.module.ts deleted file mode 100644 index 5c3de805..00000000 --- a/apps/backend/src/modules/prisma/prisma.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Global, Module } from "@nestjs/common"; - -import { PrismaService } from "src/modules/prisma/prisma.service"; - -@Global() -@Module({ - imports: [], - controllers: [], - providers: [PrismaService], - exports: [PrismaService], -}) -export class PrismaModule {} diff --git a/apps/backend/src/modules/prisma/prisma.service.ts b/apps/backend/src/modules/prisma/prisma.service.ts deleted file mode 100644 index ce53bdb7..00000000 --- a/apps/backend/src/modules/prisma/prisma.service.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Injectable, OnModuleDestroy, OnModuleInit } from "@nestjs/common"; -import { PrismaClient } from "@prisma/client"; - -@Injectable() -export class PrismaService - extends PrismaClient - implements OnModuleInit, OnModuleDestroy -{ - constructor() { - super({ - datasourceUrl: `postgresql://${process.env.POSTGRES_USER}:${process.env.POSTGRES_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.POSTGRES_DB}?schema=${process.env.DB_SCHEMA}`, - log: ["info", "warn", "error"], - }); - } - async onModuleInit() { - await this.$connect(); - } - - async onModuleDestroy() { - await this.$disconnect(); - } - - async getExtensions(): Promise< - { - oid: string | number; - extname: string; - }[] - > { - return await this.$queryRaw`SELECT * FROM pg_extension`; - } - - async getExtensionNames(): Promise { - const extensions = await this.getExtensions(); - - return extensions.map((ext) => ext.extname); - } -} diff --git a/apps/backend/src/modules/route/route.resolver.ts b/apps/backend/src/modules/route/route.resolver.ts index df81b0e8..bff1166d 100644 --- a/apps/backend/src/modules/route/route.resolver.ts +++ b/apps/backend/src/modules/route/route.resolver.ts @@ -1,16 +1,20 @@ import { Args, Parent, Query, ResolveField, Resolver } from "@nestjs/graphql"; import { RouteService } from "src/modules/route/route.service"; -import { VehicleType } from "src/types/graphql.generated"; -import { ParentType } from "src/types/parent"; +import type { VehicleType } from "src/types/graphql.generated"; +import type { ParentType } from "src/types/parent"; @Resolver("Route") export class RouteResolver { constructor(private readonly routeService: RouteService) {} + private toDatabaseRouteId(id: string): string { + return id.startsWith("L") ? id : `L${id}`; + } + @Query("route") getOne(@Args("id") id: string) { - return this.routeService.getOneGraphQL(`L${id}`); + return this.routeService.getOneGraphQL(this.toDatabaseRouteId(id)); } @Query("routes") diff --git a/apps/backend/src/modules/route/route.service.ts b/apps/backend/src/modules/route/route.service.ts index 5ce2785b..da6cc6f8 100644 --- a/apps/backend/src/modules/route/route.service.ts +++ b/apps/backend/src/modules/route/route.service.ts @@ -1,89 +1,233 @@ -import { Injectable } from "@nestjs/common"; -import { Prisma } from "@prisma/client"; +import type { GtfsRoute, Platform } from "@metro-now/database"; +import { CACHE_MANAGER, type Cache } from "@nestjs/cache-manager"; +import { Inject, Injectable } from "@nestjs/common"; import { group } from "radash"; -import { PrismaService } from "src/modules/prisma/prisma.service"; +import { CACHE_KEYS, ttl, uniqueSortedStrings } from "src/constants/cache"; +import { DatabaseService } from "src/modules/database/database.service"; import { BUS_PREFIXES, METRO_LINES, TRAIN_PREFIXES, } from "src/modules/route/route.const"; import { VehicleType } from "src/types/graphql.generated"; +import { loadCachedBatch } from "src/utils/cache-batch"; -const gtfsRouteSelect = { - id: true, - shortName: true, - longName: true, - isNight: true, - color: true, - url: true, - type: true, - GtfsRouteStop: { - select: { - directionId: true, - stopId: true, - stopSequence: true, - platform: { - select: { - id: true, - latitude: true, - longitude: true, - name: true, - isMetro: true, - code: true, - }, - }, - }, - orderBy: { - stopSequence: "asc", - }, - }, -} as const satisfies Prisma.GtfsRouteSelect; +type RouteRow = Pick< + GtfsRoute, + "color" | "id" | "isNight" | "longName" | "shortName" | "type" | "url" +>; + +type RoutePlatformRecord = Pick< + Platform, + "code" | "id" | "isMetro" | "latitude" | "longitude" | "name" +>; + +type RouteStopRow = { + directionId: string; + platform: RoutePlatformRecord | null; + routeId: string; + stopSequence: number; +}; + +const processRoute = (route: RouteRow, routeStops: RouteStopRow[]) => { + return { + ...route, + id: route.id.slice(1), + name: route.shortName, + directions: Object.entries( + group(routeStops, ({ directionId }) => directionId), + ).map(([key, value]) => ({ + id: key, + platforms: (value ?? []) + .sort((left, right) => left.stopSequence - right.stopSequence) + .flatMap((routeStop) => + routeStop.platform ? [routeStop.platform] : [], + ), + })), + }; +}; + +type GraphQLRouteRecord = ReturnType; + +const GRAPHQL_CACHE_TTL_MS = ttl({ minutes: 5 }); @Injectable() export class RouteService { - constructor(private prisma: PrismaService) {} - - private processRoute( - route: Prisma.GtfsRouteGetPayload<{ select: typeof gtfsRouteSelect }>, - ) { - return { - ...route, - id: route.id.slice(1), - name: route.shortName, - directions: Object.entries( - group(route.GtfsRouteStop, ({ directionId }) => directionId), - ).map(([key, value]) => ({ - id: key, - platforms: value?.map((v) => v.platform), - })), - }; + constructor( + private readonly database: DatabaseService, + @Inject(CACHE_MANAGER) private readonly cacheManager: Cache, + ) {} + + private async loadRouteRows( + routeIds?: readonly string[], + ): Promise { + if (routeIds && routeIds.length === 0) { + return []; + } + + let query = this.database.db + .selectFrom("GtfsRoute") + .select([ + "id", + "shortName", + "longName", + "isNight", + "color", + "url", + "type", + ]); + + if (routeIds) { + query = query.where("id", "in", [...routeIds]); + } + + return query.orderBy("id", "asc").execute(); } - async getManyGraphQL({ - where = {}, - }: { - where?: Prisma.GtfsRouteWhereInput; - } = {}) { - const routes = await this.prisma.gtfsRoute.findMany({ - select: gtfsRouteSelect, - where, - }); + private async loadRouteStops( + routeIds: readonly string[], + ): Promise> { + const routeStopsByRouteId = new Map( + routeIds.map((routeId) => [routeId, []]), + ); + + if (routeIds.length === 0) { + return routeStopsByRouteId; + } + + const rows = await this.database.db + .selectFrom("GtfsRouteStop") + .leftJoin("Platform", "Platform.id", "GtfsRouteStop.stopId") + .select([ + "GtfsRouteStop.routeId as routeId", + "GtfsRouteStop.directionId as directionId", + "GtfsRouteStop.stopSequence as stopSequence", + "Platform.id as platformId", + "Platform.latitude as platformLatitude", + "Platform.longitude as platformLongitude", + "Platform.name as platformName", + "Platform.isMetro as platformIsMetro", + "Platform.code as platformCode", + ]) + .where("GtfsRouteStop.routeId", "in", [...routeIds]) + .orderBy("GtfsRouteStop.routeId", "asc") + .orderBy("GtfsRouteStop.directionId", "asc") + .orderBy("GtfsRouteStop.stopSequence", "asc") + .execute(); + + for (const row of rows) { + routeStopsByRouteId.get(row.routeId)?.push({ + routeId: row.routeId, + directionId: row.directionId, + stopSequence: row.stopSequence, + platform: row.platformId + ? { + id: row.platformId, + latitude: row.platformLatitude ?? 0, + longitude: row.platformLongitude ?? 0, + name: row.platformName ?? "", + isMetro: row.platformIsMetro ?? false, + code: row.platformCode, + } + : null, + }); + } - return routes.map(this.processRoute); + return routeStopsByRouteId; } - async getOneGraphQL(id: string) { - const route = await this.prisma.gtfsRoute.findFirst({ - select: gtfsRouteSelect, - where: { id }, - }); + private async loadGraphQLRoutesByIds( + routeIds: readonly string[], + ): Promise { + const [routes, routeStopsByRouteId] = await Promise.all([ + this.loadRouteRows(routeIds), + this.loadRouteStops(routeIds), + ]); + + return routes.map((route) => + processRoute(route, routeStopsByRouteId.get(route.id) ?? []), + ); + } - if (!route) { - return null; + private async loadGraphQLRoutesByPlatformIds( + platformIds: readonly string[], + ): Promise> { + const routesByPlatformId = new Map( + platformIds.map((platformId) => [platformId, []]), + ); + + if (platformIds.length === 0) { + return routesByPlatformId; + } + + const routeLinks = await this.database.db + .selectFrom("GtfsRouteStop") + .select(["routeId", "stopId as platformId"]) + .distinct() + .where("stopId", "in", [...platformIds]) + .orderBy("routeId", "asc") + .execute(); + const routeIds = uniqueSortedStrings( + routeLinks.map(({ routeId }) => routeId), + ); + const routes = await this.loadGraphQLRoutesByIds(routeIds); + const routeById = new Map( + routes.map((route) => [`L${route.id}` as const, route]), + ); + + for (const { platformId, routeId } of routeLinks) { + const route = routeById.get(routeId); + + if (route) { + routesByPlatformId.get(platformId)?.push(route); + } } - return this.processRoute(route); + return routesByPlatformId; + } + + async getManyGraphQL(): Promise { + return this.cacheManager.wrap( + CACHE_KEYS.route.getManyGraphQL({}), + async () => { + const routeRows = await this.loadRouteRows(); + + return this.loadGraphQLRoutesByIds( + routeRows.map((route) => route.id), + ); + }, + GRAPHQL_CACHE_TTL_MS, + ); + } + + async getOneGraphQL(id: string): Promise { + return this.cacheManager.wrap( + CACHE_KEYS.route.getOneGraphQL(id), + async () => { + const [route] = await this.loadGraphQLRoutesByIds([id]); + + return route ?? null; + }, + GRAPHQL_CACHE_TTL_MS, + ); + } + + async getManyGraphQLByPlatformIds( + platformIds: readonly string[], + ): Promise { + const routesByPlatformId = await loadCachedBatch({ + cacheManager: this.cacheManager, + getCacheKey: CACHE_KEYS.route.getManyGraphQLByPlatformId, + keys: platformIds, + loadMissing: async (missingPlatformIds) => + this.loadGraphQLRoutesByPlatformIds(missingPlatformIds), + ttlMs: GRAPHQL_CACHE_TTL_MS, + }); + + return platformIds.map( + (platformId) => routesByPlatformId.get(platformId) ?? [], + ); } isSubstitute(routeName: string): boolean { @@ -96,9 +240,9 @@ export class RouteService { isNight(routeName: string): boolean { const routeNameParsed = this.getNameWithoutSubstitute(routeName); - const routeNumber = parseInt(routeNameParsed); + const routeNumber = Number.parseInt(routeNameParsed); - if (isNaN(routeNumber)) { + if (Number.isNaN(routeNumber)) { return false; } @@ -110,9 +254,9 @@ export class RouteService { getVehicleType(routeName: string): VehicleType { const routeNameParsed = this.getNameWithoutSubstitute(routeName); - const routeNumber = parseInt(routeNameParsed); + const routeNumber = Number.parseInt(routeNameParsed); - if (!isNaN(routeNumber)) { + if (!Number.isNaN(routeNumber)) { if (routeNumber === 58 || routeNumber === 59) { return VehicleType.TROLLEYBUS; } diff --git a/apps/backend/src/modules/status/status.controller.ts b/apps/backend/src/modules/status/status.controller.ts index fea6525f..47e82f7d 100644 --- a/apps/backend/src/modules/status/status.controller.ts +++ b/apps/backend/src/modules/status/status.controller.ts @@ -4,7 +4,7 @@ import { ApiResponse, ApiTags } from "@nestjs/swagger"; import { ApiDescription } from "src/decorators/swagger.decorator"; import { StatusService } from "src/modules/status/status.service"; import { - StatusObject, + type StatusObject, SystemStatus, SystemStatusService, } from "src/modules/status/status.types"; diff --git a/apps/backend/src/modules/status/status.service.ts b/apps/backend/src/modules/status/status.service.ts index 38e6ad5b..8765d1ca 100644 --- a/apps/backend/src/modules/status/status.service.ts +++ b/apps/backend/src/modules/status/status.service.ts @@ -1,15 +1,20 @@ -import { Injectable } from "@nestjs/common"; +import { CACHE_MANAGER, type Cache } from "@nestjs/cache-manager"; +import { Inject, Injectable } from "@nestjs/common"; -import { PrismaService } from "src/modules/prisma/prisma.service"; +import { CACHE_KEYS, ttl } from "src/constants/cache"; +import { DatabaseService } from "src/modules/database/database.service"; import { - StatusObject, + type StatusObject, SystemStatus, SystemStatusService, } from "src/modules/status/status.types"; @Injectable() export class StatusService { - constructor(private prisma: PrismaService) {} + constructor( + private readonly database: DatabaseService, + @Inject(CACHE_MANAGER) private readonly cacheManager: Cache, + ) {} getBackendStatus(): StatusObject { return { @@ -19,27 +24,61 @@ export class StatusService { } async getGeoFunctionsStatus(): Promise { - const extensionNames = await this.prisma.getExtensionNames(); - const isOk = - extensionNames.includes("cube") && - extensionNames.includes("earthdistance"); + return this.cacheManager.wrap( + CACHE_KEYS.status.getGeoFunctionsStatus, + async () => { + const extensionNames = await this.database.getExtensionNames(); + const isOk = + extensionNames.includes("cube") && + extensionNames.includes("earthdistance"); - return { - service: SystemStatusService.GEO_FUNCTIONS, - status: isOk ? SystemStatus.OK : SystemStatus.ERROR, - }; + return { + service: SystemStatusService.GEO_FUNCTIONS, + status: isOk ? SystemStatus.OK : SystemStatus.ERROR, + }; + }, + ttl({ seconds: 30 }), + ); } async getDbDataStatus(): Promise { - const stopCount = await this.prisma.stop.count(); - const routeCount = await this.prisma.route.count(); - const platformCount = await this.prisma.platform.count(); + return this.cacheManager.wrap( + CACHE_KEYS.status.getDbDataStatus, + async () => { + const [stopCountResult, routeCountResult, platformCountResult] = + await Promise.all([ + this.database.db + .selectFrom("Stop") + .select(({ fn }) => + fn.countAll().as("count"), + ) + .executeTakeFirstOrThrow(), + this.database.db + .selectFrom("Route") + .select(({ fn }) => + fn.countAll().as("count"), + ) + .executeTakeFirstOrThrow(), + this.database.db + .selectFrom("Platform") + .select(({ fn }) => + fn.countAll().as("count"), + ) + .executeTakeFirstOrThrow(), + ]); + const stopCount = Number(stopCountResult.count); + const routeCount = Number(routeCountResult.count); + const platformCount = Number(platformCountResult.count); - const isOk = stopCount > 0 && routeCount > 0 && platformCount > 0; + const isOk = + stopCount > 0 && routeCount > 0 && platformCount > 0; - return { - service: SystemStatusService.DB_DATA, - status: isOk ? SystemStatus.OK : SystemStatus.ERROR, - }; + return { + service: SystemStatusService.DB_DATA, + status: isOk ? SystemStatus.OK : SystemStatus.ERROR, + }; + }, + ttl({ seconds: 30 }), + ); } } diff --git a/apps/backend/src/modules/stop/stop-data.module.ts b/apps/backend/src/modules/stop/stop-data.module.ts new file mode 100644 index 00000000..df49992e --- /dev/null +++ b/apps/backend/src/modules/stop/stop-data.module.ts @@ -0,0 +1,9 @@ +import { Module } from "@nestjs/common"; + +import { StopService } from "src/modules/stop/stop.service"; + +@Module({ + providers: [StopService], + exports: [StopService], +}) +export class StopDataModule {} diff --git a/apps/backend/src/modules/stop/stop.controller.ts b/apps/backend/src/modules/stop/stop.controller.ts index 13b32967..b75aa36e 100644 --- a/apps/backend/src/modules/stop/stop.controller.ts +++ b/apps/backend/src/modules/stop/stop.controller.ts @@ -26,9 +26,9 @@ export class StopController { @ApiQuery(metroOnlyQuery) async getAllStopsV1( @Query("metroOnly") - metroOnlyQuery: unknown, + metroOnlyValue: unknown, ) { - const metroOnly: boolean = metroOnlyQuery === "true"; + const metroOnly: boolean = metroOnlyValue === "true"; return this.stopService.getAll({ metroOnly }); } @@ -47,7 +47,7 @@ export class StopController { throw new HttpException("Missing stop ID", HttpStatus.BAD_REQUEST); } - const res = await this.stopService.getOne({ where: { id } }); + const res = await this.stopService.getOneById(id); if (!res) { throw new HttpException("Stop ID not found", HttpStatus.NOT_FOUND); diff --git a/apps/backend/src/modules/stop/stop.module.ts b/apps/backend/src/modules/stop/stop.module.ts index 2779bc74..b972aaf3 100644 --- a/apps/backend/src/modules/stop/stop.module.ts +++ b/apps/backend/src/modules/stop/stop.module.ts @@ -1,14 +1,13 @@ import { Module } from "@nestjs/common"; import { DataloaderModule } from "src/modules/dataloader/dataloader.module"; -import { PlatformService } from "src/modules/platform/platform.service"; +import { StopDataModule } from "src/modules/stop/stop-data.module"; import { StopController } from "src/modules/stop/stop.controller"; import { StopResolver } from "src/modules/stop/stop.resolver"; -import { StopService } from "src/modules/stop/stop.service"; @Module({ controllers: [StopController], - providers: [PlatformService, StopResolver, StopService], - imports: [DataloaderModule], + providers: [StopResolver], + imports: [DataloaderModule, StopDataModule], }) export class StopModule {} diff --git a/apps/backend/src/modules/stop/stop.resolver.ts b/apps/backend/src/modules/stop/stop.resolver.ts index 29370510..3e1beaf3 100644 --- a/apps/backend/src/modules/stop/stop.resolver.ts +++ b/apps/backend/src/modules/stop/stop.resolver.ts @@ -2,7 +2,7 @@ import { Args, Parent, Query, ResolveField, Resolver } from "@nestjs/graphql"; import { PlatformsByStopLoader } from "src/modules/dataloader/platforms-by-stop.loader"; import { StopService } from "src/modules/stop/stop.service"; -import { ParentType } from "src/types/parent"; +import type { ParentType } from "src/types/parent"; @Resolver("Stop") export class StopResolver { @@ -13,7 +13,7 @@ export class StopResolver { @Query("stop") getOne(@Args("id") id: string) { - return this.stopService.getOne({ where: { id } }); + return this.stopService.getOneById(id); } @Query("stops") @@ -22,10 +22,17 @@ export class StopResolver { @Args("limit") limit: number | undefined, @Args("offset") offset: number | undefined, ) { + if (ids && ids.length > 0) { + const stops = await this.stopService.getGraphQLByIds(ids); + const start = offset ?? 0; + const end = typeof limit === "number" ? start + limit : undefined; + + return stops.slice(start, end); + } + const res = await this.stopService.getAllGraphQL({ - where: ids ? { id: { in: ids } } : {}, - limit: limit ?? undefined, - offset: offset ?? undefined, + ...(typeof limit === "number" ? { limit } : {}), + ...(typeof offset === "number" ? { offset } : {}), }); return res; diff --git a/apps/backend/src/modules/stop/stop.service.ts b/apps/backend/src/modules/stop/stop.service.ts index 0a7bf7b0..0f499f2a 100644 --- a/apps/backend/src/modules/stop/stop.service.ts +++ b/apps/backend/src/modules/stop/stop.service.ts @@ -1,110 +1,347 @@ -import { Injectable } from "@nestjs/common"; -import { Prisma } from "@prisma/client"; - -import { platformSelect } from "src/modules/platform/platform.service"; -import { PrismaService } from "src/modules/prisma/prisma.service"; - -export const getStopsSelect = ({ metroOnly }: { metroOnly: boolean }) => { - return { - id: true, - name: true, - avgLatitude: true, - avgLongitude: true, - platforms: { - select: platformSelect, - where: metroOnly ? { isMetro: true } : {}, - }, - } satisfies Prisma.StopSelect; +import type { Platform, Route, Stop } from "@metro-now/database"; +import { CACHE_MANAGER, type Cache } from "@nestjs/cache-manager"; +import { Inject, Injectable } from "@nestjs/common"; + +import { CACHE_KEYS, ttl } from "src/constants/cache"; +import { DatabaseService } from "src/modules/database/database.service"; +import { loadCachedBatch } from "src/utils/cache-batch"; + +type StopRecordBase = Pick< + Stop, + "avgLatitude" | "avgLongitude" | "id" | "name" +>; + +type PlatformRouteRecord = Pick; + +type StopPlatformRecord = Pick< + Platform, + "code" | "id" | "isMetro" | "latitude" | "longitude" | "name" | "stopId" +> & { + routes: PlatformRouteRecord[]; +}; + +type StopRecord = StopRecordBase & { + platforms: StopPlatformRecord[]; }; +type StopGraphQLPlatformRecord = Pick; + +type StopGraphQLRecord = StopRecordBase & { + platforms: StopGraphQLPlatformRecord[]; +}; + +const GRAPHQL_CACHE_TTL_MS = ttl({ minutes: 5 }); + @Injectable() export class StopService { - constructor(private prisma: PrismaService) {} + constructor( + private readonly database: DatabaseService, + @Inject(CACHE_MANAGER) private readonly cacheManager: Cache, + ) {} + + private async loadStopRows({ + ids, + limit, + offset, + }: { + ids?: readonly string[]; + limit?: number; + offset?: number; + }): Promise { + if (ids && ids.length === 0) { + return []; + } + + let query = this.database.db + .selectFrom("Stop") + .select(["id", "name", "avgLatitude", "avgLongitude"]); + + if (ids) { + query = query.where("id", "in", [...ids]); + } + + if (typeof offset === "number") { + query = query.offset(offset); + } + + if (typeof limit === "number") { + query = query.limit(limit); + } + + return query.orderBy("id", "asc").execute(); + } + + private async loadPlatformRoutesByPlatformIds( + platformIds: readonly string[], + ): Promise> { + const routesByPlatformId = new Map( + platformIds.map((platformId) => [platformId, []]), + ); + + if (platformIds.length === 0) { + return routesByPlatformId; + } + + const rows = await this.database.db + .selectFrom("PlatformsOnRoutes") + .innerJoin("Route", "Route.id", "PlatformsOnRoutes.routeId") + .select([ + "PlatformsOnRoutes.platformId as platformId", + "Route.id as routeId", + "Route.name as routeName", + ]) + .where("PlatformsOnRoutes.platformId", "in", [...platformIds]) + .orderBy("PlatformsOnRoutes.platformId", "asc") + .orderBy("Route.id", "asc") + .execute(); + + for (const row of rows) { + routesByPlatformId.get(row.platformId)?.push({ + id: row.routeId, + name: row.routeName, + }); + } + + return routesByPlatformId; + } + + private async loadPlatformsByStopIds({ + stopIds, + metroOnly, + }: { + stopIds: readonly string[]; + metroOnly?: boolean; + }): Promise> { + const platformsByStopId = new Map( + stopIds.map((stopId) => [stopId, []]), + ); + + if (stopIds.length === 0) { + return platformsByStopId; + } + + let query = this.database.db + .selectFrom("Platform") + .select([ + "id", + "latitude", + "longitude", + "name", + "isMetro", + "stopId", + "code", + ]) + .where("stopId", "in", [...stopIds]) + .orderBy("stopId", "asc") + .orderBy("id", "asc"); + + if (metroOnly) { + query = query.where("isMetro", "=", true); + } + + const platforms = await query.execute(); + const routesByPlatformId = await this.loadPlatformRoutesByPlatformIds( + platforms.map((platform) => platform.id), + ); + + for (const platform of platforms) { + if (!platform.stopId) { + continue; + } + + platformsByStopId.get(platform.stopId)?.push({ + ...platform, + routes: routesByPlatformId.get(platform.id) ?? [], + }); + } + + return platformsByStopId; + } + + private async loadStopGraphQLPlatformIdsByStopIds( + stopIds: readonly string[], + ): Promise> { + const platformsByStopId = new Map( + stopIds.map((stopId) => [stopId, []]), + ); + + if (stopIds.length === 0) { + return platformsByStopId; + } + + const rows = await this.database.db + .selectFrom("Platform") + .select(["id", "stopId"]) + .where("stopId", "in", [...stopIds]) + .orderBy("stopId", "asc") + .orderBy("id", "asc") + .execute(); + + for (const row of rows) { + if (!row.stopId) { + continue; + } + + platformsByStopId.get(row.stopId)?.push({ + id: row.id, + }); + } + + return platformsByStopId; + } + + private async loadGraphQLStopsByIds( + ids: readonly string[], + ): Promise> { + const stops = await this.loadStopRows({ + ids, + }); + const platformIdsByStopId = + await this.loadStopGraphQLPlatformIdsByStopIds( + stops.map((stop) => stop.id), + ); + const stopsById = new Map( + stops.map((stop) => [ + stop.id, + { + ...stop, + platforms: platformIdsByStopId.get(stop.id) ?? [], + }, + ]), + ); + + for (const id of ids) { + if (!stopsById.has(id)) { + stopsById.set(id, null); + } + } + + return stopsById; + } - /** @deprecated Use getAllGraphQL instead */ async getAll({ metroOnly, - where, limit, offset, }: { metroOnly: boolean; - where?: Prisma.StopWhereInput; - limit?: number | undefined; - offset?: number | undefined; - }) { - const stops = await this.prisma.stop.findMany({ - select: getStopsSelect({ metroOnly }), - where: { - ...where, - platforms: { - ...where?.platforms, - some: { - ...where?.platforms?.some, - isMetro: metroOnly, - }, - }, - }, - ...(limit && { take: limit }), - ...(offset && { skip: offset }), + limit?: number; + offset?: number; + }): Promise { + let stopIdQuery = this.database.db + .selectFrom("Platform") + .select("stopId") + .distinct() + .where("stopId", "is not", null) + .$if(metroOnly, (qb) => qb.where("isMetro", "=", true)) + .orderBy("stopId", "asc"); + + if (typeof offset === "number") { + stopIdQuery = stopIdQuery.offset(offset); + } + + if (typeof limit === "number") { + stopIdQuery = stopIdQuery.limit(limit); + } + + const stopIds = (await stopIdQuery.execute()).flatMap(({ stopId }) => + stopId ? [stopId] : [], + ); + const stops = await this.loadStopRows({ + ids: stopIds, + }); + const platformsByStopId = await this.loadPlatformsByStopIds({ + stopIds: stops.map((stop) => stop.id), + ...(metroOnly ? { metroOnly: true } : {}), }); + const stopById = new Map(stops.map((stop) => [stop.id, stop])); - return stops.map((stop) => ({ - ...stop, - platforms: stop.platforms.map((platform) => ({ - ...platform, - routes: platform.routes.map((route) => route.route), - })), - })); + return stopIds + .map((stopId) => { + const stop = stopById.get(stopId); + + if (!stop) { + return null; + } + + return { + ...stop, + platforms: platformsByStopId.get(stop.id) ?? [], + }; + }) + .filter((stop): stop is StopRecord => stop !== null); } async getAllGraphQL({ - where, limit, offset, }: { - where?: Prisma.StopWhereInput; - limit?: number | undefined; - offset?: number | undefined; - }) { - const stops = await this.prisma.stop.findMany({ - select: { - id: true, - name: true, - avgLatitude: true, - avgLongitude: true, - platforms: { - select: { - id: true, - }, - }, + limit?: number; + offset?: number; + }): Promise { + return this.cacheManager.wrap( + CACHE_KEYS.stop.getAllGraphQL({ + limit, + offset, + }), + async () => { + const stops = await this.loadStopRows({ + ...(typeof limit === "number" ? { limit } : {}), + ...(typeof offset === "number" ? { offset } : {}), + }); + const platformIdsByStopId = + await this.loadStopGraphQLPlatformIdsByStopIds( + stops.map((stop) => stop.id), + ); + + return stops.map((stop) => ({ + ...stop, + platforms: platformIdsByStopId.get(stop.id) ?? [], + })); }, - where: where ?? {}, - ...(limit && { take: limit }), - ...(offset && { skip: offset }), + GRAPHQL_CACHE_TTL_MS, + ); + } + + async getGraphQLByIds( + ids: readonly string[], + ): Promise { + const stopsById = await loadCachedBatch({ + cacheManager: this.cacheManager, + getCacheKey: CACHE_KEYS.stop.getGraphQLById, + keys: ids, + loadMissing: async (missingIds) => + this.loadGraphQLStopsByIds(missingIds), + ttlMs: GRAPHQL_CACHE_TTL_MS, }); - return stops; + return Array.from(new Set(ids)) + .map((id) => stopsById.get(id)) + .filter((stop): stop is StopGraphQLRecord => stop !== null); } - async getOne({ where }: { where?: Prisma.StopWhereInput }) { - const stop = await this.prisma.stop.findFirst({ - select: getStopsSelect({ metroOnly: false }), - where: { - ...where, - }, - }); + async getOneById(id: string): Promise { + return this.cacheManager.wrap( + CACHE_KEYS.stop.getOne({ id }), + async () => { + const [stop] = await this.loadStopRows({ + ids: [id], + }); - if (!stop) { - return null; - } + if (!stop) { + return null; + } - return { - ...stop, - platforms: (stop.platforms || []).map((platform) => ({ - ...platform, - routes: platform.routes.map((route) => route.route), - })), - }; + const platformsByStopId = await this.loadPlatformsByStopIds({ + stopIds: [id], + }); + + return { + ...stop, + platforms: platformsByStopId.get(id) ?? [], + }; + }, + GRAPHQL_CACHE_TTL_MS, + ); } } diff --git a/apps/backend/src/schema/env.schema.ts b/apps/backend/src/schema/env.schema.ts index 5571512b..15395929 100644 --- a/apps/backend/src/schema/env.schema.ts +++ b/apps/backend/src/schema/env.schema.ts @@ -1,17 +1,8 @@ +import { commonServerEnvSchema } from "@metro-now/shared"; import { z } from "zod"; -export const envSchema = z.object({ - GOLEMIO_API_KEY: z.string(), - POSTGRES_USER: z.string(), - POSTGRES_PASSWORD: z.string(), - POSTGRES_DB: z.string(), - DB_HOST: z.string(), - DB_PORT: z.coerce.number().int().positive(), - DB_SCHEMA: z.string(), - REDIS_PORT: z.coerce.number().int().positive().optional(), - REDIS_HOST: z.string().optional(), - PORT: z.coerce.number().int().positive().optional(), - LOGS: z.string().optional(), +export const envSchema = commonServerEnvSchema.extend({ + GOLEMIO_API_KEY: z.string().min(1), }); export type EnvSchema = z.infer; diff --git a/apps/backend/src/scripts/generate-types.ts b/apps/backend/src/scripts/generate-types.ts index c0fd372d..ce3c683b 100644 --- a/apps/backend/src/scripts/generate-types.ts +++ b/apps/backend/src/scripts/generate-types.ts @@ -1,4 +1,4 @@ -import { join } from "path"; +import { join } from "node:path"; import { GraphQLDefinitionsFactory } from "@nestjs/graphql"; diff --git a/apps/backend/src/types/parent.ts b/apps/backend/src/types/parent.ts index c7e761de..ff4e123e 100644 --- a/apps/backend/src/types/parent.ts +++ b/apps/backend/src/types/parent.ts @@ -1,4 +1,4 @@ -type AnyFunction = (...args: any) => any; +type AnyFunction = (...args: never[]) => unknown; export type ParentType = NonNullable< Awaited> extends Array ? U : Awaited> diff --git a/apps/backend/src/utils/array.spec.ts b/apps/backend/src/utils/array.spec.ts index 1834fa41..20110017 100644 --- a/apps/backend/src/utils/array.spec.ts +++ b/apps/backend/src/utils/array.spec.ts @@ -18,7 +18,7 @@ describe("toArray", () => { 1, 69, 420, - -Infinity, + Number.NEGATIVE_INFINITY, { key: "value", }, @@ -30,7 +30,7 @@ describe("toArray", () => { 1, 69, 420, - -Infinity, + Number.NEGATIVE_INFINITY, { key: "value", }, diff --git a/apps/backend/src/utils/cache-batch.spec.ts b/apps/backend/src/utils/cache-batch.spec.ts new file mode 100644 index 00000000..4184a7b0 --- /dev/null +++ b/apps/backend/src/utils/cache-batch.spec.ts @@ -0,0 +1,62 @@ +import type { Cache } from "@nestjs/cache-manager"; + +import { loadCachedBatch } from "src/utils/cache-batch"; + +describe("loadCachedBatch", () => { + const createCacheManager = (store: Map) => ({ + mget: jest.fn(async (keys: string[]) => + keys.map((key) => (store.has(key) ? store.get(key) : undefined)), + ), + mset: jest.fn( + async (entries: Array<{ key: string; value: unknown }>) => { + for (const entry of entries) { + store.set(entry.key, entry.value); + } + + return entries; + }, + ), + }); + + it("reuses cached values and only loads misses", async () => { + const store = new Map([ + ["platform.getGraphQLById.A", { id: "A", name: "Alpha" }], + ]); + const cacheManager = createCacheManager(store); + const loadMissing = jest.fn(async (keys: readonly string[]) => { + return new Map(keys.map((key) => [key, { id: key, name: key }])); + }); + + const valuesByKey = await loadCachedBatch({ + cacheManager: cacheManager as unknown as Cache, + getCacheKey: (key) => `platform.getGraphQLById.${key}`, + keys: ["A", "B", "A"], + loadMissing, + ttlMs: 60_000, + }); + + expect(loadMissing).toHaveBeenCalledWith(["B"]); + expect(valuesByKey.get("A")).toEqual({ id: "A", name: "Alpha" }); + expect(valuesByKey.get("B")).toEqual({ id: "B", name: "B" }); + expect(store.get("platform.getGraphQLById.B")).toEqual({ + id: "B", + name: "B", + }); + }); + + it("caches explicit null results to avoid repeated misses", async () => { + const store = new Map(); + const cacheManager = createCacheManager(store); + + const valuesByKey = await loadCachedBatch({ + cacheManager: cacheManager as unknown as Cache, + getCacheKey: (key) => `stop.getGraphQLById.${key}`, + keys: ["missing-stop"], + loadMissing: async () => new Map([["missing-stop", null]]), + ttlMs: 60_000, + }); + + expect(valuesByKey.get("missing-stop")).toBeNull(); + expect(store.get("stop.getGraphQLById.missing-stop")).toBeNull(); + }); +}); diff --git a/apps/backend/src/utils/cache-batch.ts b/apps/backend/src/utils/cache-batch.ts new file mode 100644 index 00000000..bb552c56 --- /dev/null +++ b/apps/backend/src/utils/cache-batch.ts @@ -0,0 +1,68 @@ +import type { Cache } from "@nestjs/cache-manager"; + +import { uniqueStrings } from "src/constants/cache"; + +export const loadCachedBatch = async ({ + cacheManager, + getCacheKey, + keys, + loadMissing, + ttlMs, +}: { + cacheManager: Cache; + getCacheKey: (key: Key) => string; + keys: readonly Key[]; + loadMissing: (keys: readonly Key[]) => Promise>; + ttlMs: number; +}): Promise> => { + const uniqueKeys = uniqueStrings(keys); + + if (uniqueKeys.length === 0) { + return new Map(); + } + + const cacheEntries = uniqueKeys.map((key) => ({ + cacheKey: getCacheKey(key), + key, + })); + const cachedValues = await cacheManager.mget( + cacheEntries.map(({ cacheKey }) => cacheKey), + ); + + const valuesByKey = new Map(); + const missingKeys: Key[] = []; + + for (const [index, { key }] of cacheEntries.entries()) { + const value = cachedValues[index]; + + if (value === undefined) { + missingKeys.push(key); + continue; + } + + valuesByKey.set(key, value); + } + + if (missingKeys.length === 0) { + return valuesByKey; + } + + const loadedValuesByKey = await loadMissing(missingKeys); + + const cacheWrites = missingKeys.map((key) => { + const value = loadedValuesByKey.get(key) ?? null; + valuesByKey.set(key, value); + + return { + key: getCacheKey(key), + ttl: ttlMs, + value, + }; + }); + + if (cacheWrites.length > 0) { + await cacheManager.mset(cacheWrites); + } + + return valuesByKey; +}; diff --git a/apps/backend/src/utils/delay.ts b/apps/backend/src/utils/delay.ts index ecea5e64..e79edf73 100644 --- a/apps/backend/src/utils/delay.ts +++ b/apps/backend/src/utils/delay.ts @@ -1,10 +1,9 @@ -import { delaySchema, type DelaySchema } from "src/schema/delay.schema"; +import { type DelaySchema, delaySchema } from "src/schema/delay.schema"; export const getDelayInSeconds = (delay: DelaySchema): number => { const parsed = delaySchema.safeParse(delay); if (!parsed.success) { - console.log("Invalid delay", delay); return 0; } diff --git a/apps/backend/tsconfig.build.json b/apps/backend/tsconfig.build.json index 95bcfef3..231821a3 100644 --- a/apps/backend/tsconfig.build.json +++ b/apps/backend/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", - "exclude": ["node_modules", "e2e", "prisma", "dist", "**/*spec.ts"] + "exclude": ["node_modules", "e2e", "dist", "**/*spec.ts"] } diff --git a/apps/database/.env.example b/apps/database/.env.example new file mode 100644 index 00000000..10d2aa01 --- /dev/null +++ b/apps/database/.env.example @@ -0,0 +1 @@ +DATABASE_URL="postgresql://postgres_user:postgres_password@localhost:5432/metro-now?schema=public" diff --git a/apps/database/dist/index.d.ts b/apps/database/dist/index.d.ts new file mode 100644 index 00000000..6a254f10 --- /dev/null +++ b/apps/database/dist/index.d.ts @@ -0,0 +1,115 @@ +import type { Generated, Insertable, Kysely, Selectable, Transaction, Updateable } from "kysely"; +export { sql } from "kysely"; +export declare const VehicleType: { + readonly BUS: "BUS"; + readonly FERRY: "FERRY"; + readonly FUNICULAR: "FUNICULAR"; + readonly METRO: "METRO"; + readonly TRAIN: "TRAIN"; + readonly TRAM: "TRAM"; +}; +export type VehicleType = (typeof VehicleType)[keyof typeof VehicleType]; +export declare const LogLevel: { + readonly error: "error"; + readonly info: "info"; + readonly warn: "warn"; +}; +export type LogLevel = (typeof LogLevel)[keyof typeof LogLevel]; +export interface StopTable { + id: string; + name: string; + avgLatitude: number; + avgLongitude: number; + createdAt: Generated; + updatedAt: Generated; +} +export interface PlatformTable { + id: string; + name: string; + code: string | null; + isMetro: boolean; + latitude: number; + longitude: number; + stopId: string | null; + createdAt: Generated; + updatedAt: Generated; +} +export interface RouteTable { + id: string; + name: string; + vehicleType: VehicleType | null; + isNight: boolean | null; + createdAt: Generated; + updatedAt: Generated; +} +export interface PlatformsOnRoutesTable { + id: Generated; + platformId: string; + routeId: string; + createdAt: Generated; + updatedAt: Generated; +} +export interface GtfsRouteTable { + id: string; + type: string; + shortName: string; + longName: string | null; + url: string | null; + color: string | null; + isNight: boolean | null; + createdAt: Generated; + updatedAt: Generated; +} +export interface GtfsRouteStopTable { + id: Generated; + routeId: string; + directionId: string; + stopId: string; + stopSequence: number; + createdAt: Generated; + updatedAt: Generated; +} +export interface GtfsStopTimeTable { + id: Generated; + platformId: string | null; +} +export interface LogTable { + id: Generated; + service: string; + level: LogLevel; + message: string; + context: Record | null; + createdAt: Generated; +} +export interface MetroNowDatabase { + Stop: StopTable; + Platform: PlatformTable; + Route: RouteTable; + PlatformsOnRoutes: PlatformsOnRoutesTable; + GtfsRoute: GtfsRouteTable; + GtfsRouteStop: GtfsRouteStopTable; + GtfsStopTime: GtfsStopTimeTable; + Log: LogTable; +} +export type DatabaseClient = Kysely; +export type DatabaseTransaction = Transaction; +export type Stop = Selectable; +export type Platform = Selectable; +export type Route = Selectable; +export type PlatformsOnRoutes = Selectable; +export type GtfsRoute = Selectable; +export type GtfsRouteStop = Selectable; +export type GtfsStopTime = Selectable; +export type Log = Selectable; +export type NewStop = Insertable; +export type StopUpdate = Updateable; +export type NewPlatform = Insertable; +export type PlatformUpdate = Updateable; +export type NewRoute = Insertable; +export type RouteUpdate = Updateable; +export type NewPlatformsOnRoutes = Insertable; +export type NewGtfsRoute = Insertable; +export type GtfsRouteUpdate = Updateable; +export type NewGtfsRouteStop = Insertable; +export type NewLog = Insertable; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/apps/database/dist/index.d.ts.map b/apps/database/dist/index.d.ts.map new file mode 100644 index 00000000..680e4096 --- /dev/null +++ b/apps/database/dist/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACR,SAAS,EACT,UAAU,EACV,MAAM,EACN,UAAU,EACV,WAAW,EACX,UAAU,EACb,MAAM,QAAQ,CAAC;AAEhB,OAAO,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAE7B,eAAO,MAAM,WAAW;;;;;;;CAOd,CAAC;AAEX,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,OAAO,WAAW,CAAC,CAAC;AAEzE,eAAO,MAAM,QAAQ;;;;CAIX,CAAC;AAEX,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,QAAQ,CAAC,CAAC,MAAM,OAAO,QAAQ,CAAC,CAAC;AAEhE,MAAM,WAAW,SAAS;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;IAC3B,SAAS,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;IAC3B,SAAS,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,UAAU;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;IAC3B,SAAS,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,sBAAsB;IACnC,EAAE,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;IAC3B,SAAS,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;IAC3B,SAAS,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,kBAAkB;IAC/B,EAAE,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;IAC3B,SAAS,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAC9B,EAAE,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;IACtB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,QAAQ;IACrB,EAAE,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,QAAQ,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACxC,SAAS,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,aAAa,CAAC;IACxB,KAAK,EAAE,UAAU,CAAC;IAClB,iBAAiB,EAAE,sBAAsB,CAAC;IAC1C,SAAS,EAAE,cAAc,CAAC;IAC1B,aAAa,EAAE,kBAAkB,CAAC;IAClC,YAAY,EAAE,iBAAiB,CAAC;IAChC,GAAG,EAAE,QAAQ,CAAC;CACjB;AAED,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC;AACtD,MAAM,MAAM,mBAAmB,GAAG,WAAW,CAAC,gBAAgB,CAAC,CAAC;AAEhE,MAAM,MAAM,IAAI,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;AACzC,MAAM,MAAM,QAAQ,GAAG,UAAU,CAAC,aAAa,CAAC,CAAC;AACjD,MAAM,MAAM,KAAK,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;AAC3C,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC,sBAAsB,CAAC,CAAC;AACnE,MAAM,MAAM,SAAS,GAAG,UAAU,CAAC,cAAc,CAAC,CAAC;AACnD,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,kBAAkB,CAAC,CAAC;AAC3D,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC;AACzD,MAAM,MAAM,GAAG,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;AAEvC,MAAM,MAAM,OAAO,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;AAC5C,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;AAC/C,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,aAAa,CAAC,CAAC;AACpD,MAAM,MAAM,cAAc,GAAG,UAAU,CAAC,aAAa,CAAC,CAAC;AACvD,MAAM,MAAM,QAAQ,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;AAC9C,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;AACjD,MAAM,MAAM,oBAAoB,GAAG,UAAU,CAAC,sBAAsB,CAAC,CAAC;AACtE,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,cAAc,CAAC,CAAC;AACtD,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,cAAc,CAAC,CAAC;AACzD,MAAM,MAAM,gBAAgB,GAAG,UAAU,CAAC,kBAAkB,CAAC,CAAC;AAC9D,MAAM,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC"} \ No newline at end of file diff --git a/apps/database/dist/index.js b/apps/database/dist/index.js new file mode 100644 index 00000000..698d2d89 --- /dev/null +++ b/apps/database/dist/index.js @@ -0,0 +1,19 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.LogLevel = exports.VehicleType = exports.sql = void 0; +var kysely_1 = require("kysely"); +Object.defineProperty(exports, "sql", { enumerable: true, get: function () { return kysely_1.sql; } }); +exports.VehicleType = { + BUS: "BUS", + FERRY: "FERRY", + FUNICULAR: "FUNICULAR", + METRO: "METRO", + TRAIN: "TRAIN", + TRAM: "TRAM", +}; +exports.LogLevel = { + error: "error", + info: "info", + warn: "warn", +}; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/apps/database/dist/index.js.map b/apps/database/dist/index.js.map new file mode 100644 index 00000000..34e6bdc6 --- /dev/null +++ b/apps/database/dist/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":";;;AASA,iCAA6B;AAApB,6FAAA,GAAG,OAAA;AAEC,QAAA,WAAW,GAAG;IACvB,GAAG,EAAE,KAAK;IACV,KAAK,EAAE,OAAO;IACd,SAAS,EAAE,WAAW;IACtB,KAAK,EAAE,OAAO;IACd,KAAK,EAAE,OAAO;IACd,IAAI,EAAE,MAAM;CACN,CAAC;AAIE,QAAA,QAAQ,GAAG;IACpB,KAAK,EAAE,OAAO;IACd,IAAI,EAAE,MAAM;IACZ,IAAI,EAAE,MAAM;CACN,CAAC"} \ No newline at end of file diff --git a/apps/database/index.ts b/apps/database/index.ts new file mode 100644 index 00000000..9ae4ce79 --- /dev/null +++ b/apps/database/index.ts @@ -0,0 +1,138 @@ +import type { + Generated, + Insertable, + Kysely, + Selectable, + Transaction, + Updateable, +} from "kysely"; + +export { sql } from "kysely"; + +export const VehicleType = { + BUS: "BUS", + FERRY: "FERRY", + FUNICULAR: "FUNICULAR", + METRO: "METRO", + TRAIN: "TRAIN", + TRAM: "TRAM", +} as const; + +export type VehicleType = (typeof VehicleType)[keyof typeof VehicleType]; + +export const LogLevel = { + error: "error", + info: "info", + warn: "warn", +} as const; + +export type LogLevel = (typeof LogLevel)[keyof typeof LogLevel]; + +export interface StopTable { + id: string; + name: string; + avgLatitude: number; + avgLongitude: number; + createdAt: Generated; + updatedAt: Generated; +} + +export interface PlatformTable { + id: string; + name: string; + code: string | null; + isMetro: boolean; + latitude: number; + longitude: number; + stopId: string | null; + createdAt: Generated; + updatedAt: Generated; +} + +export interface RouteTable { + id: string; + name: string; + vehicleType: VehicleType | null; + isNight: boolean | null; + createdAt: Generated; + updatedAt: Generated; +} + +export interface PlatformsOnRoutesTable { + id: Generated; + platformId: string; + routeId: string; + createdAt: Generated; + updatedAt: Generated; +} + +export interface GtfsRouteTable { + id: string; + type: string; + shortName: string; + longName: string | null; + url: string | null; + color: string | null; + isNight: boolean | null; + createdAt: Generated; + updatedAt: Generated; +} + +export interface GtfsRouteStopTable { + id: Generated; + routeId: string; + directionId: string; + stopId: string; + stopSequence: number; + createdAt: Generated; + updatedAt: Generated; +} + +export interface GtfsStopTimeTable { + id: Generated; + platformId: string | null; +} + +export interface LogTable { + id: Generated; + service: string; + level: LogLevel; + message: string; + context: Record | null; + createdAt: Generated; +} + +export interface MetroNowDatabase { + Stop: StopTable; + Platform: PlatformTable; + Route: RouteTable; + PlatformsOnRoutes: PlatformsOnRoutesTable; + GtfsRoute: GtfsRouteTable; + GtfsRouteStop: GtfsRouteStopTable; + GtfsStopTime: GtfsStopTimeTable; + Log: LogTable; +} + +export type DatabaseClient = Kysely; +export type DatabaseTransaction = Transaction; + +export type Stop = Selectable; +export type Platform = Selectable; +export type Route = Selectable; +export type PlatformsOnRoutes = Selectable; +export type GtfsRoute = Selectable; +export type GtfsRouteStop = Selectable; +export type GtfsStopTime = Selectable; +export type Log = Selectable; + +export type NewStop = Insertable; +export type StopUpdate = Updateable; +export type NewPlatform = Insertable; +export type PlatformUpdate = Updateable; +export type NewRoute = Insertable; +export type RouteUpdate = Updateable; +export type NewPlatformsOnRoutes = Insertable; +export type NewGtfsRoute = Insertable; +export type GtfsRouteUpdate = Updateable; +export type NewGtfsRouteStop = Insertable; +export type NewLog = Insertable; diff --git a/apps/database/migrate.ts b/apps/database/migrate.ts new file mode 100644 index 00000000..e0253a2d --- /dev/null +++ b/apps/database/migrate.ts @@ -0,0 +1,92 @@ +import { promises as fs } from "node:fs"; +import * as path from "node:path"; + +import { + FileMigrationProvider, + Kysely, + Migrator, + PostgresDialect, +} from "kysely"; +import { Pool } from "pg"; +import type { MetroNowDatabase } from "./index"; + +const createDb = (): Kysely => { + const connectionString = process.env.DATABASE_URL; + + if (!connectionString) { + throw new Error("DATABASE_URL environment variable is required"); + } + + return new Kysely({ + dialect: new PostgresDialect({ + pool: new Pool({ connectionString }), + }), + }); +}; + +const runMigrations = async (direction: "up" | "down"): Promise => { + const db = createDb(); + + const migrator = new Migrator({ + db, + provider: new FileMigrationProvider({ + fs, + path, + migrationFolder: path.join(__dirname, "migrations"), + }), + }); + + const { error, results } = + direction === "up" + ? await migrator.migrateToLatest() + : await migrator.migrateDown(); + + for (const result of results ?? []) { + if (result.status === "Success") { + console.log( + `Migration "${result.migrationName}" ${direction === "up" ? "applied" : "reverted"} successfully`, + ); + } else if (result.status === "Error") { + console.error( + `Failed to ${direction === "up" ? "apply" : "revert"} migration "${result.migrationName}"`, + ); + } + } + + if (error) { + console.error("Migration failed:", error); + await db.destroy(); + process.exit(1); + } + + if (!results?.length) { + console.log("No migrations to run"); + } + + await db.destroy(); +}; + +const command = process.argv[2]; + +switch (command) { + case "up": + case "deploy": + case "latest": + runMigrations("up").catch((e) => { + console.error(e); + process.exit(1); + }); + break; + case "down": + case "rollback": + runMigrations("down").catch((e) => { + console.error(e); + process.exit(1); + }); + break; + default: + console.log("Usage: ts-node migrate.ts "); + console.log(" up / deploy / latest - Apply all pending migrations"); + console.log(" down / rollback - Revert the last migration"); + process.exit(1); +} diff --git a/apps/database/migrations/0001_initial_schema.ts b/apps/database/migrations/0001_initial_schema.ts new file mode 100644 index 00000000..8db28dcb --- /dev/null +++ b/apps/database/migrations/0001_initial_schema.ts @@ -0,0 +1,167 @@ +import type { Kysely } from "kysely"; +import { sql } from "kysely"; + +export async function up( + db: // biome-ignore lint/suspicious/noExplicitAny: Kysely migration signature + Kysely, +): Promise { + // Extensions + await sql`CREATE EXTENSION IF NOT EXISTS cube`.execute(db); + await sql`CREATE EXTENSION IF NOT EXISTS earthdistance`.execute(db); + + // Enums + await sql` + DO $$ BEGIN + CREATE TYPE "VehicleType" AS ENUM ('METRO', 'BUS', 'TRAM', 'TRAIN', 'FERRY', 'FUNICULAR'); + EXCEPTION + WHEN duplicate_object THEN NULL; + END $$ + `.execute(db); + + await sql` + DO $$ BEGIN + CREATE TYPE "LogLevel" AS ENUM ('info', 'warn', 'error'); + EXCEPTION + WHEN duplicate_object THEN NULL; + END $$ + `.execute(db); + + // Stop + await sql` + CREATE TABLE IF NOT EXISTS "Stop" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "avgLatitude" DOUBLE PRECISION NOT NULL, + "avgLongitude" DOUBLE PRECISION NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + CONSTRAINT "Stop_pkey" PRIMARY KEY ("id") + ) + `.execute(db); + + // Platform + await sql` + CREATE TABLE IF NOT EXISTS "Platform" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "code" TEXT, + "isMetro" BOOLEAN NOT NULL, + "latitude" DOUBLE PRECISION NOT NULL, + "longitude" DOUBLE PRECISION NOT NULL, + "stopId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + CONSTRAINT "Platform_pkey" PRIMARY KEY ("id"), + CONSTRAINT "Platform_stopId_fkey" FOREIGN KEY ("stopId") REFERENCES "Stop"("id") ON DELETE SET NULL ON UPDATE CASCADE + ) + `.execute(db); + + // Route + await sql` + CREATE TABLE IF NOT EXISTS "Route" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "vehicleType" "VehicleType", + "isNight" BOOLEAN, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + CONSTRAINT "Route_pkey" PRIMARY KEY ("id") + ) + `.execute(db); + + // PlatformsOnRoutes + await sql` + CREATE TABLE IF NOT EXISTS "PlatformsOnRoutes" ( + "id" TEXT NOT NULL, + "platformId" TEXT NOT NULL, + "routeId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + CONSTRAINT "PlatformsOnRoutes_pkey" PRIMARY KEY ("id"), + CONSTRAINT "PlatformsOnRoutes_platformId_fkey" FOREIGN KEY ("platformId") REFERENCES "Platform"("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "PlatformsOnRoutes_routeId_fkey" FOREIGN KEY ("routeId") REFERENCES "Route"("id") ON DELETE RESTRICT ON UPDATE CASCADE + ) + `.execute(db); + + await sql` + CREATE UNIQUE INDEX IF NOT EXISTS "PlatformsOnRoutes_platformId_routeId_key" + ON "PlatformsOnRoutes"("platformId", "routeId") + `.execute(db); + + // GtfsRoute + await sql` + CREATE TABLE IF NOT EXISTS "GtfsRoute" ( + "id" TEXT NOT NULL, + "type" TEXT NOT NULL, + "shortName" TEXT NOT NULL, + "longName" TEXT, + "url" TEXT, + "color" TEXT, + "isNight" BOOLEAN, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + CONSTRAINT "GtfsRoute_pkey" PRIMARY KEY ("id") + ) + `.execute(db); + + // GtfsRouteStop + await sql` + CREATE TABLE IF NOT EXISTS "GtfsRouteStop" ( + "id" TEXT NOT NULL, + "routeId" TEXT NOT NULL, + "directionId" TEXT NOT NULL, + "stopId" TEXT NOT NULL, + "stopSequence" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + CONSTRAINT "GtfsRouteStop_pkey" PRIMARY KEY ("id"), + CONSTRAINT "GtfsRouteStop_routeId_fkey" FOREIGN KEY ("routeId") REFERENCES "GtfsRoute"("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "GtfsRouteStop_stopId_fkey" FOREIGN KEY ("stopId") REFERENCES "Platform"("id") ON DELETE RESTRICT ON UPDATE CASCADE + ) + `.execute(db); + + await sql` + CREATE UNIQUE INDEX IF NOT EXISTS "GtfsRouteStop_routeId_directionId_stopId_stopSequence_key" + ON "GtfsRouteStop"("routeId", "directionId", "stopId", "stopSequence") + `.execute(db); + + // Log + await sql` + CREATE TABLE IF NOT EXISTS "Log" ( + "id" TEXT NOT NULL, + "service" TEXT NOT NULL, + "level" "LogLevel" NOT NULL, + "message" TEXT NOT NULL, + "context" JSONB, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "Log_pkey" PRIMARY KEY ("id") + ) + `.execute(db); + + await sql` + CREATE INDEX IF NOT EXISTS "Log_service_createdAt_idx" + ON "Log"("service", "createdAt") + `.execute(db); + + await sql` + CREATE INDEX IF NOT EXISTS "Log_level_createdAt_idx" + ON "Log"("level", "createdAt") + `.execute(db); +} + +export async function down( + db: // biome-ignore lint/suspicious/noExplicitAny: Kysely migration signature + Kysely, +): Promise { + await sql`DROP TABLE IF EXISTS "Log" CASCADE`.execute(db); + await sql`DROP TABLE IF EXISTS "GtfsRouteStop" CASCADE`.execute(db); + await sql`DROP TABLE IF EXISTS "GtfsRoute" CASCADE`.execute(db); + await sql`DROP TABLE IF EXISTS "PlatformsOnRoutes" CASCADE`.execute(db); + await sql`DROP TABLE IF EXISTS "Route" CASCADE`.execute(db); + await sql`DROP TABLE IF EXISTS "Platform" CASCADE`.execute(db); + await sql`DROP TABLE IF EXISTS "Stop" CASCADE`.execute(db); + await sql`DROP TYPE IF EXISTS "LogLevel"`.execute(db); + await sql`DROP TYPE IF EXISTS "VehicleType"`.execute(db); + await sql`DROP EXTENSION IF EXISTS earthdistance`.execute(db); + await sql`DROP EXTENSION IF EXISTS cube`.execute(db); +} diff --git a/apps/database/package.json b/apps/database/package.json new file mode 100644 index 00000000..e941d46b --- /dev/null +++ b/apps/database/package.json @@ -0,0 +1,23 @@ +{ + "name": "@metro-now/database", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "migrate:deploy": "dotenv -e ../backend/.env.local -- ts-node migrate.ts up", + "migrate:rollback": "dotenv -e ../backend/.env.local -- ts-node migrate.ts down", + "seed": "dotenv -e ../backend/.env.local -- ts-node seed.ts" + }, + "dependencies": { + "kysely": "^0.28.14", + "pg": "^8.20.0" + }, + "devDependencies": { + "@types/pg": "^8.15.5", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + }, + "optionalDependencies": { + "dotenv-cli": "^8.0.0" + } +} diff --git a/apps/database/seed.ts b/apps/database/seed.ts new file mode 100644 index 00000000..9cdc4f5f --- /dev/null +++ b/apps/database/seed.ts @@ -0,0 +1,129 @@ +import * as fs from "node:fs"; + +import { Kysely, PostgresDialect } from "kysely"; +import { Pool } from "pg"; +import type { MetroNowDatabase, NewPlatform, NewRoute, NewStop } from "./index"; + +type Stop = { + id: string; + name: string; + avgLatitude: number; + avgLongitude: number; +}; + +type Route = { + id: string; + name: string; + isNight: boolean | null; + vehicleType: null; +}; + +type Platform = { + id: string; + name: string; + isMetro: boolean; + latitude: number; + longitude: number; + stopId: string; +}; + +const BATCH_SIZE = 500; + +const parseSeedFile = (path: string): T => { + const raw = fs.readFileSync(path).toString(); + + return JSON.parse(raw); +}; + +async function insertInBatches( + transaction: Kysely, + table: "Stop" | "Platform" | "Route", + values: T[], +): Promise { + for (let i = 0; i < values.length; i += BATCH_SIZE) { + // biome-ignore lint/suspicious/noExplicitAny: Kysely insertInto returns different types per table, generic batch helper needs type erasure + await (transaction.insertInto(table) as any) + .values(values.slice(i, i + BATCH_SIZE)) + .execute(); + } +} + +async function main() { + const connectionString = process.env.DATABASE_URL; + + if (!connectionString) { + throw new Error("DATABASE_URL environment variable is required"); + } + + const db = new Kysely({ + dialect: new PostgresDialect({ + pool: new Pool({ connectionString }), + }), + }); + + const stops = parseSeedFile("./seeds/stops.json"); + const routes = parseSeedFile("./seeds/routes.json"); + const platforms = parseSeedFile("./seeds/platforms.json"); + + await db.transaction().execute(async (transaction) => { + await transaction.deleteFrom("PlatformsOnRoutes").execute(); + await transaction.deleteFrom("Route").execute(); + await transaction.deleteFrom("Platform").execute(); + await transaction.deleteFrom("Stop").execute(); + + const timestamp = new Date(); + + await insertInBatches( + transaction, + "Stop", + stops.map((stop) => ({ + id: stop.id, + name: stop.name, + avgLatitude: stop.avgLatitude, + avgLongitude: stop.avgLongitude, + createdAt: timestamp, + updatedAt: timestamp, + })), + ); + + await insertInBatches( + transaction, + "Platform", + platforms.map((platform) => ({ + id: platform.id, + name: platform.name, + isMetro: platform.isMetro, + latitude: platform.latitude, + longitude: platform.longitude, + stopId: platform.stopId ?? null, + code: null, + createdAt: timestamp, + updatedAt: timestamp, + })), + ); + + await insertInBatches( + transaction, + "Route", + routes.map((route) => ({ + id: route.id, + name: route.name, + vehicleType: null, + isNight: null, + createdAt: timestamp, + updatedAt: timestamp, + })), + ); + }); + + await db.destroy(); +} + +main() + .then(() => { + console.log("Seed completed successfully"); + }) + .catch((e) => { + console.error(e); + process.exit(1); + }); diff --git a/apps/backend/prisma/seeds/platforms-on-routes.json b/apps/database/seeds/platforms-on-routes.json similarity index 100% rename from apps/backend/prisma/seeds/platforms-on-routes.json rename to apps/database/seeds/platforms-on-routes.json diff --git a/apps/backend/prisma/seeds/platforms.json b/apps/database/seeds/platforms.json similarity index 100% rename from apps/backend/prisma/seeds/platforms.json rename to apps/database/seeds/platforms.json diff --git a/apps/backend/prisma/seeds/routes.json b/apps/database/seeds/routes.json similarity index 100% rename from apps/backend/prisma/seeds/routes.json rename to apps/database/seeds/routes.json diff --git a/apps/backend/prisma/seeds/stops.json b/apps/database/seeds/stops.json similarity index 100% rename from apps/backend/prisma/seeds/stops.json rename to apps/database/seeds/stops.json diff --git a/apps/database/tsconfig.json b/apps/database/tsconfig.json new file mode 100644 index 00000000..c2f0f816 --- /dev/null +++ b/apps/database/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["index.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/apps/dataloader/.gitignore b/apps/dataloader/.gitignore new file mode 100644 index 00000000..53c37a16 --- /dev/null +++ b/apps/dataloader/.gitignore @@ -0,0 +1 @@ +dist \ No newline at end of file diff --git a/apps/dataloader/package.json b/apps/dataloader/package.json new file mode 100644 index 00000000..6e85f642 --- /dev/null +++ b/apps/dataloader/package.json @@ -0,0 +1,36 @@ +{ + "name": "@metro-now/dataloader", + "version": "1.0.0", + "description": "Metro Now background sync worker", + "main": "dist/app.js", + "scripts": { + "build": "pnpm --filter @metro-now/shared build && pnpm --filter @metro-now/database build && tsc", + "start": "node dist/app.js", + "dev": "nodemon --watch src --ext ts --exec \"pnpm run build && pnpm run start\"", + "lint": "biome check --config-path ../../biome.json ../../apps/dataloader/src ../../apps/dataloader/package.json ../../apps/dataloader/tsconfig.json", + "format": "biome format --config-path ../../biome.json ../../apps/dataloader/src ../../apps/dataloader/package.json ../../apps/dataloader/tsconfig.json --write", + "format:check": "biome check --config-path ../../biome.json ../../apps/dataloader/src ../../apps/dataloader/package.json ../../apps/dataloader/tsconfig.json", + "test": "pnpm run build && node --test dist/**/*.spec.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@fast-csv/parse": "^5.0.5", + "@metro-now/database": "workspace:*", + "@metro-now/shared": "workspace:*", + "express": "^5.2.1", + "node-cron": "^4.2.1", + "radash": "^12.1.1", + "unzipper": "^0.12.3", + "zod": "^3.24.2" + }, + "devDependencies": { + "@types/express": "^5.0.6", + "@types/node": "^25.5.0", + "@types/node-cron": "^3.0.11", + "@types/unzipper": "^0.10.11", + "nodemon": "^3.1.14", + "typescript": "^5.8.3" + } +} diff --git a/apps/dataloader/src/app.ts b/apps/dataloader/src/app.ts new file mode 100644 index 00000000..e9ee58e2 --- /dev/null +++ b/apps/dataloader/src/app.ts @@ -0,0 +1,164 @@ +import express, { type Request, type Response } from "express"; + +import { getDataloaderEnv, loadEnvironment } from "./config/env"; +import { CronService } from "./services/cron.service"; +import { DatabaseLogStore } from "./services/database-log-store.service"; +import { DatabaseService } from "./services/database.service"; +import { SyncService } from "./services/sync.service"; +import { logger } from "./utils/logger"; + +loadEnvironment(); + +const env = getDataloaderEnv(); +const app = express(); +const databaseService = new DatabaseService(); +logger.setTransport(new DatabaseLogStore(databaseService.db)); +const syncService = new SyncService(databaseService.db); +const cronService = new CronService(); + +app.use(express.json()); + +app.get("/", (_req: Request, res: Response) => { + res.send("Metro Now dataloader"); +}); + +app.get("/health", async (_req: Request, res: Response) => { + try { + const health = await databaseService.performHealthCheck(); + + res.json({ + status: "ok", + service: "dataloader", + timestamp: new Date().toISOString(), + database: "connected", + extensions: health.extensions, + }); + } catch (error) { + res.status(500).json({ + status: "error", + service: "dataloader", + message: error instanceof Error ? error.message : String(error), + }); + } +}); + +app.get("/status", (_req: Request, res: Response) => { + res.json({ + service: "dataloader", + environment: process.env.NODE_ENV ?? "development", + port: env.port, + uptimeSeconds: process.uptime(), + sync: syncService.getStatus(), + cron: cronService.getJobStatus(), + timestamp: new Date().toISOString(), + }); +}); + +app.get("/api/database/stats", async (_req: Request, res: Response) => { + try { + const [stats, preview] = await Promise.all([ + databaseService.getDatabaseStats(), + databaseService.getDataPreview(), + ]); + + res.json({ + success: true, + stats, + preview, + }); + } catch (error) { + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : String(error), + }); + } +}); + +app.post("/api/sync/run", async (_req: Request, res: Response) => { + try { + const result = await syncService.syncEverything("manual"); + + res.json({ + success: true, + result, + }); + } catch (error) { + res.status(500).json({ + success: false, + message: error instanceof Error ? error.message : String(error), + }); + } +}); + +app.get("/api/sync/status", (_req: Request, res: Response) => { + res.json({ + success: true, + sync: syncService.getStatus(), + cron: cronService.getJobStatus(), + }); +}); + +app.post("/api/cron/start/:jobName", (req: Request, res: Response) => { + const success = cronService.startJob(req.params.jobName); + + res.status(success ? 200 : 404).json({ + success, + }); +}); + +app.post("/api/cron/stop/:jobName", (req: Request, res: Response) => { + const success = cronService.stopJob(req.params.jobName); + + res.status(success ? 200 : 404).json({ + success, + }); +}); + +const bootstrap = async (): Promise => { + cronService.addJob( + { + name: "pid-sync", + description: "Syncs PID stops and GTFS snapshots into Postgres", + enabled: true, + schedule: env.syncSchedule, + }, + async () => { + await syncService.syncEverything("cron"); + }, + ); + + await syncService.syncEverything("startup"); + cronService.startAll(); + + app.listen(env.port, () => { + logger.info("Dataloader listening", { + port: env.port, + syncSchedule: env.syncSchedule, + }); + }); +}; + +const shutdown = async (signal: string): Promise => { + logger.info("Received shutdown signal", { signal }); + cronService.shutdown(); + await logger.flush(); + await databaseService.disconnect(); + process.exit(0); +}; + +void bootstrap().catch(async (error) => { + logger.error("Failed to start dataloader", { + error: error instanceof Error ? error.message : String(error), + }); + await logger.flush(); + await databaseService.disconnect(); + process.exit(1); +}); + +process.on("SIGINT", () => { + void shutdown("SIGINT"); +}); + +process.on("SIGTERM", () => { + void shutdown("SIGTERM"); +}); diff --git a/apps/dataloader/src/config/env.ts b/apps/dataloader/src/config/env.ts new file mode 100644 index 00000000..0cd86cb9 --- /dev/null +++ b/apps/dataloader/src/config/env.ts @@ -0,0 +1,50 @@ +import { existsSync } from "node:fs"; +import { resolve } from "node:path"; +import { commonServerEnvSchema, validateEnv } from "@metro-now/shared"; +import { z } from "zod"; + +type DataloaderEnv = { + port: number; + syncSchedule: string; +}; + +const dataloaderEnvSchema = commonServerEnvSchema.extend({ + DATALOADER_PORT: z.coerce.number().int().positive().default(3008), + SYNC_CRON: z.string().default("0 */7 * * *"), +}); + +const ENV_FILES = [ + ".env.local", + ".env", + "../backend/.env.local", + "../backend/.env", + "../../.env.docker", +] as const; + +let envLoaded = false; + +export const loadEnvironment = (): void => { + if (envLoaded) { + return; + } + + for (const relativePath of ENV_FILES) { + const filePath = resolve(process.cwd(), relativePath); + + if (existsSync(filePath)) { + process.loadEnvFile(filePath); + } + } + + envLoaded = true; +}; + +export const getDataloaderEnv = (): DataloaderEnv => { + loadEnvironment(); + const env = validateEnv(dataloaderEnvSchema); + + return { + port: env.DATALOADER_PORT, + syncSchedule: env.SYNC_CRON, + }; +}; diff --git a/apps/backend/src/modules/import/schema/pid-stops.schema.ts b/apps/dataloader/src/schema/pid-stops.schema.ts similarity index 100% rename from apps/backend/src/modules/import/schema/pid-stops.schema.ts rename to apps/dataloader/src/schema/pid-stops.schema.ts diff --git a/apps/dataloader/src/services/cron.service.ts b/apps/dataloader/src/services/cron.service.ts new file mode 100644 index 00000000..a661bb46 --- /dev/null +++ b/apps/dataloader/src/services/cron.service.ts @@ -0,0 +1,160 @@ +import cron, { type ScheduledTask } from "node-cron"; + +import { logger } from "../utils/logger"; + +export type CronJobState = { + description: string; + enabled: boolean; + running: boolean; + schedule: string; + lastStartedAt: string | null; + lastFinishedAt: string | null; + lastError: string | null; +}; + +type CronJobConfig = { + name: string; + description: string; + enabled: boolean; + schedule: string; +}; + +type ManagedCronJob = { + config: CronJobConfig; + lastError: string | null; + lastFinishedAt: Date | null; + lastStartedAt: Date | null; + running: boolean; + task: ScheduledTask; +}; + +export class CronService { + private readonly jobs = new Map(); + + addJob(config: CronJobConfig, task: () => Promise): void { + if (this.jobs.has(config.name)) { + this.removeJob(config.name); + } + + if (!config.enabled) { + logger.info("Skipping disabled cron job", { name: config.name }); + return; + } + + if (!cron.validate(config.schedule)) { + throw new Error(`Invalid cron schedule: ${config.schedule}`); + } + + const managedJob: ManagedCronJob = { + config, + lastError: null, + lastFinishedAt: null, + lastStartedAt: null, + running: false, + task: cron.createTask( + config.schedule, + async () => { + managedJob.running = true; + managedJob.lastStartedAt = new Date(); + managedJob.lastError = null; + + try { + logger.info("Running cron job", { + name: config.name, + schedule: config.schedule, + }); + await task(); + } catch (error) { + managedJob.lastError = + error instanceof Error + ? error.message + : String(error); + logger.error("Cron job failed", { + name: config.name, + error: managedJob.lastError, + }); + } finally { + managedJob.running = false; + managedJob.lastFinishedAt = new Date(); + } + }, + { + name: config.name, + noOverlap: true, + timezone: "Europe/Prague", + }, + ), + }; + + this.jobs.set(config.name, managedJob); + } + + startJob(name: string): boolean { + const job = this.jobs.get(name); + + if (!job) { + return false; + } + + job.task.start(); + return true; + } + + stopJob(name: string): boolean { + const job = this.jobs.get(name); + + if (!job) { + return false; + } + + job.task.stop(); + return true; + } + + removeJob(name: string): boolean { + const job = this.jobs.get(name); + + if (!job) { + return false; + } + + job.task.stop(); + this.jobs.delete(name); + + return true; + } + + startAll(): void { + for (const job of this.jobs.values()) { + job.task.start(); + } + } + + stopAll(): void { + for (const job of this.jobs.values()) { + job.task.stop(); + } + } + + getJobStatus(): Record { + return Object.fromEntries( + Array.from(this.jobs.entries()).map(([name, job]) => [ + name, + { + description: job.config.description, + enabled: job.config.enabled, + running: job.running, + schedule: job.config.schedule, + lastStartedAt: job.lastStartedAt?.toISOString() ?? null, + lastFinishedAt: job.lastFinishedAt?.toISOString() ?? null, + lastError: job.lastError, + }, + ]), + ); + } + + shutdown(): void { + this.stopAll(); + this.jobs.clear(); + } +} diff --git a/apps/dataloader/src/services/database-log-store.service.ts b/apps/dataloader/src/services/database-log-store.service.ts new file mode 100644 index 00000000..aa567fe2 --- /dev/null +++ b/apps/dataloader/src/services/database-log-store.service.ts @@ -0,0 +1,46 @@ +import { randomUUID } from "node:crypto"; +import type { DatabaseClient, NewLog } from "@metro-now/database"; + +import type { LogEntry, LogTransport } from "../utils/logger"; + +const SERVICE_NAME = "dataloader"; + +const formatLogStoreError = (error: unknown): string => { + return error instanceof Error ? error.message : String(error); +}; + +export class DatabaseLogStore implements LogTransport { + private pendingWrite: Promise = Promise.resolve(); + + constructor(private readonly db: DatabaseClient) {} + + write(entry: LogEntry): Promise { + this.pendingWrite = this.pendingWrite + .then(async () => { + const row: NewLog = { + id: randomUUID(), + service: SERVICE_NAME, + level: entry.level, + message: entry.message, + context: entry.context, + createdAt: entry.createdAt, + }; + + await this.db.insertInto("Log").values(row).execute(); + }) + .catch((error) => { + console.error( + `[${new Date().toISOString()}] [ERROR] Failed to write dataloader log to database`, + { + error: formatLogStoreError(error), + }, + ); + }); + + return this.pendingWrite; + } + + async flush(): Promise { + await this.pendingWrite; + } +} diff --git a/apps/dataloader/src/services/database.service.ts b/apps/dataloader/src/services/database.service.ts new file mode 100644 index 00000000..6ae63cca --- /dev/null +++ b/apps/dataloader/src/services/database.service.ts @@ -0,0 +1,127 @@ +import { type DatabaseClient, sql } from "@metro-now/database"; +import { createDatabaseClient } from "@metro-now/shared"; + +export class DatabaseService { + readonly db: DatabaseClient; + + constructor( + db: DatabaseClient = createDatabaseClient({ env: process.env }), + ) { + this.db = db; + } + + async getDatabaseStats(): Promise<{ + stops: number; + platforms: number; + routes: number; + platformRoutes: number; + gtfsRoutes: number; + gtfsRouteStops: number; + }> { + const [ + stopsResult, + platformsResult, + routesResult, + platformRoutesResult, + gtfsRoutesResult, + gtfsRouteStopsResult, + ] = await Promise.all([ + this.db + .selectFrom("Stop") + .select(({ fn }) => fn.countAll().as("count")) + .executeTakeFirstOrThrow(), + this.db + .selectFrom("Platform") + .select(({ fn }) => fn.countAll().as("count")) + .executeTakeFirstOrThrow(), + this.db + .selectFrom("Route") + .select(({ fn }) => fn.countAll().as("count")) + .executeTakeFirstOrThrow(), + this.db + .selectFrom("PlatformsOnRoutes") + .select(({ fn }) => fn.countAll().as("count")) + .executeTakeFirstOrThrow(), + this.db + .selectFrom("GtfsRoute") + .select(({ fn }) => fn.countAll().as("count")) + .executeTakeFirstOrThrow(), + this.db + .selectFrom("GtfsRouteStop") + .select(({ fn }) => fn.countAll().as("count")) + .executeTakeFirstOrThrow(), + ]); + + return { + stops: Number(stopsResult.count), + platforms: Number(platformsResult.count), + routes: Number(routesResult.count), + platformRoutes: Number(platformRoutesResult.count), + gtfsRoutes: Number(gtfsRoutesResult.count), + gtfsRouteStops: Number(gtfsRouteStopsResult.count), + }; + } + + async getDataPreview(): Promise<{ + stops: Array<{ + id: string; + name: string; + }>; + platforms: Array<{ + id: string; + name: string; + stopId: string | null; + }>; + gtfsRoutes: Array<{ + id: string; + shortName: string; + }>; + }> { + const [stops, platforms, gtfsRoutes] = await Promise.all([ + this.db + .selectFrom("Stop") + .select(["id", "name"]) + .orderBy("id", "asc") + .limit(5) + .execute(), + this.db + .selectFrom("Platform") + .select(["id", "name", "stopId"]) + .orderBy("id", "asc") + .limit(5) + .execute(), + this.db + .selectFrom("GtfsRoute") + .select(["id", "shortName"]) + .orderBy("id", "asc") + .limit(5) + .execute(), + ]); + + return { + stops, + platforms, + gtfsRoutes, + }; + } + + async performHealthCheck(): Promise<{ + extensions: string[]; + }> { + await sql`SELECT 1`.execute(this.db); + + const extensions = await sql<{ extname: string }>` + SELECT extname + FROM pg_extension + ORDER BY extname + `.execute(this.db); + + return { + extensions: extensions.rows.map(({ extname }) => extname), + }; + } + + async disconnect(): Promise { + await this.db.destroy(); + } +} diff --git a/apps/dataloader/src/services/gtfs.service.ts b/apps/dataloader/src/services/gtfs.service.ts new file mode 100644 index 00000000..1dc6d9bc --- /dev/null +++ b/apps/dataloader/src/services/gtfs.service.ts @@ -0,0 +1,146 @@ +import { Open as unzipperOpen } from "unzipper"; +import { z } from "zod"; + +import type { GtfsSnapshot } from "../types/sync.types"; +import { parseCsvString } from "../utils/csv.utils"; +import { fetchWithTimeout } from "../utils/fetch.utils"; + +const GTFS_ARCHIVE_URL = "https://data.pid.cz/PID_GTFS.zip"; + +const gtfsRouteRecordSchema = z.object({ + route_id: z.string().min(1), + route_short_name: z.string().min(1), + route_long_name: z.string().optional(), + route_type: z.string().min(1), + route_color: z.string().optional(), + is_night: z.string().optional(), + route_url: z.string().optional(), +}); + +const gtfsRouteStopRecordSchema = z.object({ + route_id: z.string().min(1), + direction_id: z.string().min(1), + stop_id: z.string().min(1), + stop_sequence: z.string().min(1), +}); + +export class GtfsService { + async getGtfsSnapshot(platformIds: Set): Promise { + const response = await fetchWithTimeout(GTFS_ARCHIVE_URL); + + if (!response.ok) { + throw new Error( + `Failed to fetch GTFS archive: ${response.status} ${response.statusText}`, + ); + } + + const directory = await unzipperOpen.buffer( + Buffer.from(await response.arrayBuffer()), + ); + const routesEntry = directory.files.find( + (file) => file.path === "routes.txt", + ); + const routeStopsEntry = directory.files.find( + (file) => file.path === "route_stops.txt", + ); + + if (!routesEntry) { + throw new Error("routes.txt not found in GTFS archive"); + } + + if (!routeStopsEntry) { + throw new Error("route_stops.txt not found in GTFS archive"); + } + + const rawRoutes = await parseCsvString>( + (await routesEntry.buffer()).toString(), + ); + const rawRouteStops = await parseCsvString>( + (await routeStopsEntry.buffer()).toString(), + ); + + return { + gtfsRoutes: rawRoutes.map((route) => + this.parseGtfsRouteRecord(route), + ), + gtfsRouteStops: rawRouteStops + .map((routeStop) => this.parseGtfsRouteStopRecord(routeStop)) + .filter((routeStop) => platformIds.has(routeStop.platformId)), + }; + } + + private parseGtfsRouteRecord(route: Record) { + const parsed = gtfsRouteRecordSchema.safeParse(route); + + if (!parsed.success) { + throw new Error( + `Invalid GTFS route record: ${parsed.error.message}`, + ); + } + + return { + id: parsed.data.route_id, + shortName: parsed.data.route_short_name, + longName: this.toOptionalString(parsed.data.route_long_name), + type: parsed.data.route_type, + color: this.toOptionalString(parsed.data.route_color), + isNight: this.parseNightFlag(parsed.data.is_night), + url: this.toOptionalString(parsed.data.route_url), + }; + } + + private parseGtfsRouteStopRecord(routeStop: Record) { + const parsed = gtfsRouteStopRecordSchema.safeParse(routeStop); + + if (!parsed.success) { + throw new Error( + `Invalid GTFS route stop record: ${parsed.error.message}`, + ); + } + + const stopSequence = Number(parsed.data.stop_sequence); + + if (!Number.isInteger(stopSequence)) { + throw new Error( + `Invalid GTFS stop sequence: ${parsed.data.stop_sequence}`, + ); + } + + return { + routeId: parsed.data.route_id, + directionId: parsed.data.direction_id, + platformId: this.normalizePlatformId(parsed.data.stop_id), + stopSequence, + }; + } + + private normalizePlatformId(stopId: string): string { + return stopId.split("_")[0]; + } + + private parseNightFlag(value?: string): boolean | null { + if (value === undefined || value.trim() === "") { + return null; + } + + if (value === "1") { + return true; + } + + if (value === "0") { + return false; + } + + throw new Error(`Unexpected GTFS is_night flag: ${value}`); + } + + private toOptionalString(value?: string): string | null { + if (value === undefined) { + return null; + } + + const trimmed = value.trim(); + + return trimmed.length > 0 ? trimmed : null; + } +} diff --git a/apps/dataloader/src/services/pid-import.service.ts b/apps/dataloader/src/services/pid-import.service.ts new file mode 100644 index 00000000..67492074 --- /dev/null +++ b/apps/dataloader/src/services/pid-import.service.ts @@ -0,0 +1,145 @@ +import { VehicleType } from "@metro-now/database"; + +import { + type PidStopsSchema, + pidStopsSchema, +} from "../schema/pid-stops.schema"; +import type { StopSnapshot, SyncedRoute } from "../types/sync.types"; +import { fetchWithTimeout } from "../utils/fetch.utils"; + +const PID_STOPS_URL = "https://data.pid.cz/stops/json/stops.json"; + +export class PidImportService { + async getStopSnapshot(): Promise { + return this.buildStopSnapshot(await this.getStops()); + } + + async getStops(): Promise { + const response = await fetchWithTimeout(PID_STOPS_URL, { + method: "GET", + }); + + if (!response.ok) { + throw new Error( + `Failed to fetch stops data: ${response.status} ${response.statusText}`, + ); + } + + const parsed = pidStopsSchema.safeParse(await response.json()); + + if (!parsed.success) { + throw new Error( + `Couldn't parse data from '${PID_STOPS_URL}': ${parsed.error.message}`, + ); + } + + return parsed.data; + } + + buildStopSnapshot(stopsData: PidStopsSchema): StopSnapshot { + const stops = new Map< + string, + { + id: string; + name: string; + avgLatitude: number; + avgLongitude: number; + } + >(); + for (const stopGroup of stopsData.stopGroups) { + stops.set(`U${stopGroup.node}`, { + id: `U${stopGroup.node}`, + name: stopGroup.name, + avgLatitude: stopGroup.avgLat, + avgLongitude: stopGroup.avgLon, + }); + } + + const validStopIds = new Set(stops.keys()); + const platforms = new Map< + string, + { + id: string; + name: string; + code: string | null; + isMetro: boolean; + latitude: number; + longitude: number; + stopId: string | null; + } + >(); + const routes = new Map(); + const platformRoutes = new Map< + string, + { + platformId: string; + routeId: string; + } + >(); + + for (const stopGroup of stopsData.stopGroups) { + for (const platform of stopGroup.stops) { + const platformId = platform.gtfsIds[0]?.trim(); + const platformName = platform.altIdosName?.trim(); + + if (!platformId || !platformName) { + continue; + } + + platforms.set(platformId, { + id: platformId, + name: platformName, + code: platform.platform ?? null, + isMetro: platform.isMetro === true, + latitude: platform.lat, + longitude: platform.lon, + stopId: this.resolveStopId(platformId, validStopIds), + }); + + for (const line of platform.lines) { + const routeId = String(line.id).trim(); + const routeName = line.name.trim(); + + if (!routeId || !routeName) { + continue; + } + + routes.set(routeId, { + id: routeId, + name: routeName, + vehicleType: + VehicleType[ + line.type.toUpperCase() as keyof typeof VehicleType + ] ?? null, + isNight: null, + }); + platformRoutes.set(`${platformId}::${routeId}`, { + platformId, + routeId, + }); + } + } + } + + return { + stops: Array.from(stops.values()), + platforms: Array.from(platforms.values()), + routes: Array.from(routes.values()), + platformRoutes: Array.from(platformRoutes.values()), + }; + } + + private resolveStopId( + platformId: string, + validStopIds: Set, + ): string | null { + const normalizedPlatformId = platformId.split("_")[0]; + const candidateStopId = normalizedPlatformId.split("Z")[0]; + + if (validStopIds.has(candidateStopId)) { + return candidateStopId; + } + + return null; + } +} diff --git a/apps/dataloader/src/services/sync-repository.service.ts b/apps/dataloader/src/services/sync-repository.service.ts new file mode 100644 index 00000000..b0cd86d7 --- /dev/null +++ b/apps/dataloader/src/services/sync-repository.service.ts @@ -0,0 +1,628 @@ +import { randomUUID } from "node:crypto"; +import { + type DatabaseClient, + type DatabaseTransaction, + type NewGtfsRoute, + type NewGtfsRouteStop, + type NewPlatform, + type NewPlatformsOnRoutes, + type NewRoute, + type NewStop, + sql, +} from "@metro-now/database"; + +import type { + SyncPersistenceResult, + SyncSnapshot, + SyncedGtfsRoute, + SyncedGtfsRouteStop, + SyncedPlatform, + SyncedPlatformRoute, + SyncedRoute, + SyncedStop, +} from "../types/sync.types"; +import { getSyncCounts } from "../types/sync.types"; +import { logger } from "../utils/logger"; + +const LOCK_KEY = BigInt(4_241_001); +const ENTITY_BATCH_SIZE = 100; +const RELATION_BATCH_SIZE = 500; + +type IdTableName = "GtfsRoute" | "Platform" | "Route" | "Stop"; + +export class SyncRepository { + constructor(private readonly db: DatabaseClient) {} + + async persist(snapshot: SyncSnapshot): Promise { + return this.db.transaction().execute(async (transaction) => { + await this.configureTransaction(transaction); + + const lockAcquired = + await this.tryAcquireTransactionLock(transaction); + + if (!lockAcquired) { + return { + status: "skipped", + reason: "A different dataloader instance is already syncing", + }; + } + + await this.persistSnapshot(transaction, snapshot); + + return { + status: "success", + counts: getSyncCounts(snapshot), + }; + }); + } + + private async configureTransaction( + transaction: DatabaseTransaction, + ): Promise { + await sql`SET LOCAL lock_timeout = '10s'`.execute(transaction); + await sql`SET LOCAL statement_timeout = '20min'`.execute(transaction); + } + + private async persistSnapshot( + transaction: DatabaseTransaction, + snapshot: SyncSnapshot, + ): Promise { + await this.upsertStops(transaction, snapshot.stops); + await this.upsertRoutes(transaction, snapshot.routes); + await this.upsertPlatforms(transaction, snapshot.platforms); + await this.upsertGtfsRoutes(transaction, snapshot.gtfsRoutes); + await this.syncPlatformRoutes(transaction, snapshot.platformRoutes); + await this.syncGtfsRouteStops(transaction, snapshot.gtfsRouteStops); + await this.deleteStalePlatforms(transaction, snapshot.platforms); + await this.deleteStaleRoutes(transaction, snapshot.routes); + await this.deleteStaleStops(transaction, snapshot.stops); + await this.deleteStaleGtfsRoutes(transaction, snapshot.gtfsRoutes); + } + + private async upsertStops( + transaction: DatabaseTransaction, + stops: SyncedStop[], + ): Promise { + await this.processInBatches(stops, ENTITY_BATCH_SIZE, async (chunk) => { + const timestamp = new Date(); + const values: NewStop[] = chunk.map((stop) => ({ + id: stop.id, + name: stop.name, + avgLatitude: stop.avgLatitude, + avgLongitude: stop.avgLongitude, + createdAt: timestamp, + updatedAt: timestamp, + })); + + await transaction + .insertInto("Stop") + .values(values) + .onConflict((conflict) => + conflict.column("id").doUpdateSet((expressionBuilder) => ({ + name: expressionBuilder.ref("excluded.name"), + avgLatitude: expressionBuilder.ref( + "excluded.avgLatitude", + ), + avgLongitude: expressionBuilder.ref( + "excluded.avgLongitude", + ), + updatedAt: sql`now()`, + })), + ) + .execute(); + }); + } + + private async upsertRoutes( + transaction: DatabaseTransaction, + routes: SyncedRoute[], + ): Promise { + await this.processInBatches( + routes, + ENTITY_BATCH_SIZE, + async (chunk) => { + const timestamp = new Date(); + const values: NewRoute[] = chunk.map((route) => ({ + id: route.id, + name: route.name, + vehicleType: route.vehicleType, + isNight: route.isNight, + createdAt: timestamp, + updatedAt: timestamp, + })); + + await transaction + .insertInto("Route") + .values(values) + .onConflict((conflict) => + conflict + .column("id") + .doUpdateSet((expressionBuilder) => ({ + name: expressionBuilder.ref("excluded.name"), + vehicleType: expressionBuilder.ref( + "excluded.vehicleType", + ), + isNight: + expressionBuilder.ref("excluded.isNight"), + updatedAt: sql`now()`, + })), + ) + .execute(); + }, + ); + } + + private async upsertPlatforms( + transaction: DatabaseTransaction, + platforms: SyncedPlatform[], + ): Promise { + await this.processInBatches( + platforms, + ENTITY_BATCH_SIZE, + async (chunk) => { + const timestamp = new Date(); + const values: NewPlatform[] = chunk.map((platform) => ({ + id: platform.id, + name: platform.name, + code: platform.code, + isMetro: platform.isMetro, + latitude: platform.latitude, + longitude: platform.longitude, + stopId: platform.stopId, + createdAt: timestamp, + updatedAt: timestamp, + })); + + await transaction + .insertInto("Platform") + .values(values) + .onConflict((conflict) => + conflict + .column("id") + .doUpdateSet((expressionBuilder) => ({ + name: expressionBuilder.ref("excluded.name"), + code: expressionBuilder.ref("excluded.code"), + isMetro: + expressionBuilder.ref("excluded.isMetro"), + latitude: + expressionBuilder.ref("excluded.latitude"), + longitude: + expressionBuilder.ref("excluded.longitude"), + stopId: expressionBuilder.ref( + "excluded.stopId", + ), + updatedAt: sql`now()`, + })), + ) + .execute(); + }, + ); + } + + private async upsertGtfsRoutes( + transaction: DatabaseTransaction, + gtfsRoutes: SyncedGtfsRoute[], + ): Promise { + await this.processInBatches( + gtfsRoutes, + ENTITY_BATCH_SIZE, + async (chunk) => { + const timestamp = new Date(); + const values: NewGtfsRoute[] = chunk.map((gtfsRoute) => ({ + id: gtfsRoute.id, + shortName: gtfsRoute.shortName, + longName: gtfsRoute.longName, + type: gtfsRoute.type, + color: gtfsRoute.color, + isNight: gtfsRoute.isNight, + url: gtfsRoute.url, + createdAt: timestamp, + updatedAt: timestamp, + })); + + await transaction + .insertInto("GtfsRoute") + .values(values) + .onConflict((conflict) => + conflict + .column("id") + .doUpdateSet((expressionBuilder) => ({ + shortName: + expressionBuilder.ref("excluded.shortName"), + longName: + expressionBuilder.ref("excluded.longName"), + type: expressionBuilder.ref("excluded.type"), + color: expressionBuilder.ref("excluded.color"), + isNight: + expressionBuilder.ref("excluded.isNight"), + url: expressionBuilder.ref("excluded.url"), + updatedAt: sql`now()`, + })), + ) + .execute(); + }, + ); + } + + private async syncPlatformRoutes( + transaction: DatabaseTransaction, + platformRoutes: SyncedPlatformRoute[], + ): Promise { + const existingRelations = await transaction + .selectFrom("PlatformsOnRoutes") + .select(["platformId", "routeId"]) + .execute(); + const incomingKeys = new Set( + platformRoutes.map((relation) => + this.getPlatformRouteKey(relation), + ), + ); + const existingKeys = new Set( + existingRelations.map((relation) => + this.getPlatformRouteKey(relation), + ), + ); + const relationsToCreate = platformRoutes.filter( + (relation) => !existingKeys.has(this.getPlatformRouteKey(relation)), + ); + const relationsToDelete = existingRelations.filter( + (relation) => !incomingKeys.has(this.getPlatformRouteKey(relation)), + ); + + await this.processInBatches( + relationsToCreate, + RELATION_BATCH_SIZE, + async (chunk) => { + const timestamp = new Date(); + const values: NewPlatformsOnRoutes[] = chunk.map( + (relation) => ({ + id: randomUUID(), + platformId: relation.platformId, + routeId: relation.routeId, + createdAt: timestamp, + updatedAt: timestamp, + }), + ); + + await transaction + .insertInto("PlatformsOnRoutes") + .values(values) + .onConflict((conflict) => + conflict.columns(["platformId", "routeId"]).doNothing(), + ) + .execute(); + }, + ); + await this.processInBatches( + relationsToDelete, + RELATION_BATCH_SIZE, + async (chunk) => { + if (chunk.length === 0) { + return; + } + + await transaction + .deleteFrom("PlatformsOnRoutes") + .where((expressionBuilder) => + expressionBuilder.or( + chunk.map((relation) => + expressionBuilder.and([ + expressionBuilder( + "platformId", + "=", + relation.platformId, + ), + expressionBuilder( + "routeId", + "=", + relation.routeId, + ), + ]), + ), + ), + ) + .execute(); + }, + ); + } + + private async syncGtfsRouteStops( + transaction: DatabaseTransaction, + gtfsRouteStops: SyncedGtfsRouteStop[], + ): Promise { + const existingRouteStops = await transaction + .selectFrom("GtfsRouteStop") + .select(["routeId", "directionId", "stopId", "stopSequence"]) + .execute(); + const incomingKeys = new Set( + gtfsRouteStops.map((routeStop) => + this.getGtfsRouteStopKey(routeStop), + ), + ); + const existingKeys = new Set( + existingRouteStops.map((routeStop) => + this.getGtfsRouteStopKey({ + routeId: routeStop.routeId, + directionId: routeStop.directionId, + platformId: routeStop.stopId, + stopSequence: routeStop.stopSequence, + }), + ), + ); + const routeStopsToCreate = gtfsRouteStops.filter( + (routeStop) => + !existingKeys.has(this.getGtfsRouteStopKey(routeStop)), + ); + const routeStopsToDelete = existingRouteStops.filter( + (routeStop) => + !incomingKeys.has( + this.getGtfsRouteStopKey({ + routeId: routeStop.routeId, + directionId: routeStop.directionId, + platformId: routeStop.stopId, + stopSequence: routeStop.stopSequence, + }), + ), + ); + + await this.processInBatches( + routeStopsToCreate, + RELATION_BATCH_SIZE, + async (chunk) => { + const timestamp = new Date(); + const values: NewGtfsRouteStop[] = chunk.map((routeStop) => ({ + id: randomUUID(), + routeId: routeStop.routeId, + directionId: routeStop.directionId, + stopId: routeStop.platformId, + stopSequence: routeStop.stopSequence, + createdAt: timestamp, + updatedAt: timestamp, + })); + + await transaction + .insertInto("GtfsRouteStop") + .values(values) + .onConflict((conflict) => + conflict + .columns([ + "routeId", + "directionId", + "stopId", + "stopSequence", + ]) + .doNothing(), + ) + .execute(); + }, + ); + await this.processInBatches( + routeStopsToDelete, + RELATION_BATCH_SIZE, + async (chunk) => { + if (chunk.length === 0) { + return; + } + + await transaction + .deleteFrom("GtfsRouteStop") + .where((expressionBuilder) => + expressionBuilder.or( + chunk.map((routeStop) => + expressionBuilder.and([ + expressionBuilder( + "routeId", + "=", + routeStop.routeId, + ), + expressionBuilder( + "directionId", + "=", + routeStop.directionId, + ), + expressionBuilder( + "stopId", + "=", + routeStop.stopId, + ), + expressionBuilder( + "stopSequence", + "=", + routeStop.stopSequence, + ), + ]), + ), + ), + ) + .execute(); + }, + ); + } + + private async deleteStalePlatforms( + transaction: DatabaseTransaction, + platforms: SyncedPlatform[], + ): Promise { + const incomingIds = new Set(platforms.map((platform) => platform.id)); + const staleIds = (await this.selectIds(transaction, "Platform")).filter( + (id) => !incomingIds.has(id), + ); + const deletableIds = + await this.excludePlatformIdsReferencedByGtfsStopTimes( + transaction, + staleIds, + ); + + if (deletableIds.length !== staleIds.length) { + logger.warn( + "Skipping stale platform deletes protected by GTFS stop times", + { + stalePlatformCount: staleIds.length, + blockedPlatformCount: staleIds.length - deletableIds.length, + sampleBlockedPlatformIds: staleIds + .filter((id) => !deletableIds.includes(id)) + .slice(0, 10), + }, + ); + } + + await this.deleteByIds(transaction, "Platform", deletableIds); + } + + private async deleteStaleRoutes( + transaction: DatabaseTransaction, + routes: SyncedRoute[], + ): Promise { + const incomingIds = new Set(routes.map((route) => route.id)); + const staleIds = (await this.selectIds(transaction, "Route")).filter( + (id) => !incomingIds.has(id), + ); + + await this.deleteByIds(transaction, "Route", staleIds); + } + + private async deleteStaleStops( + transaction: DatabaseTransaction, + stops: SyncedStop[], + ): Promise { + const incomingIds = new Set(stops.map((stop) => stop.id)); + const staleIds = (await this.selectIds(transaction, "Stop")).filter( + (id) => !incomingIds.has(id), + ); + + await this.deleteByIds(transaction, "Stop", staleIds); + } + + private async deleteStaleGtfsRoutes( + transaction: DatabaseTransaction, + gtfsRoutes: SyncedGtfsRoute[], + ): Promise { + const incomingIds = new Set(gtfsRoutes.map((route) => route.id)); + const staleIds = ( + await this.selectIds(transaction, "GtfsRoute") + ).filter((id) => !incomingIds.has(id)); + + await this.deleteByIds(transaction, "GtfsRoute", staleIds); + } + + private async selectIds( + transaction: DatabaseTransaction, + tableName: IdTableName, + ): Promise { + const rows = await transaction + .selectFrom(tableName) + .select("id") + .orderBy("id", "asc") + .execute(); + + return rows.map(({ id }) => id); + } + + private async deleteByIds( + transaction: DatabaseTransaction, + tableName: IdTableName, + ids: string[], + ): Promise { + await this.processInBatches(ids, RELATION_BATCH_SIZE, async (chunk) => { + if (chunk.length === 0) { + return; + } + + await transaction + .deleteFrom(tableName) + .where("id", "in", chunk) + .execute(); + }); + } + + private async tryAcquireTransactionLock( + transaction: DatabaseTransaction, + ): Promise { + const result = await sql<{ acquired: boolean }>` + SELECT pg_try_advisory_xact_lock(${LOCK_KEY}) AS acquired + `.execute(transaction); + + return result.rows[0]?.acquired ?? false; + } + + private async excludePlatformIdsReferencedByGtfsStopTimes( + transaction: DatabaseTransaction, + platformIds: string[], + ): Promise { + if (platformIds.length === 0) { + return platformIds; + } + + const gtfsStopTimeTableExists = await this.hasTable( + transaction, + "GtfsStopTime", + ); + + if (!gtfsStopTimeTableExists) { + return platformIds; + } + + const protectedPlatformIds = new Set(); + + await this.processInBatches( + platformIds, + RELATION_BATCH_SIZE, + async (chunk) => { + const rows = await transaction + .selectFrom("GtfsStopTime") + .select("platformId") + .distinct() + .where("platformId", "is not", null) + .where("platformId", "in", chunk) + .execute(); + + for (const { platformId } of rows) { + if (platformId) { + protectedPlatformIds.add(platformId); + } + } + }, + ); + + return platformIds.filter((id) => !protectedPlatformIds.has(id)); + } + + private async hasTable( + transaction: DatabaseTransaction, + tableName: string, + ): Promise { + const result = await sql<{ exists: boolean }>` + SELECT EXISTS ( + SELECT 1 + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = ${tableName} + ) AS exists + `.execute(transaction); + + return Boolean(result.rows[0]?.exists); + } + + private getPlatformRouteKey(relation: SyncedPlatformRoute): string { + return `${relation.platformId}::${relation.routeId}`; + } + + private getGtfsRouteStopKey(routeStop: SyncedGtfsRouteStop): string { + return [ + routeStop.routeId, + routeStop.directionId, + routeStop.platformId, + routeStop.stopSequence, + ].join("::"); + } + + private async processInBatches( + items: T[], + batchSize: number, + callback: (chunk: T[]) => Promise, + ): Promise { + for (let index = 0; index < items.length; index += batchSize) { + await callback(items.slice(index, index + batchSize)); + } + } +} diff --git a/apps/dataloader/src/services/sync-snapshot-validator.service.ts b/apps/dataloader/src/services/sync-snapshot-validator.service.ts new file mode 100644 index 00000000..d81a1136 --- /dev/null +++ b/apps/dataloader/src/services/sync-snapshot-validator.service.ts @@ -0,0 +1,117 @@ +import type { SyncSnapshot } from "../types/sync.types"; + +export class SyncSnapshotValidator { + validate(snapshot: SyncSnapshot): void { + this.assertNotEmpty("stops", snapshot.stops); + this.assertNotEmpty("platforms", snapshot.platforms); + this.assertNotEmpty("routes", snapshot.routes); + this.assertNotEmpty("platformRoutes", snapshot.platformRoutes); + this.assertNotEmpty("gtfsRoutes", snapshot.gtfsRoutes); + this.assertNotEmpty("gtfsRouteStops", snapshot.gtfsRouteStops); + + this.assertUniqueKeys( + "stops", + snapshot.stops.map((stop) => stop.id), + ); + this.assertUniqueKeys( + "platforms", + snapshot.platforms.map((platform) => platform.id), + ); + this.assertUniqueKeys( + "routes", + snapshot.routes.map((route) => route.id), + ); + this.assertUniqueKeys( + "platformRoutes", + snapshot.platformRoutes.map( + (relation) => `${relation.platformId}::${relation.routeId}`, + ), + ); + this.assertUniqueKeys( + "gtfsRoutes", + snapshot.gtfsRoutes.map((route) => route.id), + ); + this.assertUniqueKeys( + "gtfsRouteStops", + snapshot.gtfsRouteStops.map((routeStop) => + [ + routeStop.routeId, + routeStop.directionId, + routeStop.platformId, + routeStop.stopSequence, + ].join("::"), + ), + ); + + const stopIds = new Set(snapshot.stops.map((stop) => stop.id)); + const platformIds = new Set( + snapshot.platforms.map((platform) => platform.id), + ); + const routeIds = new Set(snapshot.routes.map((route) => route.id)); + const gtfsRouteIds = new Set( + snapshot.gtfsRoutes.map((route) => route.id), + ); + + for (const platform of snapshot.platforms) { + if (platform.stopId && !stopIds.has(platform.stopId)) { + throw new Error( + `Platform '${platform.id}' references missing stop '${platform.stopId}'`, + ); + } + } + + for (const relation of snapshot.platformRoutes) { + if (!platformIds.has(relation.platformId)) { + throw new Error( + `Platform route references missing platform '${relation.platformId}'`, + ); + } + + if (!routeIds.has(relation.routeId)) { + throw new Error( + `Platform route references missing route '${relation.routeId}'`, + ); + } + } + + for (const routeStop of snapshot.gtfsRouteStops) { + if (!gtfsRouteIds.has(routeStop.routeId)) { + throw new Error( + `GTFS route stop references missing route '${routeStop.routeId}'`, + ); + } + + if (!platformIds.has(routeStop.platformId)) { + throw new Error( + `GTFS route stop references missing platform '${routeStop.platformId}'`, + ); + } + + if (routeStop.stopSequence < 0) { + throw new Error( + `GTFS route stop has invalid stop sequence '${routeStop.stopSequence}'`, + ); + } + } + } + + private assertNotEmpty(name: string, items: unknown[]): void { + if (items.length === 0) { + throw new Error(`Refusing to sync empty ${name} dataset`); + } + } + + private assertUniqueKeys(name: string, keys: string[]): void { + const seen = new Set(); + + for (const key of keys) { + if (seen.has(key)) { + throw new Error( + `Duplicate ${name} record detected for '${key}'`, + ); + } + + seen.add(key); + } + } +} diff --git a/apps/dataloader/src/services/sync-snapshot-validator.spec.ts b/apps/dataloader/src/services/sync-snapshot-validator.spec.ts new file mode 100644 index 00000000..1541d86f --- /dev/null +++ b/apps/dataloader/src/services/sync-snapshot-validator.spec.ts @@ -0,0 +1,81 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { SyncSnapshotValidator } from "./sync-snapshot-validator.service"; + +const createSnapshot = () => ({ + stops: [ + { + id: "U1", + name: "Stop 1", + avgLatitude: 50.1, + avgLongitude: 14.4, + }, + ], + platforms: [ + { + id: "P1", + name: "Platform 1", + code: "1", + isMetro: true, + latitude: 50.1, + longitude: 14.4, + stopId: "U1", + }, + ], + routes: [ + { + id: "R1", + name: "Route 1", + vehicleType: null, + isNight: false, + }, + ], + platformRoutes: [ + { + platformId: "P1", + routeId: "R1", + }, + ], + gtfsRoutes: [ + { + id: "L1", + shortName: "C", + longName: "Line C", + type: "metro", + color: "#ff0000", + isNight: false, + url: null, + }, + ], + gtfsRouteStops: [ + { + routeId: "L1", + directionId: "0", + platformId: "P1", + stopSequence: 1, + }, + ], +}); + +test("SyncSnapshotValidator accepts a consistent snapshot", () => { + const validator = new SyncSnapshotValidator(); + + assert.doesNotThrow(() => { + validator.validate(createSnapshot()); + }); +}); + +test("SyncSnapshotValidator rejects missing platform references", () => { + const validator = new SyncSnapshotValidator(); + const snapshot = createSnapshot(); + + snapshot.gtfsRouteStops[0] = { + ...snapshot.gtfsRouteStops[0], + platformId: "missing-platform", + }; + + assert.throws(() => { + validator.validate(snapshot); + }, /missing platform 'missing-platform'/); +}); diff --git a/apps/dataloader/src/services/sync.service.ts b/apps/dataloader/src/services/sync.service.ts new file mode 100644 index 00000000..2e77837e --- /dev/null +++ b/apps/dataloader/src/services/sync.service.ts @@ -0,0 +1,114 @@ +import type { DatabaseClient } from "@metro-now/database"; + +import type { + SyncRunResult, + SyncSnapshot, + SyncTrigger, +} from "../types/sync.types"; +import { logger } from "../utils/logger"; +import { GtfsService } from "./gtfs.service"; +import { PidImportService } from "./pid-import.service"; +import { SyncRepository } from "./sync-repository.service"; +import { SyncSnapshotValidator } from "./sync-snapshot-validator.service"; + +export class SyncService { + private readonly importService = new PidImportService(); + private readonly gtfsService = new GtfsService(); + private readonly validator = new SyncSnapshotValidator(); + private readonly repository: SyncRepository; + private activeSync: Promise | undefined; + private lastRun: SyncRunResult | undefined; + + constructor(db: DatabaseClient) { + this.repository = new SyncRepository(db); + } + + async syncEverything(trigger: SyncTrigger): Promise { + if (this.activeSync) { + logger.warn("Sync already running, reusing active run", { + trigger, + }); + + return this.activeSync; + } + + const run = this.executeSync(trigger).finally(() => { + if (this.activeSync === run) { + this.activeSync = undefined; + } + }); + + this.activeSync = run; + + return run; + } + + getStatus(): { + running: boolean; + lastRun?: SyncRunResult; + } { + return { + running: this.activeSync !== undefined, + lastRun: this.lastRun, + }; + } + + private async executeSync(trigger: SyncTrigger): Promise { + const startedAt = new Date(); + + logger.info(`Starting ${trigger} sync`); + + const snapshot = await this.createSnapshot(); + + this.validator.validate(snapshot); + + const persistenceResult = await this.repository.persist(snapshot); + const finishedAt = new Date(); + const result: SyncRunResult = + persistenceResult.status === "success" + ? { + status: "success", + trigger, + startedAt, + finishedAt, + durationMs: finishedAt.getTime() - startedAt.getTime(), + counts: persistenceResult.counts, + } + : { + status: "skipped", + trigger, + startedAt, + finishedAt, + durationMs: finishedAt.getTime() - startedAt.getTime(), + reason: persistenceResult.reason, + }; + + this.lastRun = result; + + if (result.status === "success") { + logger.info(`Finished ${trigger} sync`, { + durationMs: result.durationMs, + counts: result.counts, + }); + } else { + logger.warn(`Skipped ${trigger} sync`, { + durationMs: result.durationMs, + reason: result.reason, + }); + } + + return result; + } + + private async createSnapshot(): Promise { + const stopSnapshot = await this.importService.getStopSnapshot(); + const gtfsSnapshot = await this.gtfsService.getGtfsSnapshot( + new Set(stopSnapshot.platforms.map((platform) => platform.id)), + ); + + return { + ...stopSnapshot, + ...gtfsSnapshot, + }; + } +} diff --git a/apps/dataloader/src/types/sync.types.ts b/apps/dataloader/src/types/sync.types.ts new file mode 100644 index 00000000..7c987a41 --- /dev/null +++ b/apps/dataloader/src/types/sync.types.ts @@ -0,0 +1,101 @@ +import { VehicleType } from "@metro-now/database"; + +export type SyncTrigger = "startup" | "cron" | "manual"; + +export type SyncedStop = { + id: string; + name: string; + avgLatitude: number; + avgLongitude: number; +}; + +export type SyncedPlatform = { + id: string; + name: string; + code: string | null; + isMetro: boolean; + latitude: number; + longitude: number; + stopId: string | null; +}; + +export type SyncedRoute = { + id: string; + name: string; + vehicleType: VehicleType | null; + isNight: boolean | null; +}; + +export type SyncedPlatformRoute = { + platformId: string; + routeId: string; +}; + +export type SyncedGtfsRoute = { + id: string; + shortName: string; + longName: string | null; + type: string; + color: string | null; + isNight: boolean | null; + url: string | null; +}; + +export type SyncedGtfsRouteStop = { + routeId: string; + directionId: string; + platformId: string; + stopSequence: number; +}; + +export type StopSnapshot = { + stops: SyncedStop[]; + platforms: SyncedPlatform[]; + routes: SyncedRoute[]; + platformRoutes: SyncedPlatformRoute[]; +}; + +export type GtfsSnapshot = { + gtfsRoutes: SyncedGtfsRoute[]; + gtfsRouteStops: SyncedGtfsRouteStop[]; +}; + +export type SyncSnapshot = StopSnapshot & GtfsSnapshot; + +export type SyncCounts = { + stops: number; + platforms: number; + routes: number; + platformRoutes: number; + gtfsRoutes: number; + gtfsRouteStops: number; +}; + +export type SyncRunResult = { + status: "success" | "skipped"; + trigger: SyncTrigger; + startedAt: Date; + finishedAt: Date; + durationMs: number; + counts?: SyncCounts; + reason?: string; +}; + +export type SyncPersistenceResult = + | { + status: "success"; + counts: SyncCounts; + } + | { + status: "skipped"; + reason: string; + }; + +export const getSyncCounts = (snapshot: SyncSnapshot): SyncCounts => ({ + stops: snapshot.stops.length, + platforms: snapshot.platforms.length, + routes: snapshot.routes.length, + platformRoutes: snapshot.platformRoutes.length, + gtfsRoutes: snapshot.gtfsRoutes.length, + gtfsRouteStops: snapshot.gtfsRouteStops.length, +}); diff --git a/apps/backend/src/utils/csv.utils.ts b/apps/dataloader/src/utils/csv.utils.ts similarity index 60% rename from apps/backend/src/utils/csv.utils.ts rename to apps/dataloader/src/utils/csv.utils.ts index 22c29c8d..af7fb68b 100644 --- a/apps/backend/src/utils/csv.utils.ts +++ b/apps/dataloader/src/utils/csv.utils.ts @@ -1,14 +1,12 @@ import { parseString } from "@fast-csv/parse"; export async function parseCsvString(csvString: string): Promise { - return new Promise((resolve) => { + return await new Promise((resolve, reject) => { const rows: T[] = []; parseString(csvString, { headers: true }) - .on("error", (error) => console.error(error)) + .on("error", (error) => reject(error)) .on("data", (row) => rows.push(row)) - .on("end", () => { - resolve(rows); - }); + .on("end", () => resolve(rows)); }); } diff --git a/apps/dataloader/src/utils/fetch.utils.ts b/apps/dataloader/src/utils/fetch.utils.ts new file mode 100644 index 00000000..71004675 --- /dev/null +++ b/apps/dataloader/src/utils/fetch.utils.ts @@ -0,0 +1,24 @@ +const DEFAULT_TIMEOUT_MS = 30_000; + +const formatFetchError = (url: URL | string, error: unknown): string => { + if (error instanceof Error) { + return `Failed to fetch '${url.toString()}': ${error.message}`; + } + + return `Failed to fetch '${url.toString()}': ${String(error)}`; +}; + +export const fetchWithTimeout = async ( + url: URL | string, + init: RequestInit = {}, + timeoutMs = DEFAULT_TIMEOUT_MS, +): Promise => { + try { + return await fetch(url, { + ...init, + signal: AbortSignal.timeout(timeoutMs), + }); + } catch (error) { + throw new Error(formatFetchError(url, error)); + } +}; diff --git a/apps/dataloader/src/utils/logger.spec.ts b/apps/dataloader/src/utils/logger.spec.ts new file mode 100644 index 00000000..b6997084 --- /dev/null +++ b/apps/dataloader/src/utils/logger.spec.ts @@ -0,0 +1,29 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import type { LogEntry, LogTransport } from "./logger"; +import { logger } from "./logger"; + +class TestTransport implements LogTransport { + readonly entries: LogEntry[] = []; + + async write(entry: LogEntry): Promise { + this.entries.push(entry); + } + + async flush(): Promise {} +} + +test("logger writes entries to the configured transport", async () => { + const transport = new TestTransport(); + + logger.setTransport(transport); + logger.info("hello", { foo: "bar" }); + await logger.flush(); + logger.setTransport(null); + + assert.equal(transport.entries.length, 1); + assert.equal(transport.entries[0]?.level, "info"); + assert.equal(transport.entries[0]?.message, "hello"); + assert.deepEqual(transport.entries[0]?.context, { foo: "bar" }); +}); diff --git a/apps/dataloader/src/utils/logger.ts b/apps/dataloader/src/utils/logger.ts new file mode 100644 index 00000000..1d7578a8 --- /dev/null +++ b/apps/dataloader/src/utils/logger.ts @@ -0,0 +1,69 @@ +export type LogLevel = "info" | "warn" | "error"; +export type LogContext = Record; + +export type LogEntry = { + createdAt: Date; + level: LogLevel; + message: string; + context: LogContext | null; +}; + +export type LogTransport = { + write(entry: LogEntry): Promise; + flush?(): Promise; +}; + +let logTransport: LogTransport | null = null; + +const logTransportFailure = (error: unknown): void => { + const message = error instanceof Error ? error.message : String(error); + + console.error( + `[${new Date().toISOString()}] [ERROR] Failed to persist dataloader log`, + { + error: message, + }, + ); +}; + +const writeLog = ( + level: LogLevel, + message: string, + context?: LogContext, +): void => { + const entry: LogEntry = { + createdAt: new Date(), + level, + message, + context: context ?? null, + }; + const prefix = `[${entry.createdAt.toISOString()}] [${level.toUpperCase()}]`; + + if (context) { + console[level](prefix, message, context); + } else { + console[level](prefix, message); + } + + if (logTransport) { + void logTransport.write(entry).catch(logTransportFailure); + } +}; + +export const logger = { + info(message: string, context?: LogContext) { + writeLog("info", message, context); + }, + warn(message: string, context?: LogContext) { + writeLog("warn", message, context); + }, + error(message: string, context?: LogContext) { + writeLog("error", message, context); + }, + setTransport(transport: LogTransport | null) { + logTransport = transport; + }, + async flush(): Promise { + await logTransport?.flush?.(); + }, +}; diff --git a/apps/dataloader/tsconfig.json b/apps/dataloader/tsconfig.json new file mode 100644 index 00000000..9610b07a --- /dev/null +++ b/apps/dataloader/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/apps/mobile/metro-now/metro-now Watch App/pages/closest-stop-list/ClosestStopListViewModel.swift b/apps/mobile/metro-now/metro-now Watch App/pages/closest-stop-list/ClosestStopListViewModel.swift index cb8d508f..c870586c 100644 --- a/apps/mobile/metro-now/metro-now Watch App/pages/closest-stop-list/ClosestStopListViewModel.swift +++ b/apps/mobile/metro-now/metro-now Watch App/pages/closest-stop-list/ClosestStopListViewModel.swift @@ -37,7 +37,6 @@ class ClosestStopListViewModel: NSObject, ObservableObject, CLLocationManagerDel withTimeInterval: REFETCH_INTERVAL, repeats: true ) { [weak self] _ in - guard let self, let closestStop else { return } diff --git a/apps/mobile/metro-now/metro-now Watch App/pages/platform/PlatformDetailDeparturesListView.swift b/apps/mobile/metro-now/metro-now Watch App/pages/platform/PlatformDetailDeparturesListView.swift index dea17afb..fbbd959b 100644 --- a/apps/mobile/metro-now/metro-now Watch App/pages/platform/PlatformDetailDeparturesListView.swift +++ b/apps/mobile/metro-now/metro-now Watch App/pages/platform/PlatformDetailDeparturesListView.swift @@ -6,11 +6,13 @@ import SwiftUI struct PlatformDetailDeparturesListView: View { let departures: [ApiDeparture] - var body: some View { List(departures, id: \.departure.predicted) { departure in - HStack { - Text(departure.headsign) - Spacer() - CountdownView(targetDate: departure.departure.predicted) + var body: some View { + List(departures, id: \.departure.predicted) { departure in + HStack { + Text(departure.headsign) + Spacer() + CountdownView(targetDate: departure.departure.predicted) + } } - }} + } } diff --git a/apps/mobile/metro-now/metro-now Watch App/pages/platform/PlatformDetailView.swift b/apps/mobile/metro-now/metro-now Watch App/pages/platform/PlatformDetailView.swift index d51c9a7f..b171654f 100644 --- a/apps/mobile/metro-now/metro-now Watch App/pages/platform/PlatformDetailView.swift +++ b/apps/mobile/metro-now/metro-now Watch App/pages/platform/PlatformDetailView.swift @@ -46,8 +46,8 @@ struct PlatformDetailView: View { .toolbar { if let metroLineName = metroLine?.rawValue { ToolbarItem( - placement: .confirmationAction) - { + placement: .confirmationAction + ) { if #available(watchOS 11, *) { Text(metroLineName) .overlay( diff --git a/apps/mobile/metro-now/metro-now Watch App/pages/stop/StopDepartureListItemPlaceholderView.swift b/apps/mobile/metro-now/metro-now Watch App/pages/stop/StopDepartureListItemPlaceholderView.swift index 73c85c08..1e9c2afd 100644 --- a/apps/mobile/metro-now/metro-now Watch App/pages/stop/StopDepartureListItemPlaceholderView.swift +++ b/apps/mobile/metro-now/metro-now Watch App/pages/stop/StopDepartureListItemPlaceholderView.swift @@ -6,8 +6,7 @@ import SwiftUI struct StopDepartureListItemPlaceholderView: View { let color: Color - init(color: Color? - ) { + init(color: Color?) { self.color = color ?? Color.gray.opacity(0.3) } diff --git a/apps/mobile/metro-now/metro-now Watch App/pages/stop/StopDeparturesView.swift b/apps/mobile/metro-now/metro-now Watch App/pages/stop/StopDeparturesView.swift index e22513e7..81b84453 100644 --- a/apps/mobile/metro-now/metro-now Watch App/pages/stop/StopDeparturesView.swift +++ b/apps/mobile/metro-now/metro-now Watch App/pages/stop/StopDeparturesView.swift @@ -51,8 +51,8 @@ struct StopDeparturesView: View { let platform = platforms.first( where: { $0.id == selectedPlatformId - - }) + } + ) if let platform { PlatformDetailView( diff --git a/apps/mobile/metro-now/metro-now/models/StopsViewModel.swift b/apps/mobile/metro-now/metro-now/models/StopsViewModel.swift index af1c0beb..df303f7d 100644 --- a/apps/mobile/metro-now/metro-now/models/StopsViewModel.swift +++ b/apps/mobile/metro-now/metro-now/models/StopsViewModel.swift @@ -8,10 +8,10 @@ import Foundation class LocationViewModel: NSObject, ObservableObject, CLLocationManagerDelegate { private let locationManager = CLLocationManager() - // Published property to store user's current location + /// Published property to store user's current location @Published var location: CLLocation? - // Published property to handle location access status + /// Published property to handle location access status @Published var authorizationStatus: CLAuthorizationStatus? override init() { @@ -26,7 +26,7 @@ class LocationViewModel: NSObject, ObservableObject, CLLocationManagerDelegate { authorizationStatus = locationManager.authorizationStatus } - // CLLocationManagerDelegate method: called when location updates + /// CLLocationManagerDelegate method: called when location updates func locationManager(_: CLLocationManager, didUpdateLocations locations: [CLLocation]) { // Take the most recent location guard let latestLocation = locations.last else { return } @@ -35,7 +35,7 @@ class LocationViewModel: NSObject, ObservableObject, CLLocationManagerDelegate { } } - // CLLocationManagerDelegate method: called when authorization status changes + /// CLLocationManagerDelegate method: called when authorization status changes func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { DispatchQueue.main.async { self.authorizationStatus = manager.authorizationStatus @@ -88,7 +88,6 @@ class StopsViewModel: NSObject, ObservableObject { stopPeriodicRefresh() // Stop any existing timer to avoid duplication. refreshTimer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in - guard let self else { return } diff --git a/apps/mobile/metro-now/metro-now/pages/closest-stop/ClosestStopPageViewModel.swift b/apps/mobile/metro-now/metro-now/pages/closest-stop/ClosestStopPageViewModel.swift index dfe21f9a..26b0c853 100644 --- a/apps/mobile/metro-now/metro-now/pages/closest-stop/ClosestStopPageViewModel.swift +++ b/apps/mobile/metro-now/metro-now/pages/closest-stop/ClosestStopPageViewModel.swift @@ -54,7 +54,6 @@ class ClosestStopPageViewModel: NSObject, ObservableObject, CLLocationManagerDel withTimeInterval: REFETCH_INTERVAL, repeats: true ) { [weak self] _ in - guard let self, let closestMetroStop, diff --git a/apps/mobile/metro-now/metro-now/pages/closest-stop/PlatformDeparturesListView.swift b/apps/mobile/metro-now/metro-now/pages/closest-stop/PlatformDeparturesListView.swift index 9405919f..3ba65b0b 100644 --- a/apps/mobile/metro-now/metro-now/pages/closest-stop/PlatformDeparturesListView.swift +++ b/apps/mobile/metro-now/metro-now/pages/closest-stop/PlatformDeparturesListView.swift @@ -32,8 +32,7 @@ struct PlatformDeparturesListView: View { .map(\.value) .sorted(by: { $0.first!.departure.predicted < $1.first!.departure.predicted - } - ) + }) ) } diff --git a/apps/mobile/metro-now/metro-now/pages/search/SearchPageSorting.swift b/apps/mobile/metro-now/metro-now/pages/search/SearchPageSorting.swift index 675339f9..2a2e0ffb 100644 --- a/apps/mobile/metro-now/metro-now/pages/search/SearchPageSorting.swift +++ b/apps/mobile/metro-now/metro-now/pages/search/SearchPageSorting.swift @@ -5,5 +5,7 @@ import SwiftUI enum SearchPageSorting: String, CaseIterable, Identifiable { case alphabetical, distance - var id: Self { self } + var id: Self { + self + } } diff --git a/apps/mobile/metro-now/metro-now/pages/search/stop-detail/SearchPageDetailViewModel.swift b/apps/mobile/metro-now/metro-now/pages/search/stop-detail/SearchPageDetailViewModel.swift index 3be52510..5330f67b 100644 --- a/apps/mobile/metro-now/metro-now/pages/search/stop-detail/SearchPageDetailViewModel.swift +++ b/apps/mobile/metro-now/metro-now/pages/search/stop-detail/SearchPageDetailViewModel.swift @@ -39,7 +39,6 @@ class SearchPageDetailViewModel: NSObject, ObservableObject { withTimeInterval: REFETCH_INTERVAL, repeats: true ) { [weak self] _ in - guard let self else { diff --git a/apps/mobile/metro-now/metro-now/pages/search/utils/normalizeForSearch.swift b/apps/mobile/metro-now/metro-now/pages/search/utils/normalizeForSearch.swift index 2f7ba913..7321fdb9 100644 --- a/apps/mobile/metro-now/metro-now/pages/search/utils/normalizeForSearch.swift +++ b/apps/mobile/metro-now/metro-now/pages/search/utils/normalizeForSearch.swift @@ -7,9 +7,8 @@ func normalizeForSearch(_ input: String) -> String { // Replace dots with spaces let noDots = noDiacritics.replacingOccurrences(of: ".", with: " ") // Normalize spaces (trim and replace multiple spaces with single) - let normalizedSpaces = noDots + return noDots .components(separatedBy: .whitespacesAndNewlines) .filter { !$0.isEmpty } .joined(separator: "") - return normalizedSpaces } diff --git a/apps/mobile/metro-now/metro-now/pages/settings/utils/getGithubIssueUrl.swift b/apps/mobile/metro-now/metro-now/pages/settings/utils/getGithubIssueUrl.swift index c6545a90..33dcb4b2 100644 --- a/apps/mobile/metro-now/metro-now/pages/settings/utils/getGithubIssueUrl.swift +++ b/apps/mobile/metro-now/metro-now/pages/settings/utils/getGithubIssueUrl.swift @@ -15,13 +15,11 @@ func getGithubIssueUrl(template: GithubIssueTemplate, title: String = "") -> URL return nil } - let urlWithQueryParams = url.appending(queryItems: [ + return url.appending(queryItems: [ URLQueryItem(name: "assignees", value: nil), URLQueryItem(name: "labels", value: nil), URLQueryItem(name: "projects", value: nil), URLQueryItem(name: "template", value: "\(template.rawValue).md"), URLQueryItem(name: "title", value: title), ]) - - return urlWithQueryParams } diff --git a/apps/mobile/metro-now/metro-now/pages/stop-detail/StopMetroDeparturesViewModel.swift b/apps/mobile/metro-now/metro-now/pages/stop-detail/StopMetroDeparturesViewModel.swift index a67d6dc8..401be76c 100644 --- a/apps/mobile/metro-now/metro-now/pages/stop-detail/StopMetroDeparturesViewModel.swift +++ b/apps/mobile/metro-now/metro-now/pages/stop-detail/StopMetroDeparturesViewModel.swift @@ -39,7 +39,6 @@ class StopMetroDeparturesViewModel: NSObject, ObservableObject { withTimeInterval: REFETCH_INTERVAL, repeats: true ) { [weak self] _ in - guard let self else { diff --git a/apps/mobile/metro-now/metro-now/pages/welcome/WelcomePageView.swift b/apps/mobile/metro-now/metro-now/pages/welcome/WelcomePageView.swift index f787430b..0532d732 100644 --- a/apps/mobile/metro-now/metro-now/pages/welcome/WelcomePageView.swift +++ b/apps/mobile/metro-now/metro-now/pages/welcome/WelcomePageView.swift @@ -28,8 +28,7 @@ struct WelcomePageView: View { .font(.system(size: 50)) VStack(spacing: 20) { - Text("This app is currently in development, so you might notice some features are still in progress." - ) + Text("This app is currently in development, so you might notice some features are still in progress.") Text("Thanks for your patience as we work to improve your experience! ❤️‍🔥") .fontWeight(.semibold) diff --git a/apps/mobile/metro-now/metro-nowTests/endpoint.test.swift b/apps/mobile/metro-now/metro-nowTests/endpoint.test.swift index 7f3fa15a..1be5bde9 100644 --- a/apps/mobile/metro-now/metro-nowTests/endpoint.test.swift +++ b/apps/mobile/metro-now/metro-nowTests/endpoint.test.swift @@ -5,7 +5,7 @@ import Foundation import Testing @Test("ENDPOINT") -func endpoint() async throws { +func endpoint() { #expect( !API_URL.contains("localhost"), "ENDPOINT should not contain localhost" diff --git a/apps/mobile/metro-now/metro-nowTests/get-remaining-time.test.swift b/apps/mobile/metro-now/metro-nowTests/get-remaining-time.test.swift index 7ee3ddc0..a20495d4 100644 --- a/apps/mobile/metro-now/metro-nowTests/get-remaining-time.test.swift +++ b/apps/mobile/metro-now/metro-nowTests/get-remaining-time.test.swift @@ -4,7 +4,6 @@ import Foundation import Testing -@Suite("getRemainingTime") struct GetRemainingTimeTests { @Test( "zero", @@ -13,14 +12,13 @@ struct GetRemainingTimeTests { ["0s", "0s", "0s", "-0s"] ) ) - func testGetRemainingTimeSeconds( + func getRemainingTimeSeconds( remainingTime: Double, expected: String ) { #expect(getRemainingTime(remainingTime) == expected) } - @Suite("future") struct GetRemainingTimeFutureTests { @Test( "seconds", @@ -43,7 +41,7 @@ struct GetRemainingTimeTests { ["1m 0s", "59m 0s", "59m 59s"] ) ) - func testGetRemainingTimeMinutes( + func getRemainingTimeMinutes( remainingTime: Double, expected: String ) { @@ -67,7 +65,7 @@ struct GetRemainingTimeTests { ] ) ) - func testGetRemainingTimeHours( + func getRemainingTimeHours( remainingTime: Double, expected: String ) { @@ -75,7 +73,6 @@ struct GetRemainingTimeTests { } } - @Suite("past") struct GetRemainingTimePastTests { @Test( "seconds", diff --git a/apps/mobile/metro-now/metro-nowTests/route-vehicle-type.tests.swift b/apps/mobile/metro-now/metro-nowTests/route-vehicle-type.tests.swift index 153ca4cb..5832bb9d 100644 --- a/apps/mobile/metro-now/metro-nowTests/route-vehicle-type.tests.swift +++ b/apps/mobile/metro-now/metro-nowTests/route-vehicle-type.tests.swift @@ -4,40 +4,39 @@ import Foundation import Testing -@Suite("getRouteType") struct GetRouteTypeTests { @Test("bus", arguments: ["X100", "X728", "100", "128", "245", "348", "899", "BB1", "BB2"]) - func testGetBusRouteType(routeName: String) async throws { + func getBusRouteType(routeName: String) { let result = getRouteType(routeName) #expect(result.rawValue == RouteType.bus.rawValue) } @Test("tram", arguments: ["X1", "X33", "1", "12", "89"]) - func testGetTramRouteType(routeName: String) async throws { + func getTramRouteType(routeName: String) { let result = getRouteType(routeName) #expect(result.rawValue == RouteType.tram.rawValue) } @Test("night", arguments: ["X95", "95", "99", "950", "X950"]) - func testGetNightRouteType(routeName: String) async throws { + func getNightRouteType(routeName: String) { let result = getRouteType(routeName) #expect(result.rawValue == RouteType.night.rawValue) } @Test("funicular", arguments: ["LD", "LD1"]) - func testGetFunicularRouteType(routeName: String) async throws { + func getFunicularRouteType(routeName: String) { let result = getRouteType(routeName) #expect(result.rawValue == RouteType.funicular.rawValue) } @Test("ferry", arguments: ["XP", "P", "P0", "P1", "P2", "P3", "P4", "P50", "P999"]) - func testGetFerryRouteType(routeName: String) async throws { + func getFerryRouteType(routeName: String) { let result = getRouteType(routeName) #expect(result.rawValue == RouteType.ferry.rawValue) } @Test("train", arguments: ["XS1", "XR90", "S1", "S22", "R2", "R700"]) - func testGetTrainRouteType(routeName: String) async throws { + func getTrainRouteType(routeName: String) { let result = getRouteType(routeName) #expect(result.rawValue == RouteType.train.rawValue) } diff --git a/apps/mobile/metro-now/metro-nowTests/shorten-stop-name.test.swift b/apps/mobile/metro-now/metro-nowTests/shorten-stop-name.test.swift index 2e6e4e37..938aa26d 100644 --- a/apps/mobile/metro-now/metro-nowTests/shorten-stop-name.test.swift +++ b/apps/mobile/metro-now/metro-nowTests/shorten-stop-name.test.swift @@ -4,7 +4,6 @@ import Foundation import Testing -@Suite("shortenStopName") struct ShortenStopNameTests { @Test("static", arguments: zip([ @@ -25,10 +24,10 @@ struct ShortenStopNameTests { "Černý Most", "I. P. Pavlova", ])) - func testStaticShortenStopName( + func staticShortenStopName( stopName: String, expected: String - ) async throws { + ) { let shortened = shortenStopName(stopName) #expect(shortened == expected) @@ -49,10 +48,10 @@ struct ShortenStopNameTests { "N. Veleslavín", "Nám. Míru", ])) - func testDynamicShortenStopName( + func dynamicShortenStopName( stopName: String, expected: String - ) async throws { + ) { let shortened = shortenStopName(stopName) #expect(shortened == expected) diff --git a/apps/web/src/app/map/map-layers/heatmap-layer.ts b/apps/web/src/app/map/map-layers/heatmap-layer.ts index 0d30de63..d9290a41 100644 --- a/apps/web/src/app/map/map-layers/heatmap-layer.ts +++ b/apps/web/src/app/map/map-layers/heatmap-layer.ts @@ -48,6 +48,7 @@ export const heatmapLayer: LayerProps = { [14, 0.6], [15, 0], ], + // biome-ignore lint/suspicious/noExplicitAny: mapbox-gl type mismatch for stop-based expressions } as any, }, }; diff --git a/apps/web/src/app/robots.ts b/apps/web/src/app/robots.ts index 614b3f8c..15b010ca 100644 --- a/apps/web/src/app/robots.ts +++ b/apps/web/src/app/robots.ts @@ -1,5 +1,5 @@ -import { HOMEPAGE_URL } from "../constants/api"; import type { MetadataRoute } from "next"; +import { HOMEPAGE_URL } from "../constants/api"; const robots = (): MetadataRoute.Robots => ({ rules: { diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..56800492 --- /dev/null +++ b/biome.json @@ -0,0 +1,65 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "files": { + "include": [ + "apps/backend/**/*.ts", + "apps/backend/**/*.js", + "apps/backend/tsconfig.json", + "apps/backend/package.json", + "apps/dataloader/**/*.ts", + "apps/dataloader/**/*.js", + "apps/dataloader/tsconfig.json", + "apps/dataloader/package.json", + "apps/database/**/*.ts", + "apps/database/**/*.json", + "apps/web/**/*.ts", + "apps/web/**/*.json", + "apps/web/tsconfig.json", + "lib/**/*.ts", + "lib/**/*.js", + "lib/shared/tsconfig.json", + "lib/shared/package.json", + ".github/**/*.yaml", + ".github/**/*.yml", + "biome.json", + "compose.yaml", + "package.json", + "pnpm-workspace.yaml" + ], + "ignore": [ + "**/node_modules/**", + "**/dist/**", + "**/.next/**", + "apps/backend/src/types/graphql.generated.ts", + "apps/database/seeds/**" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 4, + "lineEnding": "lf" + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "useImportType": "off" + } + } + }, + "javascript": { + "parser": { + "unsafeParameterDecoratorsEnabled": true + }, + "formatter": { + "quoteStyle": "double", + "semicolons": "always", + "trailingCommas": "all" + } + } +} diff --git a/compose.yaml b/compose.yaml index 5f366bdf..ca00bde0 100644 --- a/compose.yaml +++ b/compose.yaml @@ -26,8 +26,8 @@ services: container_name: redis-stack restart: always ports: - - 6379:6379 - - 8001:8001 + - 6479:6379 + - 8101:8001 extra_hosts: - "host.docker.internal:host-gateway" @@ -36,7 +36,7 @@ services: image: postgres:16-alpine restart: always ports: - - 5432:5432 + - 5532:5432 volumes: - postgres-data:/var/lib/postgresql/data/ env_file: @@ -52,7 +52,6 @@ services: - /bin/bash - -c - | - pnpm prisma:migrate:deploy pnpm start:prod depends_on: - postgres diff --git a/lib/shared/dist/database.d.ts b/lib/shared/dist/database.d.ts new file mode 100644 index 00000000..0b2332f7 --- /dev/null +++ b/lib/shared/dist/database.d.ts @@ -0,0 +1,13 @@ +import { type DatabaseClient } from "@metro-now/database"; +import { Pool, type PoolConfig } from "pg"; +type EnvSource = NodeJS.ProcessEnv; +export declare const createDatabaseUrl: (env?: EnvSource) => string; +export declare const createDatabasePool: ({ env, ...options }?: PoolConfig & { + env?: EnvSource; +}) => Pool; +export declare const createDatabaseClient: ({ env, pool, }?: { + env?: EnvSource; + pool?: Pool; +}) => DatabaseClient; +export {}; +//# sourceMappingURL=database.d.ts.map \ No newline at end of file diff --git a/lib/shared/dist/database.d.ts.map b/lib/shared/dist/database.d.ts.map new file mode 100644 index 00000000..acaed3a3 --- /dev/null +++ b/lib/shared/dist/database.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,cAAc,EAEtB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,IAAI,EAAE,KAAK,UAAU,EAAE,MAAM,IAAI,CAAC;AAE3C,KAAK,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC;AAiDnC,eAAO,MAAM,iBAAiB,GAAI,MAAK,SAAuB,KAAG,MAqBhE,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,sBAGhC,UAAU,GAAG;IACZ,GAAG,CAAC,EAAE,SAAS,CAAC;CACd,KAAG,IAIH,CAAC;AAEP,eAAO,MAAM,oBAAoB,GAAI,iBAGlC;IACC,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,IAAI,CAAC,EAAE,IAAI,CAAC;CACV,KAAG,cAKH,CAAC"} \ No newline at end of file diff --git a/lib/shared/dist/database.js b/lib/shared/dist/database.js new file mode 100644 index 00000000..721bb157 --- /dev/null +++ b/lib/shared/dist/database.js @@ -0,0 +1,61 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createDatabaseClient = exports.createDatabasePool = exports.createDatabaseUrl = void 0; +const kysely_1 = require("kysely"); +const pg_1 = require("pg"); +const REQUIRED_DATABASE_ENV_KEYS = [ + "POSTGRES_USER", + "POSTGRES_PASSWORD", + "POSTGRES_DB", + "DB_HOST", + "DB_PORT", + "DB_SCHEMA", +]; +const unwrapEnvValue = (value) => { + const trimmedValue = value.trim(); + if ((trimmedValue.startsWith('"') && trimmedValue.endsWith('"')) || + (trimmedValue.startsWith("'") && trimmedValue.endsWith("'"))) { + return trimmedValue.slice(1, -1); + } + return trimmedValue; +}; +const expandEnvVariables = (value, env) => { + return value.replace(/\$\{([A-Z0-9_]+)\}/g, (_match, key) => { + return env[key] ?? ""; + }); +}; +const normalizeDatabaseUrl = (databaseUrl, env) => { + const expandedDatabaseUrl = expandEnvVariables(unwrapEnvValue(databaseUrl), env); + try { + return new URL(expandedDatabaseUrl).toString(); + } + catch { + return null; + } +}; +const createDatabaseUrl = (env = process.env) => { + if (env.DATABASE_URL) { + const normalizedDatabaseUrl = normalizeDatabaseUrl(env.DATABASE_URL, env); + if (normalizedDatabaseUrl) { + return normalizedDatabaseUrl; + } + } + const missingKeys = REQUIRED_DATABASE_ENV_KEYS.filter((key) => !env[key]); + if (missingKeys.length > 0) { + throw new Error(`Missing database environment variables: ${missingKeys.join(", ")}`); + } + return `postgresql://${env.POSTGRES_USER}:${env.POSTGRES_PASSWORD}@${env.DB_HOST}:${env.DB_PORT}/${env.POSTGRES_DB}?schema=${env.DB_SCHEMA}`; +}; +exports.createDatabaseUrl = createDatabaseUrl; +const createDatabasePool = ({ env = process.env, ...options } = {}) => new pg_1.Pool({ + connectionString: (0, exports.createDatabaseUrl)(env), + ...options, +}); +exports.createDatabasePool = createDatabasePool; +const createDatabaseClient = ({ env = process.env, pool = (0, exports.createDatabasePool)({ env }), } = {}) => new kysely_1.Kysely({ + dialect: new kysely_1.PostgresDialect({ + pool, + }), +}); +exports.createDatabaseClient = createDatabaseClient; +//# sourceMappingURL=database.js.map \ No newline at end of file diff --git a/lib/shared/dist/database.js.map b/lib/shared/dist/database.js.map new file mode 100644 index 00000000..57f7632b --- /dev/null +++ b/lib/shared/dist/database.js.map @@ -0,0 +1 @@ +{"version":3,"file":"database.js","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":";;;AAIA,mCAAiD;AACjD,2BAA2C;AAI3C,MAAM,0BAA0B,GAAG;IAC/B,eAAe;IACf,mBAAmB;IACnB,aAAa;IACb,SAAS;IACT,SAAS;IACT,WAAW;CACL,CAAC;AAEX,MAAM,cAAc,GAAG,CAAC,KAAa,EAAU,EAAE;IAC7C,MAAM,YAAY,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAElC,IACI,CAAC,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,YAAY,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC5D,CAAC,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,YAAY,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAC9D,CAAC;QACC,OAAO,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACrC,CAAC;IAED,OAAO,YAAY,CAAC;AACxB,CAAC,CAAC;AAEF,MAAM,kBAAkB,GAAG,CAAC,KAAa,EAAE,GAAc,EAAU,EAAE;IACjE,OAAO,KAAK,CAAC,OAAO,CAChB,qBAAqB,EACrB,CAAC,MAAc,EAAE,GAAW,EAAE,EAAE;QAC5B,OAAO,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;IAC1B,CAAC,CACJ,CAAC;AACN,CAAC,CAAC;AAEF,MAAM,oBAAoB,GAAG,CACzB,WAAmB,EACnB,GAAc,EACD,EAAE;IACf,MAAM,mBAAmB,GAAG,kBAAkB,CAC1C,cAAc,CAAC,WAAW,CAAC,EAC3B,GAAG,CACN,CAAC;IAEF,IAAI,CAAC;QACD,OAAO,IAAI,GAAG,CAAC,mBAAmB,CAAC,CAAC,QAAQ,EAAE,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,IAAI,CAAC;IAChB,CAAC;AACL,CAAC,CAAC;AAEK,MAAM,iBAAiB,GAAG,CAAC,MAAiB,OAAO,CAAC,GAAG,EAAU,EAAE;IACtE,IAAI,GAAG,CAAC,YAAY,EAAE,CAAC;QACnB,MAAM,qBAAqB,GAAG,oBAAoB,CAC9C,GAAG,CAAC,YAAY,EAChB,GAAG,CACN,CAAC;QAEF,IAAI,qBAAqB,EAAE,CAAC;YACxB,OAAO,qBAAqB,CAAC;QACjC,CAAC;IACL,CAAC;IAED,MAAM,WAAW,GAAG,0BAA0B,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IAE1E,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACX,2CAA2C,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACtE,CAAC;IACN,CAAC;IAED,OAAO,gBAAgB,GAAG,CAAC,aAAa,IAAI,GAAG,CAAC,iBAAiB,IAAI,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,WAAW,WAAW,GAAG,CAAC,SAAS,EAAE,CAAC;AACjJ,CAAC,CAAC;AArBW,QAAA,iBAAiB,qBAqB5B;AAEK,MAAM,kBAAkB,GAAG,CAAC,EAC/B,GAAG,GAAG,OAAO,CAAC,GAAG,EACjB,GAAG,OAAO,KAGV,EAAE,EAAQ,EAAE,CACZ,IAAI,SAAI,CAAC;IACL,gBAAgB,EAAE,IAAA,yBAAiB,EAAC,GAAG,CAAC;IACxC,GAAG,OAAO;CACb,CAAC,CAAC;AATM,QAAA,kBAAkB,sBASxB;AAEA,MAAM,oBAAoB,GAAG,CAAC,EACjC,GAAG,GAAG,OAAO,CAAC,GAAG,EACjB,IAAI,GAAG,IAAA,0BAAkB,EAAC,EAAE,GAAG,EAAE,CAAC,MAIlC,EAAE,EAAkB,EAAE,CACtB,IAAI,eAAM,CAAmB;IACzB,OAAO,EAAE,IAAI,wBAAe,CAAC;QACzB,IAAI;KACP,CAAC;CACL,CAAC,CAAC;AAXM,QAAA,oBAAoB,wBAW1B"} \ No newline at end of file diff --git a/lib/shared/dist/env.d.ts b/lib/shared/dist/env.d.ts new file mode 100644 index 00000000..2e6165bf --- /dev/null +++ b/lib/shared/dist/env.d.ts @@ -0,0 +1,77 @@ +import { z } from "zod"; +export declare const databaseEnvSchema: z.ZodObject<{ + DATABASE_URL: z.ZodOptional; + POSTGRES_USER: z.ZodString; + POSTGRES_PASSWORD: z.ZodString; + POSTGRES_DB: z.ZodString; + DB_HOST: z.ZodString; + DB_PORT: z.ZodNumber; + DB_SCHEMA: z.ZodString; +}, "strip", z.ZodTypeAny, { + POSTGRES_USER: string; + POSTGRES_PASSWORD: string; + POSTGRES_DB: string; + DB_HOST: string; + DB_PORT: number; + DB_SCHEMA: string; + DATABASE_URL?: string | undefined; +}, { + POSTGRES_USER: string; + POSTGRES_PASSWORD: string; + POSTGRES_DB: string; + DB_HOST: string; + DB_PORT: number; + DB_SCHEMA: string; + DATABASE_URL?: string | undefined; +}>; +export declare const redisEnvSchema: z.ZodObject<{ + REDIS_HOST: z.ZodOptional; + REDIS_PORT: z.ZodOptional; +}, "strip", z.ZodTypeAny, { + REDIS_HOST?: string | undefined; + REDIS_PORT?: number | undefined; +}, { + REDIS_HOST?: string | undefined; + REDIS_PORT?: number | undefined; +}>; +export declare const commonServerEnvSchema: z.ZodObject; + POSTGRES_USER: z.ZodString; + POSTGRES_PASSWORD: z.ZodString; + POSTGRES_DB: z.ZodString; + DB_HOST: z.ZodString; + DB_PORT: z.ZodNumber; + DB_SCHEMA: z.ZodString; +}, { + REDIS_HOST: z.ZodOptional; + REDIS_PORT: z.ZodOptional; +}>, { + PORT: z.ZodOptional; + LOGS: z.ZodOptional; +}>, "strip", z.ZodTypeAny, { + POSTGRES_USER: string; + POSTGRES_PASSWORD: string; + POSTGRES_DB: string; + DB_HOST: string; + DB_PORT: number; + DB_SCHEMA: string; + DATABASE_URL?: string | undefined; + REDIS_HOST?: string | undefined; + REDIS_PORT?: number | undefined; + PORT?: number | undefined; + LOGS?: string | undefined; +}, { + POSTGRES_USER: string; + POSTGRES_PASSWORD: string; + POSTGRES_DB: string; + DB_HOST: string; + DB_PORT: number; + DB_SCHEMA: string; + DATABASE_URL?: string | undefined; + REDIS_HOST?: string | undefined; + REDIS_PORT?: number | undefined; + PORT?: number | undefined; + LOGS?: string | undefined; +}>; +export declare const validateEnv: (schema: Schema, env?: NodeJS.ProcessEnv) => z.infer; +//# sourceMappingURL=env.d.ts.map \ No newline at end of file diff --git a/lib/shared/dist/env.d.ts.map b/lib/shared/dist/env.d.ts.map new file mode 100644 index 00000000..9de3f875 --- /dev/null +++ b/lib/shared/dist/env.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;EAQ5B,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;EAGzB,CAAC;AAEH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAK5B,CAAC;AAEP,eAAO,MAAM,WAAW,GAAI,MAAM,SAAS,CAAC,CAAC,UAAU,EACnD,QAAQ,MAAM,EACd,MAAK,MAAM,CAAC,UAAwB,KACrC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAsB,CAAC"} \ No newline at end of file diff --git a/lib/shared/dist/env.js b/lib/shared/dist/env.js new file mode 100644 index 00000000..c922d9c3 --- /dev/null +++ b/lib/shared/dist/env.js @@ -0,0 +1,26 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.validateEnv = exports.commonServerEnvSchema = exports.redisEnvSchema = exports.databaseEnvSchema = void 0; +const zod_1 = require("zod"); +exports.databaseEnvSchema = zod_1.z.object({ + DATABASE_URL: zod_1.z.string().optional(), + POSTGRES_USER: zod_1.z.string().min(1), + POSTGRES_PASSWORD: zod_1.z.string().min(1), + POSTGRES_DB: zod_1.z.string().min(1), + DB_HOST: zod_1.z.string().min(1), + DB_PORT: zod_1.z.coerce.number().int().positive(), + DB_SCHEMA: zod_1.z.string().min(1), +}); +exports.redisEnvSchema = zod_1.z.object({ + REDIS_HOST: zod_1.z.string().min(1).optional(), + REDIS_PORT: zod_1.z.coerce.number().int().positive().optional(), +}); +exports.commonServerEnvSchema = exports.databaseEnvSchema + .merge(exports.redisEnvSchema) + .extend({ + PORT: zod_1.z.coerce.number().int().positive().optional(), + LOGS: zod_1.z.string().optional(), +}); +const validateEnv = (schema, env = process.env) => schema.parse(env); +exports.validateEnv = validateEnv; +//# sourceMappingURL=env.js.map \ No newline at end of file diff --git a/lib/shared/dist/env.js.map b/lib/shared/dist/env.js.map new file mode 100644 index 00000000..1f552297 --- /dev/null +++ b/lib/shared/dist/env.js.map @@ -0,0 +1 @@ +{"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AAEX,QAAA,iBAAiB,GAAG,OAAC,CAAC,MAAM,CAAC;IACtC,YAAY,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACnC,aAAa,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAChC,iBAAiB,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACpC,WAAW,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9B,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1B,OAAO,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IAC3C,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAC/B,CAAC,CAAC;AAEU,QAAA,cAAc,GAAG,OAAC,CAAC,MAAM,CAAC;IACnC,UAAU,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACxC,UAAU,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CAC5D,CAAC,CAAC;AAEU,QAAA,qBAAqB,GAAG,yBAAiB;KACjD,KAAK,CAAC,sBAAc,CAAC;KACrB,MAAM,CAAC;IACJ,IAAI,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IACnD,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC9B,CAAC,CAAC;AAEA,MAAM,WAAW,GAAG,CACvB,MAAc,EACd,MAAyB,OAAO,CAAC,GAAG,EACrB,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AAH3B,QAAA,WAAW,eAGgB"} \ No newline at end of file diff --git a/lib/shared/dist/index.d.ts b/lib/shared/dist/index.d.ts new file mode 100644 index 00000000..9ada603e --- /dev/null +++ b/lib/shared/dist/index.d.ts @@ -0,0 +1,3 @@ +export * from "./env"; +export * from "./database"; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/lib/shared/dist/index.d.ts.map b/lib/shared/dist/index.d.ts.map new file mode 100644 index 00000000..c34908d8 --- /dev/null +++ b/lib/shared/dist/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,OAAO,CAAC;AACtB,cAAc,YAAY,CAAC"} \ No newline at end of file diff --git a/lib/shared/dist/index.js b/lib/shared/dist/index.js new file mode 100644 index 00000000..33a80096 --- /dev/null +++ b/lib/shared/dist/index.js @@ -0,0 +1,19 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +__exportStar(require("./env"), exports); +__exportStar(require("./database"), exports); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/lib/shared/dist/index.js.map b/lib/shared/dist/index.js.map new file mode 100644 index 00000000..84d7b9c7 --- /dev/null +++ b/lib/shared/dist/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,wCAAsB;AACtB,6CAA2B"} \ No newline at end of file diff --git a/lib/shared/package.json b/lib/shared/package.json new file mode 100644 index 00000000..bb44790b --- /dev/null +++ b/lib/shared/package.json @@ -0,0 +1,23 @@ +{ + "name": "@metro-now/shared", + "version": "1.0.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "pnpm --filter @metro-now/database build && tsc", + "lint": "biome check --config-path ../../biome.json ../../lib/shared/src ../../lib/shared/package.json ../../lib/shared/tsconfig.json", + "format": "biome format --config-path ../../biome.json ../../lib/shared/src ../../lib/shared/package.json ../../lib/shared/tsconfig.json --write", + "format:check": "biome check --config-path ../../biome.json ../../lib/shared/src ../../lib/shared/package.json ../../lib/shared/tsconfig.json" + }, + "dependencies": { + "@metro-now/database": "workspace:*", + "kysely": "^0.28.14", + "pg": "^8.20.0", + "zod": "^3.24.2" + }, + "devDependencies": { + "@types/node": "^25.5.0", + "@types/pg": "^8.15.5", + "typescript": "^5.8.3" + } +} diff --git a/lib/shared/src/database.ts b/lib/shared/src/database.ts new file mode 100644 index 00000000..4a27fdbb --- /dev/null +++ b/lib/shared/src/database.ts @@ -0,0 +1,102 @@ +import { + type DatabaseClient, + type MetroNowDatabase, +} from "@metro-now/database"; +import { Kysely, PostgresDialect } from "kysely"; +import { Pool, type PoolConfig } from "pg"; + +type EnvSource = NodeJS.ProcessEnv; + +const REQUIRED_DATABASE_ENV_KEYS = [ + "POSTGRES_USER", + "POSTGRES_PASSWORD", + "POSTGRES_DB", + "DB_HOST", + "DB_PORT", + "DB_SCHEMA", +] as const; + +const unwrapEnvValue = (value: string): string => { + const trimmedValue = value.trim(); + + if ( + (trimmedValue.startsWith('"') && trimmedValue.endsWith('"')) || + (trimmedValue.startsWith("'") && trimmedValue.endsWith("'")) + ) { + return trimmedValue.slice(1, -1); + } + + return trimmedValue; +}; + +const expandEnvVariables = (value: string, env: EnvSource): string => { + return value.replace( + /\$\{([A-Z0-9_]+)\}/g, + (_match: string, key: string) => { + return env[key] ?? ""; + }, + ); +}; + +const normalizeDatabaseUrl = ( + databaseUrl: string, + env: EnvSource, +): string | null => { + const expandedDatabaseUrl = expandEnvVariables( + unwrapEnvValue(databaseUrl), + env, + ); + + try { + return new URL(expandedDatabaseUrl).toString(); + } catch { + return null; + } +}; + +export const createDatabaseUrl = (env: EnvSource = process.env): string => { + if (env.DATABASE_URL) { + const normalizedDatabaseUrl = normalizeDatabaseUrl( + env.DATABASE_URL, + env, + ); + + if (normalizedDatabaseUrl) { + return normalizedDatabaseUrl; + } + } + + const missingKeys = REQUIRED_DATABASE_ENV_KEYS.filter((key) => !env[key]); + + if (missingKeys.length > 0) { + throw new Error( + `Missing database environment variables: ${missingKeys.join(", ")}`, + ); + } + + return `postgresql://${env.POSTGRES_USER}:${env.POSTGRES_PASSWORD}@${env.DB_HOST}:${env.DB_PORT}/${env.POSTGRES_DB}?schema=${env.DB_SCHEMA}`; +}; + +export const createDatabasePool = ({ + env = process.env, + ...options +}: PoolConfig & { + env?: EnvSource; +} = {}): Pool => + new Pool({ + connectionString: createDatabaseUrl(env), + ...options, + }); + +export const createDatabaseClient = ({ + env = process.env, + pool = createDatabasePool({ env }), +}: { + env?: EnvSource; + pool?: Pool; +} = {}): DatabaseClient => + new Kysely({ + dialect: new PostgresDialect({ + pool, + }), + }); diff --git a/lib/shared/src/env.ts b/lib/shared/src/env.ts new file mode 100644 index 00000000..33443caa --- /dev/null +++ b/lib/shared/src/env.ts @@ -0,0 +1,28 @@ +import { z } from "zod"; + +export const databaseEnvSchema = z.object({ + DATABASE_URL: z.string().optional(), + POSTGRES_USER: z.string().min(1), + POSTGRES_PASSWORD: z.string().min(1), + POSTGRES_DB: z.string().min(1), + DB_HOST: z.string().min(1), + DB_PORT: z.coerce.number().int().positive(), + DB_SCHEMA: z.string().min(1), +}); + +export const redisEnvSchema = z.object({ + REDIS_HOST: z.string().min(1).optional(), + REDIS_PORT: z.coerce.number().int().positive().optional(), +}); + +export const commonServerEnvSchema = databaseEnvSchema + .merge(redisEnvSchema) + .extend({ + PORT: z.coerce.number().int().positive().optional(), + LOGS: z.string().optional(), + }); + +export const validateEnv = ( + schema: Schema, + env: NodeJS.ProcessEnv = process.env, +): z.infer => schema.parse(env); diff --git a/lib/shared/src/index.ts b/lib/shared/src/index.ts new file mode 100644 index 00000000..34aea4fa --- /dev/null +++ b/lib/shared/src/index.ts @@ -0,0 +1,2 @@ +export * from "./env"; +export * from "./database"; diff --git a/lib/shared/tsconfig.json b/lib/shared/tsconfig.json new file mode 100644 index 00000000..4a62d6df --- /dev/null +++ b/lib/shared/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/package.json b/package.json index f8049167..b2f644c5 100644 --- a/package.json +++ b/package.json @@ -9,16 +9,21 @@ "backend:build": "pnpm run -r --filter @metro-now/backend build", "backend:dev": "pnpm run -r --filter @metro-now/backend dev", "web:dev": "pnpm run -r --filter @metro-now/web dev", + "db:migrate:deploy": "pnpm run -r --filter @metro-now/database migrate:deploy", + "db:migrate:rollback": "pnpm run -r --filter @metro-now/database migrate:rollback", + "db:seed": "pnpm run -r --filter @metro-now/database seed", + "biome:check": "pnpm --filter @metro-now/backend format:check && pnpm --filter @metro-now/dataloader format:check && pnpm --filter @metro-now/shared format:check", + "biome:write": "pnpm --filter @metro-now/backend format && pnpm --filter @metro-now/dataloader format && pnpm --filter @metro-now/shared format", "precommit": "pnpm format", - "format": "pnpm --recursive --parallel format && pnpm exec prettier . --write", + "format": "pnpm --recursive --parallel format", "app:format": "pnpm --filter @metro-now/mobile format", - "format:check": "pnpm -r format:check && pnpm exec prettier . --check", + "format:check": "pnpm -r format:check", "docker:up:dev": "docker compose up postgres redis-stack", "docker:up": "docker compose up -d --build", "docker:down": "docker compose down --remove-orphans --volumes" }, "devDependencies": { - "prettier": "^3.5.3" + "@biomejs/biome": "^1.9.4" }, "homepage": "https://metro-now.vercel.app/", "repository": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 93f7b7e0..b5852bb9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,63 +8,69 @@ importers: .: devDependencies: - prettier: - specifier: ^3.5.3 - version: 3.5.3 + '@biomejs/biome': + specifier: ^1.9.4 + version: 1.9.4 apps/backend: dependencies: '@apollo/server': - specifier: ^4.11.3 - version: 4.11.3(graphql@16.10.0) - '@fast-csv/parse': - specifier: ^5.0.2 - version: 5.0.2 + specifier: ^5.5.0 + version: 5.5.0(graphql@16.13.2) + '@as-integrations/express5': + specifier: ^1.1.2 + version: 1.1.2(@apollo/server@5.5.0(graphql@16.13.2))(express@5.2.1) '@keyv/redis': - specifier: ^4.3.3 - version: 4.3.3 + specifier: ^5.1.6 + version: 5.1.6(keyv@5.6.0) + '@metro-now/database': + specifier: workspace:* + version: link:../database + '@metro-now/shared': + specifier: workspace:* + version: link:../../lib/shared '@nestjs/apollo': - specifier: ^13.0.4 - version: 13.0.4(@apollo/server@4.11.3(graphql@16.10.0))(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/graphql@13.0.4(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13)(reflect-metadata@0.2.2)(rxjs@7.8.2))(graphql@16.10.0)(reflect-metadata@0.2.2)(ts-morph@25.0.1))(graphql@16.10.0) + specifier: ^13.2.4 + version: 13.2.4(@apollo/server@5.5.0(graphql@16.13.2))(@as-integrations/express5@1.1.2(@apollo/server@5.5.0(graphql@16.13.2))(express@5.2.1))(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/graphql@13.2.4(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(graphql@16.13.2)(reflect-metadata@0.2.2)(ts-morph@25.0.1))(graphql@16.13.2) '@nestjs/cache-manager': - specifier: ^3.0.1 - version: 3.0.1(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13)(reflect-metadata@0.2.2)(rxjs@7.8.2))(cache-manager@6.4.2)(keyv@5.3.2)(rxjs@7.8.2) + specifier: ^3.1.0 + version: 3.1.0(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(cache-manager@7.2.8)(keyv@5.6.0)(rxjs@7.8.2) '@nestjs/common': - specifier: ^11.0.13 - version: 11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: ^11.1.17 + version: 11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/config': - specifier: ^4.0.2 - version: 4.0.2(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + specifier: ^4.0.3 + version: 4.0.3(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/core': - specifier: ^11.0.13 - version: 11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: ^11.1.17 + version: 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/graphql': - specifier: ^13.0.4 - version: 13.0.4(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13)(reflect-metadata@0.2.2)(rxjs@7.8.2))(graphql@16.10.0)(reflect-metadata@0.2.2)(ts-morph@25.0.1) + specifier: ^13.2.4 + version: 13.2.4(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(graphql@16.13.2)(reflect-metadata@0.2.2)(ts-morph@25.0.1) '@nestjs/platform-express': - specifier: ^11.0.13 - version: 11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.13) - '@nestjs/schedule': - specifier: ^5.0.1 - version: 5.0.1(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13)(reflect-metadata@0.2.2)(rxjs@7.8.2)) + specifier: ^11.1.17 + version: 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17) '@nestjs/swagger': - specifier: ^11.1.1 - version: 11.1.1(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) - '@prisma/client': - specifier: 5.20.0 - version: 5.20.0(prisma@5.22.0) + specifier: ^11.2.6 + version: 11.2.6(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) cache-manager: - specifier: ^6.4.2 - version: 6.4.2 + specifier: ^7.2.8 + version: 7.2.8 + cacheable: + specifier: ^2.3.4 + version: 2.3.4 dataloader: specifier: ^2.2.3 version: 2.2.3 graphql: - specifier: ^16.10.0 - version: 16.10.0 + specifier: ^16.13.2 + version: 16.13.2 + keyv: + specifier: ^5.6.0 + version: 5.6.0 radash: - specifier: ^12.1.0 - version: 12.1.0 + specifier: ^12.1.1 + version: 12.1.1 reflect-metadata: specifier: ^0.2.2 version: 0.2.2 @@ -74,32 +80,22 @@ importers: ts-morph: specifier: ^25.0.1 version: 25.0.1 - unzipper: - specifier: ^0.12.3 - version: 0.12.3 zod: specifier: ^3.24.2 version: 3.24.2 - optionalDependencies: - dotenv-cli: - specifier: ^8.0.0 - version: 8.0.0 - prisma: - specifier: ^5.20.0 - version: 5.22.0 devDependencies: '@nestjs/cli': - specifier: ^11.0.6 - version: 11.0.6(@types/node@22.14.0) + specifier: ^11.0.16 + version: 11.0.16(@types/node@22.14.0) '@nestjs/schematics': - specifier: ^11.0.4 - version: 11.0.4(chokidar@4.0.3)(typescript@5.8.3) + specifier: ^11.0.9 + version: 11.0.9(chokidar@4.0.3)(typescript@5.8.3) '@nestjs/testing': - specifier: ^11.0.13 - version: 11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.13)) + specifier: ^11.1.17 + version: 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)) '@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 @@ -109,51 +105,24 @@ importers: '@types/supertest': specifier: ^6.0.3 version: 6.0.3 - '@types/unzipper': - specifier: ^0.10.11 - version: 0.10.11 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.8.3) - eslint: - specifier: ^8.57.1 - version: 8.57.1 - eslint-config-prettier: - specifier: ^9.1.0 - version: 9.1.0(eslint@8.57.1) - eslint-import-resolver-typescript: - specifier: ^3.10.0 - version: 3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) - eslint-plugin-import: - specifier: ^2.31.0 - version: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1) - eslint-plugin-prettier: - specifier: ^5.2.6 - version: 5.2.6(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.5.3) jest: specifier: ^29.7.0 version: 29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.3)) - prettier: - specifier: ^3.5.3 - version: 3.5.3 rimraf: - specifier: ^6.0.1 - version: 6.0.1 + specifier: ^6.1.3 + version: 6.1.3 source-map-support: specifier: ^0.5.21 version: 0.5.21 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))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.3)))(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@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.3)))(typescript@5.8.3) ts-loader: - specifier: ^9.5.2 - version: 9.5.2(typescript@5.8.3)(webpack@5.98.0) + specifier: ^9.5.4 + version: 9.5.4(typescript@5.8.3)(webpack@5.104.1) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.14.0)(typescript@5.8.3) @@ -164,6 +133,75 @@ importers: specifier: ^5.8.3 version: 5.8.3 + apps/database: + dependencies: + kysely: + specifier: ^0.28.14 + version: 0.28.14 + pg: + specifier: ^8.20.0 + version: 8.20.0 + optionalDependencies: + dotenv-cli: + specifier: ^8.0.0 + version: 8.0.0 + devDependencies: + '@types/pg': + specifier: ^8.15.5 + version: 8.20.0 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@25.5.0)(typescript@5.8.3) + typescript: + specifier: ^5.8.3 + version: 5.8.3 + + apps/dataloader: + dependencies: + '@fast-csv/parse': + specifier: ^5.0.5 + version: 5.0.5 + '@metro-now/database': + specifier: workspace:* + version: link:../database + '@metro-now/shared': + specifier: workspace:* + version: link:../../lib/shared + express: + specifier: ^5.2.1 + version: 5.2.1 + node-cron: + specifier: ^4.2.1 + version: 4.2.1 + radash: + specifier: ^12.1.1 + version: 12.1.1 + unzipper: + specifier: ^0.12.3 + version: 0.12.3 + zod: + specifier: ^3.24.2 + version: 3.24.2 + devDependencies: + '@types/express': + specifier: ^5.0.6 + version: 5.0.6 + '@types/node': + specifier: ^25.5.0 + version: 25.5.0 + '@types/node-cron': + specifier: ^3.0.11 + version: 3.0.11 + '@types/unzipper': + specifier: ^0.10.11 + version: 0.10.11 + nodemon: + specifier: ^3.1.14 + version: 3.1.14 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + apps/mobile: {} apps/web: @@ -239,6 +277,31 @@ importers: specifier: ^5.8.3 version: 5.8.3 + lib/shared: + dependencies: + '@metro-now/database': + specifier: workspace:* + version: link:../../apps/database + kysely: + specifier: ^0.28.14 + version: 0.28.14 + pg: + specifier: ^8.20.0 + version: 8.20.0 + zod: + specifier: ^3.24.2 + version: 3.24.2 + devDependencies: + '@types/node': + specifier: ^25.5.0 + version: 25.5.0 + '@types/pg': + specifier: ^8.15.5 + version: 8.20.0 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + packages: '@alloc/quick-lru@5.2.0': @@ -249,8 +312,8 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - '@angular-devkit/core@19.2.6': - resolution: {integrity: sha512-WFgiYhrDMq83UNaGRAneIM7CYYdBozD+yYA9BjoU8AgBLKtrvn6S8ZcjKAk5heoHtY/u8pEb0mwDTz9gxFmJZQ==} + '@angular-devkit/core@19.2.17': + resolution: {integrity: sha512-Ah008x2RJkd0F+NLKqIpA34/vUGwjlprRCkvddjDopAWRzYn6xCkz1Tqwuhn0nR1Dy47wTLKYD999TYl5ONOAQ==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} peerDependencies: chokidar: ^4.0.0 @@ -258,13 +321,26 @@ packages: chokidar: optional: true - '@angular-devkit/schematics-cli@19.2.6': - resolution: {integrity: sha512-OCLVk1YbTWfaZwpKPnd+9A34eMAZIRjntdugGvfw21ok9dUA8gICGDhfYATSfnU8/AbVQMTPK5sgG0xhUEm3UA==} + '@angular-devkit/core@19.2.19': + resolution: {integrity: sha512-JbLL+4IMLMBgjLZlnPG4lYDfz4zGrJ/s6Aoon321NJKuw1Kb1k5KpFu9dUY0BqLIe8xPQ2UJBpI+xXdK5MXMHQ==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^4.0.0 + peerDependenciesMeta: + chokidar: + optional: true + + '@angular-devkit/schematics-cli@19.2.19': + resolution: {integrity: sha512-7q9UY6HK6sccL9F3cqGRUwKhM7b/XfD2YcVaZ2WD7VMaRlRm85v6mRjSrfKIAwxcQU0UK27kMc79NIIqaHjzxA==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} hasBin: true - '@angular-devkit/schematics@19.2.6': - resolution: {integrity: sha512-YTAxNnT++5eflx19OUHmOWu597/TbTel+QARiZCv1xQw99+X8DCKKOUXtqBRd53CAHlREDI33Rn/JLY3NYgMLQ==} + '@angular-devkit/schematics@19.2.17': + resolution: {integrity: sha512-ADfbaBsrG8mBF6Mfs+crKA/2ykB8AJI50Cv9tKmZfwcUcyAdmTr+vVvhsBCfvUAEokigSsgqgpYxfkJVxhJYeg==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + + '@angular-devkit/schematics@19.2.19': + resolution: {integrity: sha512-J4Jarr0SohdrHcb40gTL4wGPCQ952IMWF1G/MSAQfBAPvA9ZKApYhpxcY7PmehVePve+ujpus1dGsJ7dPxz8Kg==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} '@apollo/cache-control-types@1.0.3': @@ -276,8 +352,8 @@ packages: resolution: {integrity: sha512-Lahx5zntHPZia35myYDBRuF58tlwPskwHc5CWBZC/4bMKB6siTBWwtMrkqXcsNwQiFSzSx5hKdRPUmemrEp3Gg==} hasBin: true - '@apollo/server-gateway-interface@1.1.1': - resolution: {integrity: sha512-pGwCl/po6+rxRmDMFgozKQo2pbsSwE91TpsDBAOgf74CRDPXHHtM88wbwjab0wMMZh95QfR45GGyDIdhY24bkQ==} + '@apollo/server-gateway-interface@2.0.0': + resolution: {integrity: sha512-3HEMD6fSantG2My3jWkb9dvfkF9vJ4BDLRjMgsnD790VINtuPaEp+h3Hg9HOHiWkML6QsOhnaRqZ+gvhp3y8Nw==} peerDependencies: graphql: 14.x || 15.x || 16.x @@ -288,18 +364,18 @@ packages: peerDependencies: '@apollo/server': ^4.0.0 - '@apollo/server@4.11.3': - resolution: {integrity: sha512-mW8idE2q0/BN14mimfJU5DAnoPHZRrAWgwsVLBEdACds+mxapIYxIbI6AH4AsOpxfrpvHts3PCYDbopy1XPW1g==} - engines: {node: '>=14.16.0'} + '@apollo/server@5.5.0': + resolution: {integrity: sha512-vWtodBOK/SZwBTJzItECOmLfL8E8pn/IdvP7pnxN5g2tny9iW4+9sxdajE798wV1H2+PYp/rRcl/soSHIBKMPw==} + engines: {node: '>=20'} peerDependencies: - graphql: ^16.6.0 + graphql: ^16.11.0 '@apollo/usage-reporting-protobuf@4.1.1': resolution: {integrity: sha512-u40dIUePHaSKVshcedO7Wp+mPiZsaU6xjv9J+VyxpoU/zL6Jle+9zWeG98tr/+SZ0nZ4OXhrbb8SNr0rAPpIDA==} - '@apollo/utils.createhash@2.0.2': - resolution: {integrity: sha512-UkS3xqnVFLZ3JFpEmU/2cM2iKJotQXMoSTgxXsfQgXLC5gR1WaepoXagmYnPSA7Q/2cmnyTYK5OgAgoC4RULPg==} - engines: {node: '>=14'} + '@apollo/utils.createhash@3.0.1': + resolution: {integrity: sha512-CKrlySj4eQYftBE5MJ8IzKwIibQnftDT7yGfsJy5KSEEnLlPASX0UTpbKqkjlVEwPPd4mEwI7WOM7XNxEuO05A==} + engines: {node: '>=16'} '@apollo/utils.dropunuseddefinitions@2.0.1': resolution: {integrity: sha512-EsPIBqsSt2BwDsv8Wu76LK5R1KtsVkNoO4b0M5aK0hx+dGg9xJXuqlr7Fo34Dl+y83jmzn+UvEW+t1/GP2melA==} @@ -307,21 +383,21 @@ packages: peerDependencies: graphql: 14.x || 15.x || 16.x - '@apollo/utils.fetcher@2.0.1': - resolution: {integrity: sha512-jvvon885hEyWXd4H6zpWeN3tl88QcWnHp5gWF5OPF34uhvoR+DFqcNxs9vrRaBBSY3qda3Qe0bdud7tz2zGx1A==} - engines: {node: '>=14'} + '@apollo/utils.fetcher@3.1.0': + resolution: {integrity: sha512-Z3QAyrsQkvrdTuHAFwWDNd+0l50guwoQUoaDQssLOjkmnmVuvXlJykqlEJolio+4rFwBnWdoY1ByFdKaQEcm7A==} + engines: {node: '>=16'} - '@apollo/utils.isnodelike@2.0.1': - resolution: {integrity: sha512-w41XyepR+jBEuVpoRM715N2ZD0xMD413UiJx8w5xnAZD2ZkSJnMJBoIzauK83kJpSgNuR6ywbV29jG9NmxjK0Q==} - engines: {node: '>=14'} + '@apollo/utils.isnodelike@3.0.0': + resolution: {integrity: sha512-xrjyjfkzunZ0DeF6xkHaK5IKR8F1FBq6qV+uZ+h9worIF/2YSzA0uoBxGv6tbTeo9QoIQnRW4PVFzGix5E7n/g==} + engines: {node: '>=16'} - '@apollo/utils.keyvaluecache@2.1.1': - resolution: {integrity: sha512-qVo5PvUUMD8oB9oYvq4ViCjYAMWnZ5zZwEjNF37L2m1u528x5mueMlU+Cr1UinupCgdB78g+egA1G98rbJ03Vw==} - engines: {node: '>=14'} + '@apollo/utils.keyvaluecache@4.0.0': + resolution: {integrity: sha512-mKw1myRUkQsGPNB+9bglAuhviodJ2L2MRYLTafCMw5BIo7nbvCPNCkLnIHjZ1NOzH7SnMAr5c9LmXiqsgYqLZw==} + engines: {node: '>=20'} - '@apollo/utils.logger@2.0.1': - resolution: {integrity: sha512-YuplwLHaHf1oviidB7MxnCXAdHp3IqYV8n0momZ3JfLniae92eYqMIx+j5qJFX6WKJPs6q7bczmV4lXIsTu5Pg==} - engines: {node: '>=14'} + '@apollo/utils.logger@3.0.0': + resolution: {integrity: sha512-M8V8JOTH0F2qEi+ktPfw4RL7MvUycDfKp7aEap2eWXfL5SqWHN6jTLbj5f5fj1cceHpyaUSOZlvlaaryaxZAmg==} + engines: {node: '>=16'} '@apollo/utils.printwithreducedwhitespace@2.0.1': resolution: {integrity: sha512-9M4LUXV/fQBh8vZWlLvb/HyyhjJ77/I5ZKu+NBWV/BmYGyRmoEP9EVAy7LCVoY3t8BDcyCAGfxJaLFCSuQkPUg==} @@ -353,13 +429,20 @@ packages: peerDependencies: graphql: 14.x || 15.x || 16.x - '@apollo/utils.withrequired@2.0.1': - resolution: {integrity: sha512-YBDiuAX9i1lLc6GeTy1m7DGLFn/gMnvXqlalOIMjM7DeOgIacEjjfwPqb0M1CQ2v11HhR15d1NmxJoRCfrNqcA==} - engines: {node: '>=14'} + '@apollo/utils.withrequired@3.0.0': + resolution: {integrity: sha512-aaxeavfJ+RHboh7c2ofO5HHtQobGX4AgUujXP4CXpREHp9fQ9jPi6K9T1jrAKe7HIipoP0OJ1gd6JamSkFIpvA==} + engines: {node: '>=16'} '@apollographql/graphql-playground-html@1.6.29': resolution: {integrity: sha512-xCcXpoz52rI4ksJSdOCxeOCn2DLocxwHf9dVT/Q90Pte1LX+LY+91SFtJF3KXVHH8kEin+g1KKCQPKBjZJfWNA==} + '@as-integrations/express5@1.1.2': + resolution: {integrity: sha512-BxfwtcWNf2CELDkuPQxi5Zl3WqY/dQVJYafeCBOGoFQjv5M0fjhxmAFZ9vKx/5YKKNeok4UY6PkFbHzmQrdxIA==} + engines: {node: '>=20'} + peerDependencies: + '@apollo/server': ^4.0.0 || ^5.0.0 + express: ^5.0.0 + '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -525,9 +608,71 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@biomejs/biome@1.9.4': + resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@1.9.4': + resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@1.9.4': + resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@1.9.4': + resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@1.9.4': + resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@1.9.4': + resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@1.9.4': + resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@1.9.4': + resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@1.9.4': + resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@borewit/text-codec@0.2.2': + resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} + '@braintree/sanitize-url@6.0.4': resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==} + '@cacheable/memory@2.0.8': + resolution: {integrity: sha512-FvEb29x5wVwu/Kf93IWwsOOEuhHh6dYCJF3vcKLzXc0KXIW181AOzv6ceT4ZpBHDvAfG60eqb+ekmrnLHIy+jw==} + + '@cacheable/utils@2.4.1': + resolution: {integrity: sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==} + '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -563,16 +708,17 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@fast-csv/parse@5.0.2': - resolution: {integrity: sha512-gMu1Btmm99TP+wc0tZnlH30E/F1Gw1Tah3oMDBHNPe9W8S68ixVHjt89Wg5lh7d9RuQMtwN+sGl5kxR891+fzw==} + '@fast-csv/parse@5.0.5': + resolution: {integrity: sha512-M0IbaXZDbxfOnpVE5Kps/a6FGlILLhtLsvWd9qNH3d2TxNnpbNkFf3KD26OmJX6MHq7PdQAl5htStDwnuwHx6w==} - '@graphql-tools/merge@8.4.2': - resolution: {integrity: sha512-XbrHAaj8yDuINph+sAfuq3QCZ/tKblrTLOpirK0+CAgNlZUCHs0Fa+xtMUURgwCVThLle1AF7svJCxFizygLsw==} + '@graphql-tools/merge@9.0.24': + resolution: {integrity: sha512-NzWx/Afl/1qHT3Nm1bghGG2l4jub28AdvtG11PoUlmjcIjnFBJMv4vqL0qnxWe8A82peWo4/TkVdjJRLXwgGEw==} + engines: {node: '>=16.0.0'} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - '@graphql-tools/merge@9.0.24': - resolution: {integrity: sha512-NzWx/Afl/1qHT3Nm1bghGG2l4jub28AdvtG11PoUlmjcIjnFBJMv4vqL0qnxWe8A82peWo4/TkVdjJRLXwgGEw==} + '@graphql-tools/merge@9.1.7': + resolution: {integrity: sha512-Y5E1vTbTabvcXbkakdFUt4zUIzB1fyaEnVmIWN0l0GMed2gdD01TpZWLUm4RNAxpturvolrb24oGLQrBbPLSoQ==} engines: {node: '>=16.0.0'} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 @@ -583,8 +729,9 @@ packages: peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - '@graphql-tools/schema@9.0.19': - resolution: {integrity: sha512-oBRPoNBtCkk0zbUsyP4GaIzCt8C0aCI4ycIRUL67KK5pOHljKLBBtGT+Jr6hkzA74C8Gco8bpZPe7aWFjiaK2w==} + '@graphql-tools/schema@10.0.31': + resolution: {integrity: sha512-ZewRgWhXef6weZ0WiP7/MV47HXiuFbFpiDUVLQl6mgXsWSsGELKFxQsyUCBos60Qqy1JEFAIu3Ns6GGYjGkqkQ==} + engines: {node: '>=16.0.0'} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 @@ -594,8 +741,9 @@ packages: peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - '@graphql-tools/utils@9.2.1': - resolution: {integrity: sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==} + '@graphql-tools/utils@11.0.0': + resolution: {integrity: sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==} + engines: {node: '>=16.0.0'} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 @@ -734,6 +882,10 @@ packages: cpu: [x64] os: [win32] + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + '@inquirer/checkbox@4.1.5': resolution: {integrity: sha512-swPczVU+at65xa5uPfNP9u3qx/alNwiaykiI/ExpsmMSQW55trmZcwhYWzw/7fj+n6Q8z1eENvR7vFfq9oPSAQ==} engines: {node: '>=18'} @@ -743,6 +895,24 @@ packages: '@types/node': optional: true + '@inquirer/checkbox@4.3.2': + resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/confirm@5.1.9': resolution: {integrity: sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w==} engines: {node: '>=18'} @@ -761,6 +931,15 @@ packages: '@types/node': optional: true + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/editor@4.2.10': resolution: {integrity: sha512-5GVWJ+qeI6BzR6TIInLP9SXhWCEcvgFQYmcRG6d6RIlhFjM5TyG18paTGBgRYyEouvCmzeco47x9zX9tQEofkw==} engines: {node: '>=18'} @@ -770,6 +949,15 @@ packages: '@types/node': optional: true + '@inquirer/editor@4.2.23': + resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/expand@4.0.12': resolution: {integrity: sha512-jV8QoZE1fC0vPe6TnsOfig+qwu7Iza1pkXoUJ3SroRagrt2hxiL+RbM432YAihNR7m7XnU0HWl/WQ35RIGmXHw==} engines: {node: '>=18'} @@ -779,10 +967,32 @@ packages: '@types/node': optional: true + '@inquirer/expand@4.0.23': + resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/figures@1.0.11': resolution: {integrity: sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==} engines: {node: '>=18'} + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + '@inquirer/input@4.1.9': resolution: {integrity: sha512-mshNG24Ij5KqsQtOZMgj5TwEjIf+F2HOESk6bjMwGWgcH5UBe8UoljwzNFHqdMbGYbgAf6v2wU/X9CAdKJzgOA==} engines: {node: '>=18'} @@ -792,6 +1002,15 @@ packages: '@types/node': optional: true + '@inquirer/input@4.3.1': + resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/number@3.0.12': resolution: {integrity: sha512-7HRFHxbPCA4e4jMxTQglHJwP+v/kpFsCf2szzfBHy98Wlc3L08HL76UDiA87TOdX5fwj2HMOLWqRWv9Pnn+Z5Q==} engines: {node: '>=18'} @@ -801,6 +1020,15 @@ packages: '@types/node': optional: true + '@inquirer/number@3.0.23': + resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/password@4.0.12': resolution: {integrity: sha512-FlOB0zvuELPEbnBYiPaOdJIaDzb2PmJ7ghi/SVwIHDDSQ2K4opGBkF+5kXOg6ucrtSUQdLhVVY5tycH0j0l+0g==} engines: {node: '>=18'} @@ -810,8 +1038,8 @@ packages: '@types/node': optional: true - '@inquirer/prompts@7.3.2': - resolution: {integrity: sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==} + '@inquirer/password@4.0.23': + resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -819,8 +1047,17 @@ packages: '@types/node': optional: true - '@inquirer/prompts@7.4.1': - resolution: {integrity: sha512-UlmM5FVOZF0gpoe1PT/jN4vk8JmpIWBlMvTL8M+hlvPmzN89K6z03+IFmyeu/oFCenwdwHDr2gky7nIGSEVvlA==} + '@inquirer/prompts@7.10.1': + resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.3.2': + resolution: {integrity: sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -837,6 +1074,15 @@ packages: '@types/node': optional: true + '@inquirer/rawlist@4.1.11': + resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/search@3.0.12': resolution: {integrity: sha512-H/kDJA3kNlnNIjB8YsaXoQI0Qccgf0Na14K1h8ExWhNmUg2E941dyFPrZeugihEa9AZNW5NdsD/NcvUME83OPQ==} engines: {node: '>=18'} @@ -846,6 +1092,15 @@ packages: '@types/node': optional: true + '@inquirer/search@3.2.2': + resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/select@4.1.1': resolution: {integrity: sha512-IUXzzTKVdiVNMA+2yUvPxWsSgOG4kfX93jOM4Zb5FgujeInotv5SPIJVeXQ+fO4xu7tW8VowFhdG5JRmmCyQ1Q==} engines: {node: '>=18'} @@ -855,6 +1110,24 @@ packages: '@types/node': optional: true + '@inquirer/select@4.4.2': + resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/type@3.0.6': resolution: {integrity: sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==} engines: {node: '>=18'} @@ -966,12 +1239,20 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@keyv/redis@4.3.3': - resolution: {integrity: sha512-J/uhvKu/Qfh11yMUs+9KdcGCLmWFd3vMxtDVQh2j9cOcnrpnM5jE1xU+K1/kI89czSVEdeMyqTC9gGNtwi3JEQ==} + '@keyv/bigmap@1.3.1': + resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==} + engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.6.0 + + '@keyv/redis@5.1.6': + resolution: {integrity: sha512-eKvW6pspvVaU5dxigaIDZr635/Uw6urTXL3gNbY9WTR8d3QigZQT+r8gxYSEOsw4+1cCBsC4s7T2ptR0WC9LfQ==} engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.6.0 - '@keyv/serialize@1.0.3': - resolution: {integrity: sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g==} + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} '@lukeed/csprng@1.1.0': resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} @@ -1012,8 +1293,8 @@ packages: peerDependencies: react: '>=16' - '@microsoft/tsdoc@0.15.1': - resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@microsoft/tsdoc@0.16.0': + resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} '@napi-rs/simple-git-android-arm-eabi@0.1.19': resolution: {integrity: sha512-XryEH/hadZ4Duk/HS/HC/cA1j0RHmqUGey3MsCf65ZS0VrWMqChXM/xlTPWuY5jfCc/rPubHaqI7DZlbexnX/g==} @@ -1106,13 +1387,14 @@ packages: '@napi-rs/wasm-runtime@0.2.8': resolution: {integrity: sha512-OBlgKdX7gin7OIq4fadsjpg+cp2ZphvAIKucHsNfTdJiqdOmOEwQd/bHi0VwNrcw5xpBJyUw6cK/QilCqy1BSg==} - '@nestjs/apollo@13.0.4': - resolution: {integrity: sha512-BlMZcnqpoxM2oCgsyBkOb3BLDVW1Ym3um5n1mGcF9Mu3vXVuD0mGW1bUMHe3TXViNve8D6bCvGgxwl6dayQL2w==} + '@nestjs/apollo@13.2.4': + resolution: {integrity: sha512-5gKyDQDm+E+AIYofFxXXUhi/4z9YgSjXtjlXUixCj+n6oVcJOilQ7zxlnu3vHiBZz8kEn2ZvVZTJ+i5V2xQ+ew==} peerDependencies: '@apollo/gateway': ^2.0.0 - '@apollo/server': ^4.11.3 + '@apollo/server': ^5.0.0 '@apollo/subgraph': ^2.0.0 - '@as-integrations/fastify': ^2.1.1 + '@as-integrations/express5': '*' + '@as-integrations/fastify': ^2.1.1 || ^3.0.0 '@nestjs/common': ^11.0.1 '@nestjs/core': ^11.0.1 '@nestjs/graphql': ^13.0.0 @@ -1122,11 +1404,13 @@ packages: optional: true '@apollo/subgraph': optional: true + '@as-integrations/express5': + optional: true '@as-integrations/fastify': optional: true - '@nestjs/cache-manager@3.0.1': - resolution: {integrity: sha512-4UxTnR0fsmKL5YDalU2eLFVnL+OBebWUpX+hEduKGncrVKH4PPNoiRn1kXyOCjmzb0UvWgqubpssNouc8e0MCw==} + '@nestjs/cache-manager@3.1.0': + resolution: {integrity: sha512-pEIqYZrBcE8UdkJmZRduurvoUfdU+3kRPeO1R2muiMbZnRuqlki5klFFNllO9LyYWzrx98bd1j0PSPKSJk1Wbw==} peerDependencies: '@nestjs/common': ^9.0.0 || ^10.0.0 || ^11.0.0 '@nestjs/core': ^9.0.0 || ^10.0.0 || ^11.0.0 @@ -1134,12 +1418,12 @@ packages: keyv: '>=5' rxjs: ^7.8.1 - '@nestjs/cli@11.0.6': - resolution: {integrity: sha512-Xco8pTdWHCpTXPTYMkUGAE+C7JXvAv38oVUaQeL81o7UOAi39w8p456r+IjONN/7ekjzakWnqepDzuTtH5Xk5w==} + '@nestjs/cli@11.0.16': + resolution: {integrity: sha512-P0H+Vcjki6P5160E5QnMt3Q0X5FTg4PZkP99Ig4lm/4JWqfw32j3EXv3YBTJ2DmxLwOQ/IS9F7dzKpMAgzKTGg==} engines: {node: '>= 20.11'} hasBin: true peerDependencies: - '@swc/cli': ^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 + '@swc/cli': ^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 '@swc/core': ^1.3.62 peerDependenciesMeta: '@swc/cli': @@ -1147,11 +1431,11 @@ packages: '@swc/core': optional: true - '@nestjs/common@11.0.13': - resolution: {integrity: sha512-cXqXJPQTcJIYqT8GtBYqjYY9sklCBqp/rh9z1R40E60gWnsU598YIQWkojSFRI9G7lT/+uF+jqSrg/CMPBk7QQ==} + '@nestjs/common@11.1.17': + resolution: {integrity: sha512-hLODw5Abp8OQgA+mUO4tHou4krKgDtUcM9j5Ihxncst9XeyxYBTt2bwZm4e4EQr5E352S4Fyy6V3iFx9ggxKAg==} peerDependencies: - class-transformer: '*' - class-validator: '*' + class-transformer: '>=0.4.1' + class-validator: '>=0.13.2' reflect-metadata: ^0.1.12 || ^0.2.0 rxjs: ^7.1.0 peerDependenciesMeta: @@ -1160,14 +1444,14 @@ packages: class-validator: optional: true - '@nestjs/config@4.0.2': - resolution: {integrity: sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==} + '@nestjs/config@4.0.3': + resolution: {integrity: sha512-FQ3M3Ohqfl+nHAn5tp7++wUQw0f2nAk+SFKe8EpNRnIifPqvfJP6JQxPKtFLMOHbyer4X646prFG4zSRYEssQQ==} peerDependencies: '@nestjs/common': ^10.0.0 || ^11.0.0 rxjs: ^7.1.0 - '@nestjs/core@11.0.13': - resolution: {integrity: sha512-1xjrsYjff4sg4MfvF+/NInOq+7oI1D1vK8Yj9wkrbBH1dM+h2At71tccbFfl/eJUt4ckZlH+XmROnt/T0daYcA==} + '@nestjs/core@11.1.17': + resolution: {integrity: sha512-lD5mAYekTTurF3vDaa8C2OKPnjiz4tsfxIc5XlcSUzOhkwWf6Ay3HKvt6FmvuWQam6uIIHX52Clg+e6tAvf/cg==} engines: {node: '>= 20'} peerDependencies: '@nestjs/common': ^11.0.0 @@ -1184,17 +1468,17 @@ packages: '@nestjs/websockets': optional: true - '@nestjs/graphql@13.0.4': - resolution: {integrity: sha512-TEWFl9MCbut7A8k/BvrR/hWD8wlvUUxp4mzxUhbfyBef28Zwy6trlhcGpDoM2ENIb7HShWcro4CKNwXwj/YWmA==} + '@nestjs/graphql@13.2.4': + resolution: {integrity: sha512-UXtsY4o1gsSG8tlY2HI3NuWyW15eOSG7IX1nG92T5/VtsEsafhtYOnc0H9Z7zFzUn0tPBCcRte1NgVNEIgAAdw==} peerDependencies: '@apollo/subgraph': ^2.9.3 '@nestjs/common': ^11.0.1 '@nestjs/core': ^11.0.1 class-transformer: '*' class-validator: '*' - graphql: ^16.10.0 + graphql: ^16.11.0 reflect-metadata: ^0.1.13 || ^0.2.0 - ts-morph: ^20.0.0 || ^21.0.0 || ^24.0.0 || ^25.0.0 + ts-morph: ^20.0.0 || ^21.0.0 || ^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 peerDependenciesMeta: '@apollo/subgraph': optional: true @@ -1218,27 +1502,21 @@ packages: class-validator: optional: true - '@nestjs/platform-express@11.0.13': - resolution: {integrity: sha512-SaxfIDORX1oV8T6nxr/pltnW2g+3fCRPs5YwO0jBj2d8sC03Axjwlxp/ASg2mf6xvOSBD6ZbhjVLVVDZymyFXQ==} + '@nestjs/platform-express@11.1.17': + resolution: {integrity: sha512-mAf4eOsSBsTOn/VbrUO1gsjW6dVh91qqXPMXun4dN8SnNjf7PTQagM9o8d6ab8ZBpNe6UdZftdrZoDetU+n4Qg==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 - '@nestjs/schedule@5.0.1': - resolution: {integrity: sha512-kFoel84I4RyS2LNPH6yHYTKxB16tb3auAEciFuc788C3ph6nABkUfzX5IE+unjVaRX+3GuruJwurNepMlHXpQg==} - peerDependencies: - '@nestjs/common': ^10.0.0 || ^11.0.0 - '@nestjs/core': ^10.0.0 || ^11.0.0 - - '@nestjs/schematics@11.0.4': - resolution: {integrity: sha512-DSAdkfEgKsy54eB+iMwalod+dWX3cMNG1xp9QiSGC5GISck/9pJ8Y9/dnYXvC7s2DAwkwL+Jsywi8gXMVl3OGg==} + '@nestjs/schematics@11.0.9': + resolution: {integrity: sha512-0NfPbPlEaGwIT8/TCThxLzrlz3yzDNkfRNpbL7FiplKq3w4qXpJg0JYwqgMEJnLQZm3L/L/5XjoyfJHUO3qX9g==} peerDependencies: typescript: '>=4.8.2' - '@nestjs/swagger@11.1.1': - resolution: {integrity: sha512-k7jEiocSQ5bL6RSnEjQ1h4uT4fErgshWQIhaVjyvufIEyBfH0Fv0Q2lihH2QLqeDjBkrH5bW0Twbqf3SlLOwCw==} + '@nestjs/swagger@11.2.6': + resolution: {integrity: sha512-oiXOxMQqDFyv1AKAqFzSo6JPvMEs4uA36Eyz/s2aloZLxUjcLfUMELSLSNQunr61xCPTpwEOShfmO7NIufKXdA==} peerDependencies: - '@fastify/static': ^8.0.0 + '@fastify/static': ^8.0.0 || ^9.0.0 '@nestjs/common': ^11.0.1 '@nestjs/core': ^11.0.1 class-transformer: '*' @@ -1252,8 +1530,8 @@ packages: class-validator: optional: true - '@nestjs/testing@11.0.13': - resolution: {integrity: sha512-9E9HxD3EmiQky+pqYvpV0cHKlxYJJqHm2GmXoKHF72Raa0JTfQpamnLl6TPjDy2XOqA7oSSBDnEwku8vZ46Cdw==} + '@nestjs/testing@11.1.17': + resolution: {integrity: sha512-lNffw+z+2USewmw4W0tsK+Rq94A2N4PiHbcqoRUu5y8fnqxQeIWGHhjo5BFCqj7eivqJBhT7WdRydxVq4rAHzg==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 @@ -1336,6 +1614,10 @@ packages: cpu: [x64] os: [win32] + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1357,41 +1639,16 @@ packages: engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'} hasBin: true + '@paralleldrive/cuid2@2.3.1': + resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@pkgr/core@0.2.0': - resolution: {integrity: sha512-vsJDAkYR6qCPu+ioGScGiMYR7LvZYIXh/dlQeviqoTWNCVfKTLYD/LkNWH4Mxsv2a5vpIRc77FN5DnmK1eBggQ==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - '@prisma/client@5.20.0': - resolution: {integrity: sha512-CLv55ZuMuUawMsxoqxGtLT3bEZoa2W8L3Qnp6rDIFWy+ZBrUcOFKdoeGPSnbBqxc3SkdxJrF+D1veN/WNynZYA==} - engines: {node: '>=16.13'} - peerDependencies: - prisma: '*' - peerDependenciesMeta: - prisma: - optional: true - - '@prisma/debug@5.22.0': - resolution: {integrity: sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==} - - '@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2': - resolution: {integrity: sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==} - - '@prisma/engines@5.22.0': - resolution: {integrity: sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==} - - '@prisma/fetch-engine@5.22.0': - resolution: {integrity: sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==} - - '@prisma/get-platform@5.22.0': - resolution: {integrity: sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==} - '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -1422,34 +1679,14 @@ packages: '@protobufjs/utf8@1.1.0': resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} - '@redis/bloom@1.2.0': - resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@redis/client@1.6.0': - resolution: {integrity: sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==} - engines: {node: '>=14'} - - '@redis/graph@1.1.1': - resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@redis/json@1.0.7': - resolution: {integrity: sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@redis/search@1.2.0': - resolution: {integrity: sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@redis/time-series@1.1.0': - resolution: {integrity: sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==} + '@redis/client@5.11.0': + resolution: {integrity: sha512-GHoprlNQD51Xq2Ztd94HHV94MdFZQ3CVrpA04Fz8MVoHM0B7SlbmPEVIjwTbcv58z8QyjnrOuikS0rWF03k5dQ==} + engines: {node: '>= 18'} peerDependencies: - '@redis/client': ^1.0.0 + '@node-rs/xxhash': ^1.1.0 + peerDependenciesMeta: + '@node-rs/xxhash': + optional: true '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -1492,6 +1729,13 @@ packages: '@theguild/remark-npm2yarn@0.2.1': resolution: {integrity: sha512-jUTFWwDxtLEFtGZh/TW/w30ySaDJ8atKWH8dq2/IiQF61dPrGfETpl0WxD0VdBfuLOeU14/kop466oBSRO/5CA==} + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} + engines: {node: '>=18'} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@ts-morph/common@0.26.1': resolution: {integrity: sha512-Sn28TGl/4cFpcM+jwsH1wLncYq3FtN/BIpem+HOygfBWPT5pAeS5dB4VFVzV8FbnOKHpDLZmvAl4AjPEev5idA==} @@ -1558,17 +1802,14 @@ packages: '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} - '@types/express-serve-static-core@4.19.6': - resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} + '@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@4.17.21': - resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} - - '@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/geojson-vt@3.2.5': resolution: {integrity: sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==} @@ -1615,9 +1856,6 @@ packages: '@types/long@4.0.2': resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} - '@types/luxon@3.4.2': - resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} - '@types/mapbox-gl@3.4.1': resolution: {integrity: sha512-NsGKKtgW93B+UaLPti6B7NwlxYlES5DpV5Gzj9F75rK5ALKsqSk15CiEHbOnTr09RGbr6ZYiCdI+59NNNcAImg==} @@ -1645,8 +1883,8 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node-fetch@2.6.12': - resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} + '@types/node-cron@3.0.11': + resolution: {integrity: sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==} '@types/node@20.17.30': resolution: {integrity: sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==} @@ -1654,9 +1892,15 @@ packages: '@types/node@22.14.0': resolution: {integrity: sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==} + '@types/node@25.5.0': + resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} + '@types/pbf@3.0.5': resolution: {integrity: sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==} + '@types/pg@8.20.0': + resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + '@types/prop-types@15.7.14': resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} @@ -1674,14 +1918,11 @@ packages: '@types/react@18.3.20': resolution: {integrity: sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==} - '@types/semver@7.7.0': - resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==} - '@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==} @@ -1710,17 +1951,6 @@ packages: '@types/yargs@17.0.33': resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} - '@typescript-eslint/eslint-plugin@6.21.0': - resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - '@typescript-eslint/parser@6.21.0': resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1735,16 +1965,6 @@ packages: resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} engines: {node: ^16.0.0 || >=18.0.0} - '@typescript-eslint/type-utils@6.21.0': - resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - '@typescript-eslint/types@6.21.0': resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1758,12 +1978,6 @@ packages: typescript: optional: true - '@typescript-eslint/utils@6.21.0': - resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - '@typescript-eslint/visitor-keys@6.21.0': resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1901,14 +2115,16 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} - engines: {node: '>= 0.6'} - accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + acorn-import-phases@1.0.4: + resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} + engines: {node: '>=10.13.0'} + peerDependencies: + acorn: ^8.14.0 + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1923,6 +2139,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -1990,8 +2211,8 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} - ansis@3.17.0: - resolution: {integrity: sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==} + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} engines: {node: '>=14'} any-promise@1.3.0: @@ -2034,13 +2255,14 @@ packages: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} - array-flatten@1.1.1: - resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - array-includes@3.1.8: resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} engines: {node: '>= 0.4'} + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} @@ -2093,9 +2315,6 @@ packages: async-retry@1.3.3: resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} - async@3.2.6: - resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -2145,9 +2364,18 @@ 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} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.10.12: + resolution: {integrity: sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==} + engines: {node: '>=6.0.0'} + hasBin: true + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -2158,12 +2386,8 @@ packages: bluebird@3.7.2: resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} - body-parser@1.20.3: - resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - - 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: @@ -2172,6 +2396,10 @@ packages: brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -2181,6 +2409,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + bs-logger@0.2.6: resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} engines: {node: '>= 6'} @@ -2194,9 +2427,6 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - buffer@6.0.3: - resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -2211,8 +2441,11 @@ packages: bytewise@1.1.0: resolution: {integrity: sha512-rHuuseJ9iQ0na6UDhnrRVDh8YnWVlU6xM3VH6q/+yHDeUH2zIhUzP+2/h3LIrhLDBtTqzWpE3p3tP/boefskKQ==} - cache-manager@6.4.2: - resolution: {integrity: sha512-oT0d1cGWZAlqEGDPjOfhmldTS767jT6kBT3KIdn7MX5OevlRVYqJT+LxRv5WY4xW9heJtYxeRRXaoKlEW+Biew==} + cache-manager@7.2.8: + resolution: {integrity: sha512-0HDaDLBBY/maa/LmUVAr70XUOwsiQD+jyzCBjmUErYZUKdMS9dT59PqW59PpVqfGM7ve6H0J6307JTpkCYefHQ==} + + cacheable@2.3.4: + resolution: {integrity: sha512-djgxybDbw9fL/ZWMI3+CE8ZilNxcwFkVtDc1gJ+IlOSSWkSMPQabhV/XCHTQ6pwwN6aivXPZ43omTooZiX06Ew==} call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} @@ -2245,6 +2478,9 @@ packages: caniuse-lite@1.0.30001711: resolution: {integrity: sha512-OpFA8GsKtoV3lCcsI3U5XBAV+oVrMu96OS8XafKqnhOaEAW2mveD1Mx81Sx/02chERwhDakuXs28zbyEc4QMKg==} + caniuse-lite@1.0.30001781: + resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -2275,6 +2511,9 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + cheap-ruler@4.0.0: resolution: {integrity: sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw==} @@ -2388,8 +2627,8 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} - comment-json@4.2.5: - resolution: {integrity: sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==} + comment-json@4.4.1: + resolution: {integrity: sha512-r1To31BQD5060QdkC+Iheai7gHwoSZobzunqkf2/kQ6xIAfJyrKNAFUwdKvkK7Qgu7pVTKQEa7ok7Ed3ycAJgg==} engines: {node: '>= 6'} component-emitter@1.3.1: @@ -2401,18 +2640,14 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - concat-stream@1.6.2: - resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} - engines: {'0': node >= 0.8} + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} consola@3.4.2: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} - content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} - engines: {node: '>= 0.6'} - content-disposition@1.0.0: resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} engines: {node: '>= 0.6'} @@ -2424,9 +2659,6 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-signature@1.0.6: - resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} - cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -2445,6 +2677,10 @@ packages: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + cose-base@1.0.3: resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} @@ -2465,9 +2701,6 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - cron@3.5.0: - resolution: {integrity: sha512-0eYZqCnapmxYcV06uktql93wNWdlTmmBFP2iYz+JPVcQqlyFYcn1lFuIk4R54pkOmE7mcldTAPZv6X5XA4Q46A==} - cross-inspect@1.0.1: resolution: {integrity: sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==} engines: {node: '>=16.0.0'} @@ -2665,14 +2898,6 @@ packages: dayjs@1.11.13: resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} - debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -2681,8 +2906,8 @@ packages: supports-color: optional: true - debug@4.3.7: - resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -2690,8 +2915,8 @@ packages: supports-color: optional: true - debug@4.4.0: - resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -2743,10 +2968,6 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} - destroy@1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - detect-libc@2.0.3: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} @@ -2802,14 +3023,18 @@ packages: resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} engines: {node: '>=12'} - dotenv-expand@12.0.1: - resolution: {integrity: sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==} + dotenv-expand@12.0.3: + resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} engines: {node: '>=12'} dotenv@16.4.7: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + dset@3.1.4: resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} engines: {node: '>=4'} @@ -2830,14 +3055,12 @@ 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 - electron-to-chromium@1.5.132: resolution: {integrity: sha512-QgX9EBvWGmvSRa74zqfnG7+Eno0Ak0vftBll0Pt2/z5b3bEGYL6OUXLgKPtvx73dn3dvwrlyVkjPKRRlhLYTEg==} + electron-to-chromium@1.5.328: + resolution: {integrity: sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w==} + elkjs@0.9.3: resolution: {integrity: sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==} @@ -2851,10 +3074,6 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - encodeurl@1.0.2: - resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} - engines: {node: '>= 0.8'} - encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -2874,6 +3093,10 @@ packages: resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} engines: {node: '>= 0.4'} + es-abstract@1.24.1: + resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} + engines: {node: '>= 0.4'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -2886,8 +3109,8 @@ packages: resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} engines: {node: '>= 0.4'} - es-module-lexer@1.6.0: - resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} @@ -2937,12 +3160,6 @@ packages: typescript: optional: true - eslint-config-prettier@9.1.0: - resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - eslint-import-resolver-node@0.3.9: resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} @@ -2959,8 +3176,8 @@ packages: eslint-plugin-import-x: optional: true - eslint-module-utils@2.12.0: - resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' @@ -2980,8 +3197,8 @@ packages: eslint-import-resolver-webpack: optional: true - eslint-plugin-import@2.31.0: - resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==} + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' @@ -2996,20 +3213,6 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - eslint-plugin-prettier@5.2.6: - resolution: {integrity: sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - '@types/eslint': '>=8.0.0' - eslint: '>=8.0.0' - eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' - prettier: '>=3.0.0' - peerDependenciesMeta: - '@types/eslint': - optional: true - eslint-config-prettier: - optional: true - eslint-plugin-react-hooks@5.0.0-canary-7118f5dd7-20230705: resolution: {integrity: sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw==} engines: {node: '>=10'} @@ -3118,12 +3321,8 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - express@4.21.2: - resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} - engines: {node: '>= 0.10.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'} extend-shallow@2.0.1: @@ -3144,9 +3343,6 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-diff@1.3.0: - resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -3181,17 +3377,14 @@ packages: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} - filelist@1.0.4: - resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + file-type@21.3.2: + resolution: {integrity: sha512-DLkUvGwep3poOV2wpzbHCOnSKGk1LzyXTv+aHFgN2VFl96wnp8YA9YjO2qPzg5PuL8q/SW9Pdi6WTkYOIh995w==} + engines: {node: '>=20'} fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - finalhandler@1.3.1: - resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} - engines: {node: '>= 0.8'} - finalhandler@2.1.0: resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} engines: {node: '>= 0.8'} @@ -3240,8 +3433,13 @@ packages: 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==} @@ -3275,10 +3473,6 @@ packages: react-dom: optional: true - fresh@0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} - fresh@2.0.0: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} @@ -3312,10 +3506,6 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - generic-pool@3.9.0: - resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} - engines: {node: '>= 4'} - gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -3384,20 +3574,24 @@ packages: glob@10.3.10: resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} engines: {node: '>=16 || 14 >=14.17'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true - glob@11.0.1: - resolution: {integrity: sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==} + glob@13.0.0: + resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} engines: {node: 20 || >=22} - hasBin: true + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} @@ -3415,10 +3609,6 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} - gopd@1.1.0: - resolution: {integrity: sha512-FQoVQnqcdk4hVM4JN1eromaun4iuS34oStkdlLENLdpULsuQcTyXj8w7ayhuUfPwEYZ1ZOooOTT6fdA9Vmx/RA==} - engines: {node: '>= 0.4'} - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -3435,24 +3625,24 @@ packages: peerDependencies: graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - graphql-ws@6.0.4: - resolution: {integrity: sha512-8b4OZtNOvv8+NZva8HXamrc0y1jluYC0+13gdh7198FKjVzXyTvVc95DCwGzaKEfn3YuWZxUqjJlHe3qKM/F2g==} + graphql-ws@6.0.7: + resolution: {integrity: sha512-yoLRW+KRlDmnnROdAu7sX77VNLC0bsFoZyGQJLy1cF+X/SkLg/fWkRGrEEYQK8o2cafJ2wmEaMqMEZB3U3DYDg==} engines: {node: '>=20'} peerDependencies: '@fastify/websocket': ^10 || ^11 + crossws: ~0.3 graphql: ^15.10.1 || ^16 - uWebSockets.js: ^20 ws: ^8 peerDependenciesMeta: '@fastify/websocket': optional: true - uWebSockets.js: + crossws: optional: true ws: optional: true - graphql@16.10.0: - resolution: {integrity: sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==} + graphql@16.13.2: + resolution: {integrity: sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} gray-matter@4.0.3: @@ -3462,6 +3652,11 @@ packages: grid-index@1.1.0: resolution: {integrity: sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==} + handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} + engines: {node: '>=0.4.7'} + hasBin: true + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -3470,14 +3665,14 @@ packages: resolution: {integrity: sha512-P+1n3MnwjR/Epg9BBo1KT8qbye2g2Ou4sFumihwt6I4tsUX7jnLcX4BTOSKg/B1ZrIYMN9FcEnG4x5a7NB8Eng==} engines: {node: '>=0.10.0'} + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - has-own-prop@2.0.0: - resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==} - engines: {node: '>=8'} - has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} @@ -3497,6 +3692,10 @@ packages: resolution: {integrity: sha512-FwO1BUVWkyHasWDW4S8o0ssQXjvyghLV2rfVhnN36b2bbcj45eGiuzdn9XOvOpjV3TKQD7Gm2BWNXdE9V4KKYg==} engines: {node: '>=12'} + hashery@1.5.1: + resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} + engines: {node: '>=20'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -3537,9 +3736,11 @@ packages: hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} - hexoid@2.0.0: - resolution: {integrity: sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==} - engines: {node: '>=8'} + hookified@1.15.1: + resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} + + hookified@2.1.0: + resolution: {integrity: sha512-ootKng4eaxNxa7rx6FJv2YKef3DuhqbEj3l70oGXwddPQEEnISm50TEZQclqiLTAtilT2nu7TErtCO523hHkyg==} html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -3551,6 +3752,10 @@ 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'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -3563,17 +3768,20 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} - import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -3716,6 +3924,10 @@ packages: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + is-number-object@1.1.1: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} @@ -3856,15 +4068,6 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jackspeak@4.1.0: - resolution: {integrity: sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==} - engines: {node: 20 || >=22} - - 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} @@ -4013,6 +4216,10 @@ 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'} @@ -4065,8 +4272,8 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - keyv@5.3.2: - resolution: {integrity: sha512-Lji2XRxqqa5Wg+CHLVfFKBImfJZ4pCSccu9eVWK6w4c2SDFLd8JAn1zqTuSFnsxb7ope6rMsnIHfp+eBbRBRZQ==} + keyv@5.6.0: + resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} khroma@2.1.0: resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} @@ -4083,6 +4290,10 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + kysely@0.28.14: + resolution: {integrity: sha512-SU3lgh0rPvq7upc6vvdVrCsSMUG1h3ChvHVOY7wJ2fw4C9QEB7X3d5eyYEyULUX7UQtxZJtZXGuT6U2US72UYA==} + engines: {node: '>=20.0.0'} + language-subtag-registry@0.3.23: resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} @@ -4108,8 +4319,12 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - loader-runner@4.3.0: - resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + load-esm@1.0.3: + resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==} + engines: {node: '>=13.2.0'} + + loader-runner@4.3.1: + resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} engines: {node: '>=6.11.5'} locate-path@5.0.0: @@ -4161,6 +4376,9 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} @@ -4192,14 +4410,6 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lru-cache@7.18.3: - resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} - engines: {node: '>=12'} - - luxon@3.5.0: - resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==} - engines: {node: '>=12'} - magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -4299,9 +4509,6 @@ packages: resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} engines: {node: '>= 4.0.0'} - merge-descriptors@1.0.3: - resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} - merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} @@ -4463,11 +4670,6 @@ packages: resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} engines: {node: '>= 0.6'} - mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} - hasBin: true - mime@2.6.0: resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} engines: {node: '>=4.0.0'} @@ -4477,17 +4679,13 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} - minimatch@10.0.1: - resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} - engines: {node: 20 || >=22} + 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@9.0.3: resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} engines: {node: '>=16 || 14 >=14.17'} @@ -4503,9 +4701,9 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - mkdirp@0.5.6: - resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} - hasBin: true + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} motion-dom@11.18.1: resolution: {integrity: sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==} @@ -4537,15 +4735,12 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} - ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - multer@1.4.5-lts.2: - resolution: {integrity: sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==} - engines: {node: '>= 6.0.0'} + multer@2.1.1: + resolution: {integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==} + engines: {node: '>= 10.16.0'} murmurhash-js@1.0.0: resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==} @@ -4565,14 +4760,6 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} - - negotiator@0.6.4: - resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} - engines: {node: '>= 0.6'} - negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} @@ -4638,24 +4825,27 @@ packages: node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + node-cron@4.2.1: + resolution: {integrity: sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==} + engines: {node: '>=6.0.0'} + node-emoji@1.11.0: resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + node-releases@2.0.36: + resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + + nodemon@3.1.14: + resolution: {integrity: sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==} + engines: {node: '>=10'} + hasBin: true + non-layered-tidy-tree-layout@2.0.2: resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==} @@ -4822,23 +5012,61 @@ packages: resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} engines: {node: 20 || >=22} - path-to-regexp@0.1.12: - resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} path-to-regexp@8.2.0: resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} engines: {node: '>=16'} - path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pbf@3.3.0: + resolution: {integrity: sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==} + hasBin: true + + periscopic@3.1.0: + resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} + + 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-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==} - pbf@3.3.0: - resolution: {integrity: sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==} - hasBin: true + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} - periscopic@3.1.0: - resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} + 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==} @@ -4916,6 +5144,22 @@ packages: resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + potpack@2.0.0: resolution: {integrity: sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==} @@ -4923,24 +5167,10 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-linter-helpers@1.0.0: - resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} - engines: {node: '>=6.0.0'} - - prettier@3.5.3: - resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} - engines: {node: '>=14'} - hasBin: true - 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@5.22.0: - resolution: {integrity: sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==} - engines: {node: '>=16.13'} - hasBin: true - process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -4970,6 +5200,9 @@ packages: pseudomap@1.0.2: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} + pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -4977,43 +5210,40 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + qified@0.9.0: + resolution: {integrity: sha512-4q61YgkHbY6gmwkqm0BsxyLDO3UYdrdiJTJ7JiaZb3xpW1duxn135SB7KqUEkCiuu5O4W+TtwEWP2VjmSRanvA==} + engines: {node: '>=20'} + qrcode.react@4.2.0: resolution: {integrity: sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - qs@6.13.0: - resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} - engines: {node: '>=0.6'} - qs@6.14.0: 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'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} quickselect@3.0.0: resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==} - radash@12.1.0: - resolution: {integrity: sha512-b0Zcf09AhqKS83btmUeYBS8tFK7XL2e3RvLmZcm0sTdF1/UUlHSsjXdCcWNxe7yfmAlPve5ym0DmKGtTzP6kVQ==} + radash@12.1.1: + resolution: {integrity: sha512-h36JMxKRqrAxVD8201FrCpyeNuUY9Y5zZwujr20fFO77tpUtGa6EZzfKw/3WaiBX95fq7+MpsuMLNdSnORAwSA==} engines: {node: '>=14.18.0'} - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - raw-body@2.5.2: - resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} - engines: {node: '>= 0.8'} - - 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'} react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} @@ -5064,9 +5294,6 @@ packages: reading-time@1.5.0: resolution: {integrity: sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==} - redis@4.7.0: - resolution: {integrity: sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==} - reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -5114,10 +5341,6 @@ packages: remove-accents@0.5.0: resolution: {integrity: sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==} - repeat-string@1.6.1: - resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} - engines: {node: '>=0.10'} - require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -5174,8 +5397,8 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rimraf@6.0.1: - resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==} + rimraf@6.1.3: + resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} engines: {node: 20 || >=22} hasBin: true @@ -5230,8 +5453,8 @@ packages: resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} engines: {node: '>= 10.13.0'} - schema-utils@4.3.0: - resolution: {integrity: sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==} + schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} scroll-into-view-if-needed@3.1.0: @@ -5250,25 +5473,19 @@ packages: engines: {node: '>=10'} hasBin: true - send@0.19.0: - resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} - engines: {node: '>= 0.8.0'} + 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'} - serialize-javascript@6.0.2: - resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - serialize-to-js@3.1.2: resolution: {integrity: sha512-owllqNuDDEimQat7EPG0tH7JjO090xKNzUtYz6X+Sk2BXDnOCilDdNLwjWeFywG9xkJul1ULvtUQa9O4pUaY0w==} engines: {node: '>=4.0.0'} - serve-static@1.16.2: - resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} - engines: {node: '>= 0.8.0'} - serve-static@2.2.0: resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} engines: {node: '>= 18'} @@ -5345,6 +5562,10 @@ packages: simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -5393,6 +5614,10 @@ packages: resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} 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==} @@ -5407,6 +5632,14 @@ 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'} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -5487,6 +5720,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strtok3@10.3.5: + resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} + engines: {node: '>=18'} + style-to-object@0.4.4: resolution: {integrity: sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==} @@ -5517,21 +5754,25 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true - 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'} supercluster@8.0.1: resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==} - 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@4.5.0: resolution: {integrity: sha512-ycQR/UbvI9xIlEdQT1TQqwoXtEldExbCEAJgRo5YXlmSKjv6ThHnP9/vwGa1gr19Gfw+LkFd7KqYMhzrRC5JYw==} engines: {node: '>=4'} + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -5544,8 +5785,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - swagger-ui-dist@5.20.5: - resolution: {integrity: sha512-7DqzFVHAW5MRhmWRDgd2Xr7RQUGaJv+7RfGmwChlOxz+tMLBmvHDz3vuVgaoj2CWNpTHxIm8aTsCBeJVxNrpjA==} + swagger-ui-dist@5.31.0: + resolution: {integrity: sha512-zSUTIck02fSga6rc0RZP3b7J7wgHXwLea8ZjgLA3Vgnb8QeOl3Wou2/j5QkzSGeoz6HusP/coYuJl33aQxQZpg==} symbol-observable@1.2.0: resolution: {integrity: sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==} @@ -5555,10 +5796,6 @@ packages: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} - synckit@0.11.2: - resolution: {integrity: sha512-1IUffI8zZ8qUMB3NUJIjk0RpLroG/8NkQDAWH1NbB2iJ0/5pn3M8rxfNzMz4GH9OnYaGYn31LEDSXJp/qIlxgA==} - engines: {node: ^14.18.0 || >=16.0.0} - tailwindcss@3.4.17: resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} engines: {node: '>=14.0.0'} @@ -5568,8 +5805,12 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} - terser-webpack-plugin@5.3.14: - resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==} + tapable@2.3.2: + resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} + engines: {node: '>=6'} + + terser-webpack-plugin@5.4.0: + resolution: {integrity: sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==} engines: {node: '>= 10.13.0'} peerDependencies: '@swc/core': '*' @@ -5633,11 +5874,12 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + token-types@6.1.2: + resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} + engines: {node: '>=14.16'} - tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + touch@3.1.1: + resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} hasBin: true trim-lines@3.0.1: @@ -5659,17 +5901,18 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - ts-jest@29.3.1: - resolution: {integrity: sha512-FT2PIRtZABwl6+ZCry8IY7JZ3xMuppsEV9qFVHOVe8jDzggwUZ9TsM4chyJxL9yi6LvkqcZYU3LmapEE454zBQ==} + 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': @@ -5682,9 +5925,11 @@ packages: optional: true esbuild: optional: true + jest-util: + optional: true - ts-loader@9.5.2: - resolution: {integrity: sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==} + ts-loader@9.5.4: + resolution: {integrity: sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==} engines: {node: '>=12.0.0'} peerDependencies: typescript: '*' @@ -5741,8 +5986,8 @@ packages: resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} 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@1.6.18: @@ -5772,13 +6017,13 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript@5.7.3: - resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} engines: {node: '>=14.17'} hasBin: true - typescript@5.8.3: - resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true @@ -5788,20 +6033,35 @@ packages: typewise@1.0.3: resolution: {integrity: sha512-aXofE06xGhaQSPzt8hlTY+/YWQhm9P0jYUp1f2XtmW/3Bk0qzXcyFWAtPoo2uTGQj1ZwbDuSyuxicq+aDo8lCQ==} + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + uid@2.0.2: resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} engines: {node: '>=8'} + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} + undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + unified@10.1.2: resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} @@ -5883,15 +6143,21 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - utils-merge@1.0.1: - resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} - engines: {node: '>= 0.4.0'} + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} @@ -5909,10 +6175,6 @@ packages: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} - value-or-promise@1.0.12: - resolution: {integrity: sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==} - engines: {node: '>=12'} - vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -5947,8 +6209,8 @@ packages: walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} - watchpack@2.4.2: - resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} + watchpack@2.5.1: + resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} engines: {node: '>=10.13.0'} wcwidth@1.0.1: @@ -5960,19 +6222,16 @@ packages: web-worker@1.5.0: resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==} - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webpack-node-externals@3.0.0: resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} engines: {node: '>=6'} - webpack-sources@3.2.3: - resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + webpack-sources@3.3.4: + resolution: {integrity: sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==} engines: {node: '>=10.13.0'} - webpack@5.98.0: - resolution: {integrity: sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==} + webpack@5.104.1: + resolution: {integrity: sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==} engines: {node: '>=10.13.0'} hasBin: true peerDependencies: @@ -5981,12 +6240,9 @@ packages: webpack-cli: optional: true - whatwg-mimetype@3.0.0: - resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} - engines: {node: '>=12'} - - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} @@ -6017,6 +6273,9 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -6048,8 +6307,8 @@ packages: utf-8-validate: optional: true - ws@8.18.1: - resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==} + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -6079,9 +6338,6 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yaml@2.7.1: resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==} engines: {node: '>= 14'} @@ -6107,6 +6363,10 @@ packages: resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} engines: {node: '>=18'} + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + zod@3.24.2: resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} @@ -6122,7 +6382,18 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 - '@angular-devkit/core@19.2.6(chokidar@4.0.3)': + '@angular-devkit/core@19.2.17(chokidar@4.0.3)': + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + jsonc-parser: 3.3.1 + picomatch: 4.0.2 + rxjs: 7.8.1 + source-map: 0.7.4 + optionalDependencies: + chokidar: 4.0.3 + + '@angular-devkit/core@19.2.19(chokidar@4.0.3)': dependencies: ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) @@ -6133,10 +6404,10 @@ snapshots: optionalDependencies: chokidar: 4.0.3 - '@angular-devkit/schematics-cli@19.2.6(@types/node@22.14.0)(chokidar@4.0.3)': + '@angular-devkit/schematics-cli@19.2.19(@types/node@22.14.0)(chokidar@4.0.3)': dependencies: - '@angular-devkit/core': 19.2.6(chokidar@4.0.3) - '@angular-devkit/schematics': 19.2.6(chokidar@4.0.3) + '@angular-devkit/core': 19.2.19(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.19(chokidar@4.0.3) '@inquirer/prompts': 7.3.2(@types/node@22.14.0) ansi-colors: 4.1.3 symbol-observable: 4.0.0 @@ -6145,9 +6416,19 @@ snapshots: - '@types/node' - chokidar - '@angular-devkit/schematics@19.2.6(chokidar@4.0.3)': + '@angular-devkit/schematics@19.2.17(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.17(chokidar@4.0.3) + jsonc-parser: 3.3.1 + magic-string: 0.30.17 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + + '@angular-devkit/schematics@19.2.19(chokidar@4.0.3)': dependencies: - '@angular-devkit/core': 19.2.6(chokidar@4.0.3) + '@angular-devkit/core': 19.2.19(chokidar@4.0.3) jsonc-parser: 3.3.1 magic-string: 0.30.17 ora: 5.4.1 @@ -6155,9 +6436,9 @@ snapshots: transitivePeerDependencies: - chokidar - '@apollo/cache-control-types@1.0.3(graphql@16.10.0)': + '@apollo/cache-control-types@1.0.3(graphql@16.13.2)': dependencies: - graphql: 16.10.0 + graphql: 16.13.2 '@apollo/protobufjs@1.2.7': dependencies: @@ -6174,107 +6455,108 @@ snapshots: '@types/long': 4.0.2 long: 4.0.0 - '@apollo/server-gateway-interface@1.1.1(graphql@16.10.0)': + '@apollo/server-gateway-interface@2.0.0(graphql@16.13.2)': dependencies: '@apollo/usage-reporting-protobuf': 4.1.1 - '@apollo/utils.fetcher': 2.0.1 - '@apollo/utils.keyvaluecache': 2.1.1 - '@apollo/utils.logger': 2.0.1 - graphql: 16.10.0 + '@apollo/utils.fetcher': 3.1.0 + '@apollo/utils.keyvaluecache': 4.0.0 + '@apollo/utils.logger': 3.0.0 + graphql: 16.13.2 - '@apollo/server-plugin-landing-page-graphql-playground@4.0.1(@apollo/server@4.11.3(graphql@16.10.0))': + '@apollo/server-plugin-landing-page-graphql-playground@4.0.1(@apollo/server@5.5.0(graphql@16.13.2))': dependencies: - '@apollo/server': 4.11.3(graphql@16.10.0) + '@apollo/server': 5.5.0(graphql@16.13.2) '@apollographql/graphql-playground-html': 1.6.29 - '@apollo/server@4.11.3(graphql@16.10.0)': + '@apollo/server@5.5.0(graphql@16.13.2)': dependencies: - '@apollo/cache-control-types': 1.0.3(graphql@16.10.0) - '@apollo/server-gateway-interface': 1.1.1(graphql@16.10.0) + '@apollo/cache-control-types': 1.0.3(graphql@16.13.2) + '@apollo/server-gateway-interface': 2.0.0(graphql@16.13.2) '@apollo/usage-reporting-protobuf': 4.1.1 - '@apollo/utils.createhash': 2.0.2 - '@apollo/utils.fetcher': 2.0.1 - '@apollo/utils.isnodelike': 2.0.1 - '@apollo/utils.keyvaluecache': 2.1.1 - '@apollo/utils.logger': 2.0.1 - '@apollo/utils.usagereporting': 2.1.0(graphql@16.10.0) - '@apollo/utils.withrequired': 2.0.1 - '@graphql-tools/schema': 9.0.19(graphql@16.10.0) - '@types/express': 4.17.21 - '@types/express-serve-static-core': 4.19.6 - '@types/node-fetch': 2.6.12 + '@apollo/utils.createhash': 3.0.1 + '@apollo/utils.fetcher': 3.1.0 + '@apollo/utils.isnodelike': 3.0.0 + '@apollo/utils.keyvaluecache': 4.0.0 + '@apollo/utils.logger': 3.0.0 + '@apollo/utils.usagereporting': 2.1.0(graphql@16.13.2) + '@apollo/utils.withrequired': 3.0.0 + '@graphql-tools/schema': 10.0.23(graphql@16.13.2) async-retry: 1.3.3 + body-parser: 2.2.2 + content-type: 1.0.5 cors: 2.8.5 - express: 4.21.2 - graphql: 16.10.0 + finalhandler: 2.1.0 + graphql: 16.13.2 loglevel: 1.9.2 - lru-cache: 7.18.3 - negotiator: 0.6.4 - node-abort-controller: 3.1.1 - node-fetch: 2.7.0 - uuid: 9.0.1 - whatwg-mimetype: 3.0.0 + lru-cache: 11.1.0 + negotiator: 1.0.0 + uuid: 11.1.0 + whatwg-mimetype: 4.0.0 transitivePeerDependencies: - - encoding - supports-color '@apollo/usage-reporting-protobuf@4.1.1': dependencies: '@apollo/protobufjs': 1.2.7 - '@apollo/utils.createhash@2.0.2': + '@apollo/utils.createhash@3.0.1': dependencies: - '@apollo/utils.isnodelike': 2.0.1 + '@apollo/utils.isnodelike': 3.0.0 sha.js: 2.4.11 - '@apollo/utils.dropunuseddefinitions@2.0.1(graphql@16.10.0)': + '@apollo/utils.dropunuseddefinitions@2.0.1(graphql@16.13.2)': dependencies: - graphql: 16.10.0 + graphql: 16.13.2 - '@apollo/utils.fetcher@2.0.1': {} + '@apollo/utils.fetcher@3.1.0': {} - '@apollo/utils.isnodelike@2.0.1': {} + '@apollo/utils.isnodelike@3.0.0': {} - '@apollo/utils.keyvaluecache@2.1.1': + '@apollo/utils.keyvaluecache@4.0.0': dependencies: - '@apollo/utils.logger': 2.0.1 - lru-cache: 7.18.3 + '@apollo/utils.logger': 3.0.0 + lru-cache: 11.1.0 - '@apollo/utils.logger@2.0.1': {} + '@apollo/utils.logger@3.0.0': {} - '@apollo/utils.printwithreducedwhitespace@2.0.1(graphql@16.10.0)': + '@apollo/utils.printwithreducedwhitespace@2.0.1(graphql@16.13.2)': dependencies: - graphql: 16.10.0 + graphql: 16.13.2 - '@apollo/utils.removealiases@2.0.1(graphql@16.10.0)': + '@apollo/utils.removealiases@2.0.1(graphql@16.13.2)': dependencies: - graphql: 16.10.0 + graphql: 16.13.2 - '@apollo/utils.sortast@2.0.1(graphql@16.10.0)': + '@apollo/utils.sortast@2.0.1(graphql@16.13.2)': dependencies: - graphql: 16.10.0 + graphql: 16.13.2 lodash.sortby: 4.7.0 - '@apollo/utils.stripsensitiveliterals@2.0.1(graphql@16.10.0)': + '@apollo/utils.stripsensitiveliterals@2.0.1(graphql@16.13.2)': dependencies: - graphql: 16.10.0 + graphql: 16.13.2 - '@apollo/utils.usagereporting@2.1.0(graphql@16.10.0)': + '@apollo/utils.usagereporting@2.1.0(graphql@16.13.2)': dependencies: '@apollo/usage-reporting-protobuf': 4.1.1 - '@apollo/utils.dropunuseddefinitions': 2.0.1(graphql@16.10.0) - '@apollo/utils.printwithreducedwhitespace': 2.0.1(graphql@16.10.0) - '@apollo/utils.removealiases': 2.0.1(graphql@16.10.0) - '@apollo/utils.sortast': 2.0.1(graphql@16.10.0) - '@apollo/utils.stripsensitiveliterals': 2.0.1(graphql@16.10.0) - graphql: 16.10.0 + '@apollo/utils.dropunuseddefinitions': 2.0.1(graphql@16.13.2) + '@apollo/utils.printwithreducedwhitespace': 2.0.1(graphql@16.13.2) + '@apollo/utils.removealiases': 2.0.1(graphql@16.13.2) + '@apollo/utils.sortast': 2.0.1(graphql@16.13.2) + '@apollo/utils.stripsensitiveliterals': 2.0.1(graphql@16.13.2) + graphql: 16.13.2 - '@apollo/utils.withrequired@2.0.1': {} + '@apollo/utils.withrequired@3.0.0': {} '@apollographql/graphql-playground-html@1.6.29': dependencies: xss: 1.0.15 + '@as-integrations/express5@1.1.2(@apollo/server@5.5.0(graphql@16.13.2))(express@5.2.1)': + dependencies: + '@apollo/server': 5.5.0(graphql@16.13.2) + express: 5.2.1 + '@babel/code-frame@7.26.2': dependencies: '@babel/helper-validator-identifier': 7.25.9 @@ -6296,7 +6578,7 @@ snapshots: '@babel/traverse': 7.27.0 '@babel/types': 7.27.0 convert-source-map: 2.0.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -6454,7 +6736,7 @@ snapshots: '@babel/parser': 7.27.0 '@babel/template': 7.27.0 '@babel/types': 7.27.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -6466,8 +6748,57 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} + '@biomejs/biome@1.9.4': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 1.9.4 + '@biomejs/cli-darwin-x64': 1.9.4 + '@biomejs/cli-linux-arm64': 1.9.4 + '@biomejs/cli-linux-arm64-musl': 1.9.4 + '@biomejs/cli-linux-x64': 1.9.4 + '@biomejs/cli-linux-x64-musl': 1.9.4 + '@biomejs/cli-win32-arm64': 1.9.4 + '@biomejs/cli-win32-x64': 1.9.4 + + '@biomejs/cli-darwin-arm64@1.9.4': + optional: true + + '@biomejs/cli-darwin-x64@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64@1.9.4': + optional: true + + '@biomejs/cli-linux-x64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-x64@1.9.4': + optional: true + + '@biomejs/cli-win32-arm64@1.9.4': + optional: true + + '@biomejs/cli-win32-x64@1.9.4': + optional: true + + '@borewit/text-codec@0.2.2': {} + '@braintree/sanitize-url@6.0.4': {} + '@cacheable/memory@2.0.8': + dependencies: + '@cacheable/utils': 2.4.1 + '@keyv/bigmap': 1.3.1(keyv@5.6.0) + hookified: 1.15.1 + keyv: 5.6.0 + + '@cacheable/utils@2.4.1': + dependencies: + hashery: 1.5.1 + keyv: 5.6.0 + '@colors/colors@1.5.0': optional: true @@ -6501,11 +6832,11 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.3.7 + debug: 4.4.0(supports-color@5.5.0) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 - import-fresh: 3.3.0 + import-fresh: 3.3.1 js-yaml: 4.1.0 minimatch: 3.1.2 strip-json-comments: 3.1.1 @@ -6514,7 +6845,7 @@ snapshots: '@eslint/js@8.57.1': {} - '@fast-csv/parse@5.0.2': + '@fast-csv/parse@5.0.5': dependencies: lodash.escaperegexp: 4.1.2 lodash.groupby: 4.6.0 @@ -6523,51 +6854,52 @@ snapshots: lodash.isundefined: 3.0.1 lodash.uniq: 4.5.0 - '@graphql-tools/merge@8.4.2(graphql@16.10.0)': + '@graphql-tools/merge@9.0.24(graphql@16.13.2)': dependencies: - '@graphql-tools/utils': 9.2.1(graphql@16.10.0) - graphql: 16.10.0 + '@graphql-tools/utils': 10.8.6(graphql@16.13.2) + graphql: 16.13.2 tslib: 2.8.1 - '@graphql-tools/merge@9.0.24(graphql@16.10.0)': + '@graphql-tools/merge@9.1.7(graphql@16.13.2)': dependencies: - '@graphql-tools/utils': 10.8.6(graphql@16.10.0) - graphql: 16.10.0 + '@graphql-tools/utils': 11.0.0(graphql@16.13.2) + graphql: 16.13.2 tslib: 2.8.1 - '@graphql-tools/schema@10.0.23(graphql@16.10.0)': + '@graphql-tools/schema@10.0.23(graphql@16.13.2)': dependencies: - '@graphql-tools/merge': 9.0.24(graphql@16.10.0) - '@graphql-tools/utils': 10.8.6(graphql@16.10.0) - graphql: 16.10.0 + '@graphql-tools/merge': 9.0.24(graphql@16.13.2) + '@graphql-tools/utils': 10.8.6(graphql@16.13.2) + graphql: 16.13.2 tslib: 2.8.1 - '@graphql-tools/schema@9.0.19(graphql@16.10.0)': + '@graphql-tools/schema@10.0.31(graphql@16.13.2)': dependencies: - '@graphql-tools/merge': 8.4.2(graphql@16.10.0) - '@graphql-tools/utils': 9.2.1(graphql@16.10.0) - graphql: 16.10.0 + '@graphql-tools/merge': 9.1.7(graphql@16.13.2) + '@graphql-tools/utils': 11.0.0(graphql@16.13.2) + graphql: 16.13.2 tslib: 2.8.1 - value-or-promise: 1.0.12 - '@graphql-tools/utils@10.8.6(graphql@16.10.0)': + '@graphql-tools/utils@10.8.6(graphql@16.13.2)': dependencies: - '@graphql-typed-document-node/core': 3.2.0(graphql@16.10.0) + '@graphql-typed-document-node/core': 3.2.0(graphql@16.13.2) '@whatwg-node/promise-helpers': 1.3.0 cross-inspect: 1.0.1 dset: 3.1.4 - graphql: 16.10.0 + graphql: 16.13.2 tslib: 2.8.1 - '@graphql-tools/utils@9.2.1(graphql@16.10.0)': + '@graphql-tools/utils@11.0.0(graphql@16.13.2)': dependencies: - '@graphql-typed-document-node/core': 3.2.0(graphql@16.10.0) - graphql: 16.10.0 + '@graphql-typed-document-node/core': 3.2.0(graphql@16.13.2) + '@whatwg-node/promise-helpers': 1.3.0 + cross-inspect: 1.0.1 + graphql: 16.13.2 tslib: 2.8.1 - '@graphql-typed-document-node/core@3.2.0(graphql@16.10.0)': + '@graphql-typed-document-node/core@3.2.0(graphql@16.13.2)': dependencies: - graphql: 16.10.0 + graphql: 16.13.2 '@headlessui/react@1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -6583,7 +6915,7 @@ snapshots: '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.7 + debug: 4.4.0(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -6667,6 +6999,8 @@ snapshots: '@img/sharp-win32-x64@0.33.5': optional: true + '@inquirer/ansi@1.0.2': {} + '@inquirer/checkbox@4.1.5(@types/node@22.14.0)': dependencies: '@inquirer/core': 10.1.10(@types/node@22.14.0) @@ -6677,6 +7011,23 @@ snapshots: optionalDependencies: '@types/node': 22.14.0 + '@inquirer/checkbox@4.3.2(@types/node@22.14.0)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.14.0) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.14.0) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.14.0 + + '@inquirer/confirm@5.1.21(@types/node@22.14.0)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.14.0) + '@inquirer/type': 3.0.10(@types/node@22.14.0) + optionalDependencies: + '@types/node': 22.14.0 + '@inquirer/confirm@5.1.9(@types/node@22.14.0)': dependencies: '@inquirer/core': 10.1.10(@types/node@22.14.0) @@ -6697,6 +7048,19 @@ snapshots: optionalDependencies: '@types/node': 22.14.0 + '@inquirer/core@10.3.2(@types/node@22.14.0)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.14.0) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.14.0 + '@inquirer/editor@4.2.10(@types/node@22.14.0)': dependencies: '@inquirer/core': 10.1.10(@types/node@22.14.0) @@ -6705,6 +7069,14 @@ snapshots: optionalDependencies: '@types/node': 22.14.0 + '@inquirer/editor@4.2.23(@types/node@22.14.0)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.14.0) + '@inquirer/external-editor': 1.0.3(@types/node@22.14.0) + '@inquirer/type': 3.0.10(@types/node@22.14.0) + optionalDependencies: + '@types/node': 22.14.0 + '@inquirer/expand@4.0.12(@types/node@22.14.0)': dependencies: '@inquirer/core': 10.1.10(@types/node@22.14.0) @@ -6713,8 +7085,25 @@ snapshots: optionalDependencies: '@types/node': 22.14.0 + '@inquirer/expand@4.0.23(@types/node@22.14.0)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.14.0) + '@inquirer/type': 3.0.10(@types/node@22.14.0) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.14.0 + + '@inquirer/external-editor@1.0.3(@types/node@22.14.0)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 22.14.0 + '@inquirer/figures@1.0.11': {} + '@inquirer/figures@1.0.15': {} + '@inquirer/input@4.1.9(@types/node@22.14.0)': dependencies: '@inquirer/core': 10.1.10(@types/node@22.14.0) @@ -6722,6 +7111,13 @@ snapshots: optionalDependencies: '@types/node': 22.14.0 + '@inquirer/input@4.3.1(@types/node@22.14.0)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.14.0) + '@inquirer/type': 3.0.10(@types/node@22.14.0) + optionalDependencies: + '@types/node': 22.14.0 + '@inquirer/number@3.0.12(@types/node@22.14.0)': dependencies: '@inquirer/core': 10.1.10(@types/node@22.14.0) @@ -6729,6 +7125,13 @@ snapshots: optionalDependencies: '@types/node': 22.14.0 + '@inquirer/number@3.0.23(@types/node@22.14.0)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.14.0) + '@inquirer/type': 3.0.10(@types/node@22.14.0) + optionalDependencies: + '@types/node': 22.14.0 + '@inquirer/password@4.0.12(@types/node@22.14.0)': dependencies: '@inquirer/core': 10.1.10(@types/node@22.14.0) @@ -6737,22 +7140,30 @@ snapshots: optionalDependencies: '@types/node': 22.14.0 - '@inquirer/prompts@7.3.2(@types/node@22.14.0)': + '@inquirer/password@4.0.23(@types/node@22.14.0)': dependencies: - '@inquirer/checkbox': 4.1.5(@types/node@22.14.0) - '@inquirer/confirm': 5.1.9(@types/node@22.14.0) - '@inquirer/editor': 4.2.10(@types/node@22.14.0) - '@inquirer/expand': 4.0.12(@types/node@22.14.0) - '@inquirer/input': 4.1.9(@types/node@22.14.0) - '@inquirer/number': 3.0.12(@types/node@22.14.0) - '@inquirer/password': 4.0.12(@types/node@22.14.0) - '@inquirer/rawlist': 4.0.12(@types/node@22.14.0) - '@inquirer/search': 3.0.12(@types/node@22.14.0) - '@inquirer/select': 4.1.1(@types/node@22.14.0) + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.14.0) + '@inquirer/type': 3.0.10(@types/node@22.14.0) optionalDependencies: '@types/node': 22.14.0 - '@inquirer/prompts@7.4.1(@types/node@22.14.0)': + '@inquirer/prompts@7.10.1(@types/node@22.14.0)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@22.14.0) + '@inquirer/confirm': 5.1.21(@types/node@22.14.0) + '@inquirer/editor': 4.2.23(@types/node@22.14.0) + '@inquirer/expand': 4.0.23(@types/node@22.14.0) + '@inquirer/input': 4.3.1(@types/node@22.14.0) + '@inquirer/number': 3.0.23(@types/node@22.14.0) + '@inquirer/password': 4.0.23(@types/node@22.14.0) + '@inquirer/rawlist': 4.1.11(@types/node@22.14.0) + '@inquirer/search': 3.2.2(@types/node@22.14.0) + '@inquirer/select': 4.4.2(@types/node@22.14.0) + optionalDependencies: + '@types/node': 22.14.0 + + '@inquirer/prompts@7.3.2(@types/node@22.14.0)': dependencies: '@inquirer/checkbox': 4.1.5(@types/node@22.14.0) '@inquirer/confirm': 5.1.9(@types/node@22.14.0) @@ -6775,6 +7186,14 @@ snapshots: optionalDependencies: '@types/node': 22.14.0 + '@inquirer/rawlist@4.1.11(@types/node@22.14.0)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.14.0) + '@inquirer/type': 3.0.10(@types/node@22.14.0) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.14.0 + '@inquirer/search@3.0.12(@types/node@22.14.0)': dependencies: '@inquirer/core': 10.1.10(@types/node@22.14.0) @@ -6784,6 +7203,15 @@ snapshots: optionalDependencies: '@types/node': 22.14.0 + '@inquirer/search@3.2.2(@types/node@22.14.0)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.14.0) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.14.0) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.14.0 + '@inquirer/select@4.1.1(@types/node@22.14.0)': dependencies: '@inquirer/core': 10.1.10(@types/node@22.14.0) @@ -6794,6 +7222,20 @@ snapshots: optionalDependencies: '@types/node': 22.14.0 + '@inquirer/select@4.4.2(@types/node@22.14.0)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.14.0) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.14.0) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.14.0 + + '@inquirer/type@3.0.10(@types/node@22.14.0)': + optionalDependencies: + '@types/node': 22.14.0 + '@inquirer/type@3.0.6(@types/node@22.14.0)': optionalDependencies: '@types/node': 22.14.0 @@ -6820,7 +7262,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 @@ -6833,14 +7275,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)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.3)) + jest-config: 29.7.0(@types/node@25.5.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -6865,7 +7307,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': @@ -6883,7 +7325,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 @@ -6905,7 +7347,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 @@ -6975,7 +7417,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 @@ -7006,15 +7448,22 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@keyv/redis@4.3.3': + '@keyv/bigmap@1.3.1(keyv@5.6.0)': dependencies: - cluster-key-slot: 1.1.2 - keyv: 5.3.2 - redis: 4.7.0 + hashery: 1.5.1 + hookified: 1.15.1 + keyv: 5.6.0 - '@keyv/serialize@1.0.3': + '@keyv/redis@5.1.6(keyv@5.6.0)': dependencies: - buffer: 6.0.3 + '@redis/client': 5.11.0 + cluster-key-slot: 1.1.2 + hookified: 1.15.1 + keyv: 5.6.0 + transitivePeerDependencies: + - '@node-rs/xxhash' + + '@keyv/serialize@1.1.1': {} '@lukeed/csprng@1.1.0': {} @@ -7071,7 +7520,7 @@ snapshots: '@types/react': 18.3.20 react: 18.3.1 - '@microsoft/tsdoc@0.15.1': {} + '@microsoft/tsdoc@0.16.0': {} '@napi-rs/simple-git-android-arm-eabi@0.1.19': optional: true @@ -7138,46 +7587,47 @@ snapshots: '@tybys/wasm-util': 0.9.0 optional: true - '@nestjs/apollo@13.0.4(@apollo/server@4.11.3(graphql@16.10.0))(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/graphql@13.0.4(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13)(reflect-metadata@0.2.2)(rxjs@7.8.2))(graphql@16.10.0)(reflect-metadata@0.2.2)(ts-morph@25.0.1))(graphql@16.10.0)': + '@nestjs/apollo@13.2.4(@apollo/server@5.5.0(graphql@16.13.2))(@as-integrations/express5@1.1.2(@apollo/server@5.5.0(graphql@16.13.2))(express@5.2.1))(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/graphql@13.2.4(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(graphql@16.13.2)(reflect-metadata@0.2.2)(ts-morph@25.0.1))(graphql@16.13.2)': dependencies: - '@apollo/server': 4.11.3(graphql@16.10.0) - '@apollo/server-plugin-landing-page-graphql-playground': 4.0.1(@apollo/server@4.11.3(graphql@16.10.0)) - '@nestjs/common': 11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/graphql': 13.0.4(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13)(reflect-metadata@0.2.2)(rxjs@7.8.2))(graphql@16.10.0)(reflect-metadata@0.2.2)(ts-morph@25.0.1) - graphql: 16.10.0 + '@apollo/server': 5.5.0(graphql@16.13.2) + '@apollo/server-plugin-landing-page-graphql-playground': 4.0.1(@apollo/server@5.5.0(graphql@16.13.2)) + '@nestjs/common': 11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/graphql': 13.2.4(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(graphql@16.13.2)(reflect-metadata@0.2.2)(ts-morph@25.0.1) + graphql: 16.13.2 iterall: 1.3.0 lodash.omit: 4.5.0 tslib: 2.8.1 + optionalDependencies: + '@as-integrations/express5': 1.1.2(@apollo/server@5.5.0(graphql@16.13.2))(express@5.2.1) - '@nestjs/cache-manager@3.0.1(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13)(reflect-metadata@0.2.2)(rxjs@7.8.2))(cache-manager@6.4.2)(keyv@5.3.2)(rxjs@7.8.2)': + '@nestjs/cache-manager@3.1.0(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(cache-manager@7.2.8)(keyv@5.6.0)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) - cache-manager: 6.4.2 - keyv: 5.3.2 + '@nestjs/common': 11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cache-manager: 7.2.8 + keyv: 5.6.0 rxjs: 7.8.2 - '@nestjs/cli@11.0.6(@types/node@22.14.0)': + '@nestjs/cli@11.0.16(@types/node@22.14.0)': dependencies: - '@angular-devkit/core': 19.2.6(chokidar@4.0.3) - '@angular-devkit/schematics': 19.2.6(chokidar@4.0.3) - '@angular-devkit/schematics-cli': 19.2.6(@types/node@22.14.0)(chokidar@4.0.3) - '@inquirer/prompts': 7.4.1(@types/node@22.14.0) - '@nestjs/schematics': 11.0.4(chokidar@4.0.3)(typescript@5.7.3) - ansis: 3.17.0 + '@angular-devkit/core': 19.2.19(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.19(chokidar@4.0.3) + '@angular-devkit/schematics-cli': 19.2.19(@types/node@22.14.0)(chokidar@4.0.3) + '@inquirer/prompts': 7.10.1(@types/node@22.14.0) + '@nestjs/schematics': 11.0.9(chokidar@4.0.3)(typescript@5.9.3) + ansis: 4.2.0 chokidar: 4.0.3 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.7.3)(webpack@5.98.0) - glob: 11.0.1 + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.104.1) + glob: 13.0.0 node-emoji: 1.11.0 ora: 5.4.1 - tree-kill: 1.2.2 tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 - typescript: 5.7.3 - webpack: 5.98.0 + typescript: 5.9.3 + webpack: 5.104.1 webpack-node-externals: 3.0.0 transitivePeerDependencies: - '@types/node' @@ -7185,127 +7635,125 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: + file-type: 21.3.2 iterare: 1.2.1 + load-esm: 1.0.3 reflect-metadata: 0.2.2 rxjs: 7.8.2 tslib: 2.8.1 uid: 2.0.2 + transitivePeerDependencies: + - supports-color - '@nestjs/config@4.0.2(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': + '@nestjs/config@4.0.3(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2) - dotenv: 16.4.7 - dotenv-expand: 12.0.1 - lodash: 4.17.21 + '@nestjs/common': 11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2) + dotenv: 17.2.3 + dotenv-expand: 12.0.3 + lodash: 4.17.23 rxjs: 7.8.2 - '@nestjs/core@11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nuxt/opencollective': 0.4.1 fast-safe-stringify: 2.1.1 iterare: 1.2.1 - path-to-regexp: 8.2.0 + path-to-regexp: 8.3.0 reflect-metadata: 0.2.2 rxjs: 7.8.2 tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/platform-express': 11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.13) + '@nestjs/platform-express': 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17) - '@nestjs/graphql@13.0.4(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13)(reflect-metadata@0.2.2)(rxjs@7.8.2))(graphql@16.10.0)(reflect-metadata@0.2.2)(ts-morph@25.0.1)': + '@nestjs/graphql@13.2.4(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(graphql@16.13.2)(reflect-metadata@0.2.2)(ts-morph@25.0.1)': dependencies: - '@graphql-tools/merge': 9.0.24(graphql@16.10.0) - '@graphql-tools/schema': 10.0.23(graphql@16.10.0) - '@graphql-tools/utils': 10.8.6(graphql@16.10.0) - '@nestjs/common': 11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) + '@graphql-tools/merge': 9.1.7(graphql@16.13.2) + '@graphql-tools/schema': 10.0.31(graphql@16.13.2) + '@graphql-tools/utils': 11.0.0(graphql@16.13.2) + '@nestjs/common': 11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) chokidar: 4.0.3 fast-glob: 3.3.3 - graphql: 16.10.0 - graphql-tag: 2.12.6(graphql@16.10.0) - graphql-ws: 6.0.4(graphql@16.10.0)(ws@8.18.1) - lodash: 4.17.21 + graphql: 16.13.2 + graphql-tag: 2.12.6(graphql@16.13.2) + graphql-ws: 6.0.7(graphql@16.13.2)(ws@8.19.0) + lodash: 4.17.23 normalize-path: 3.0.0 reflect-metadata: 0.2.2 - subscriptions-transport-ws: 0.11.0(graphql@16.10.0) + subscriptions-transport-ws: 0.11.0(graphql@16.13.2) tslib: 2.8.1 - ws: 8.18.1 + ws: 8.19.0 optionalDependencies: ts-morph: 25.0.1 transitivePeerDependencies: - '@fastify/websocket' - bufferutil - - uWebSockets.js + - crossws - utf-8-validate - '@nestjs/mapped-types@2.1.0(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)': + '@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 - '@nestjs/platform-express@11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.13)': + '@nestjs/platform-express@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)': dependencies: - '@nestjs/common': 11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) - cors: 2.8.5 - express: 5.1.0 - multer: 1.4.5-lts.2 - path-to-regexp: 8.2.0 + '@nestjs/common': 11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cors: 2.8.6 + express: 5.2.1 + multer: 2.1.1 + path-to-regexp: 8.3.0 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@nestjs/schedule@5.0.1(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13)(reflect-metadata@0.2.2)(rxjs@7.8.2))': - dependencies: - '@nestjs/common': 11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) - cron: 3.5.0 - - '@nestjs/schematics@11.0.4(chokidar@4.0.3)(typescript@5.7.3)': + '@nestjs/schematics@11.0.9(chokidar@4.0.3)(typescript@5.8.3)': dependencies: - '@angular-devkit/core': 19.2.6(chokidar@4.0.3) - '@angular-devkit/schematics': 19.2.6(chokidar@4.0.3) - comment-json: 4.2.5 + '@angular-devkit/core': 19.2.17(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.17(chokidar@4.0.3) + comment-json: 4.4.1 jsonc-parser: 3.3.1 pluralize: 8.0.0 - typescript: 5.7.3 + typescript: 5.8.3 transitivePeerDependencies: - chokidar - '@nestjs/schematics@11.0.4(chokidar@4.0.3)(typescript@5.8.3)': + '@nestjs/schematics@11.0.9(chokidar@4.0.3)(typescript@5.9.3)': dependencies: - '@angular-devkit/core': 19.2.6(chokidar@4.0.3) - '@angular-devkit/schematics': 19.2.6(chokidar@4.0.3) - comment-json: 4.2.5 + '@angular-devkit/core': 19.2.17(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.17(chokidar@4.0.3) + comment-json: 4.4.1 jsonc-parser: 3.3.1 pluralize: 8.0.0 - typescript: 5.8.3 + typescript: 5.9.3 transitivePeerDependencies: - chokidar - '@nestjs/swagger@11.1.1(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)': + '@nestjs/swagger@11.2.6(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)': dependencies: - '@microsoft/tsdoc': 0.15.1 - '@nestjs/common': 11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) - js-yaml: 4.1.0 - lodash: 4.17.21 - path-to-regexp: 8.2.0 + '@microsoft/tsdoc': 0.16.0 + '@nestjs/common': 11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) + js-yaml: 4.1.1 + lodash: 4.17.23 + path-to-regexp: 8.3.0 reflect-metadata: 0.2.2 - swagger-ui-dist: 5.20.5 + swagger-ui-dist: 5.31.0 - '@nestjs/testing@11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.13))': + '@nestjs/testing@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17))': dependencies: - '@nestjs/common': 11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.0.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-express': 11.0.13(@nestjs/common@11.0.13(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.13) + '@nestjs/platform-express': 11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17) '@next/env@14.2.15': {} @@ -7346,6 +7794,8 @@ snapshots: '@next/swc-win32-x64-msvc@14.2.15': optional: true + '@noble/hashes@1.8.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -7356,51 +7806,23 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.17.1 - - '@nolyfill/is-core-module@1.0.39': {} - - '@nuxt/opencollective@0.4.1': - dependencies: - consola: 3.4.2 - - '@pkgjs/parseargs@0.11.0': - optional: true - - '@pkgr/core@0.2.0': {} - - '@popperjs/core@2.11.8': {} - - '@prisma/client@5.20.0(prisma@5.22.0)': - optionalDependencies: - prisma: 5.22.0 - - '@prisma/debug@5.22.0': - optional: true + fastq: 1.17.1 - '@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2': - optional: true + '@nolyfill/is-core-module@1.0.39': {} - '@prisma/engines@5.22.0': + '@nuxt/opencollective@0.4.1': dependencies: - '@prisma/debug': 5.22.0 - '@prisma/engines-version': 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2 - '@prisma/fetch-engine': 5.22.0 - '@prisma/get-platform': 5.22.0 - optional: true + consola: 3.4.2 - '@prisma/fetch-engine@5.22.0': + '@paralleldrive/cuid2@2.3.1': dependencies: - '@prisma/debug': 5.22.0 - '@prisma/engines-version': 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2 - '@prisma/get-platform': 5.22.0 - optional: true + '@noble/hashes': 1.8.0 - '@prisma/get-platform@5.22.0': - dependencies: - '@prisma/debug': 5.22.0 + '@pkgjs/parseargs@0.11.0': optional: true + '@popperjs/core@2.11.8': {} + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -7424,31 +7846,9 @@ snapshots: '@protobufjs/utf8@1.1.0': {} - '@redis/bloom@1.2.0(@redis/client@1.6.0)': - dependencies: - '@redis/client': 1.6.0 - - '@redis/client@1.6.0': + '@redis/client@5.11.0': dependencies: cluster-key-slot: 1.1.2 - generic-pool: 3.9.0 - yallist: 4.0.0 - - '@redis/graph@1.1.1(@redis/client@1.6.0)': - dependencies: - '@redis/client': 1.6.0 - - '@redis/json@1.0.7(@redis/client@1.6.0)': - dependencies: - '@redis/client': 1.6.0 - - '@redis/search@1.2.0(@redis/client@1.6.0)': - dependencies: - '@redis/client': 1.6.0 - - '@redis/time-series@1.1.0(@redis/client@1.6.0)': - dependencies: - '@redis/client': 1.6.0 '@rtsao/scc@1.1.0': {} @@ -7494,6 +7894,15 @@ snapshots: npm-to-yarn: 2.2.1 unist-util-visit: 5.0.0 + '@tokenizer/inflate@0.4.1': + dependencies: + debug: 4.4.3 + token-types: 6.1.2 + transitivePeerDependencies: + - supports-color + + '@tokenizer/token@0.3.0': {} + '@ts-morph/common@0.26.1': dependencies: fast-glob: 3.3.3 @@ -7541,11 +7950,11 @@ 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': {} @@ -7564,11 +7973,11 @@ snapshots: '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/eslint@9.6.1': dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 '@types/estree-jsx@1.0.5': @@ -7577,32 +7986,20 @@ snapshots: '@types/estree@1.0.7': {} - '@types/express-serve-static-core@4.19.6': - dependencies: - '@types/node': 22.14.0 - '@types/qs': 6.9.18 - '@types/range-parser': 1.2.7 - '@types/send': 0.17.4 + '@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@4.17.21': - dependencies: - '@types/body-parser': 1.19.5 - '@types/express-serve-static-core': 4.19.6 - '@types/qs': 6.9.18 - '@types/serve-static': 1.15.7 - - '@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/geojson-vt@3.2.5': dependencies: @@ -7612,7 +8009,7 @@ snapshots: '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 22.14.0 + '@types/node': 25.5.0 '@types/hast@2.3.10': dependencies: @@ -7649,8 +8046,6 @@ snapshots: '@types/long@4.0.2': {} - '@types/luxon@3.4.2': {} - '@types/mapbox-gl@3.4.1': dependencies: '@types/geojson': 7946.0.16 @@ -7679,10 +8074,7 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node-fetch@2.6.12': - dependencies: - '@types/node': 22.14.0 - form-data: 4.0.2 + '@types/node-cron@3.0.11': {} '@types/node@20.17.30': dependencies: @@ -7692,8 +8084,18 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@25.5.0': + dependencies: + undici-types: 7.18.2 + '@types/pbf@3.0.5': {} + '@types/pg@8.20.0': + dependencies: + '@types/node': 25.5.0 + pg-protocol: 1.13.0 + pg-types: 2.2.0 + '@types/prop-types@15.7.14': {} '@types/qs@6.9.18': {} @@ -7709,18 +8111,15 @@ snapshots: '@types/prop-types': 15.7.14 csstype: 3.1.3 - '@types/semver@7.7.0': {} - '@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': + '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 22.14.0 - '@types/send': 0.17.4 + '@types/node': 25.5.0 '@types/stack-utils@2.0.3': {} @@ -7728,7 +8127,7 @@ snapshots: 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/supercluster@7.1.3': @@ -7746,7 +8145,7 @@ snapshots: '@types/unzipper@0.10.11': dependencies: - '@types/node': 22.14.0 + '@types/node': 25.5.0 '@types/yargs-parser@21.0.3': {} @@ -7754,33 +8153,13 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)': - dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.8.3) - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@5.8.3) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.0 - eslint: 8.57.1 - graphemer: 1.4.0 - ignore: 5.3.2 - natural-compare: 1.4.0 - semver: 7.7.1 - ts-api-utils: 1.4.3(typescript@5.8.3) - optionalDependencies: - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3)': dependencies: '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) eslint: 8.57.1 optionalDependencies: typescript: 5.8.3 @@ -7792,25 +8171,13 @@ snapshots: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - '@typescript-eslint/type-utils@6.21.0(eslint@8.57.1)(typescript@5.8.3)': - dependencies: - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.8.3) - debug: 4.4.0 - eslint: 8.57.1 - ts-api-utils: 1.4.3(typescript@5.8.3) - optionalDependencies: - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/types@6.21.0': {} '@typescript-eslint/typescript-estree@6.21.0(typescript@5.8.3)': dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -7821,20 +8188,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@6.21.0(eslint@8.57.1)(typescript@5.8.3)': - dependencies: - '@eslint-community/eslint-utils': 4.5.1(eslint@8.57.1) - '@types/json-schema': 7.0.15 - '@types/semver': 7.7.0 - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3) - eslint: 8.57.1 - semver: 7.7.1 - transitivePeerDependencies: - - supports-color - - typescript - '@typescript-eslint/visitor-keys@6.21.0': dependencies: '@typescript-eslint/types': 6.21.0 @@ -7973,16 +8326,15 @@ snapshots: '@xtuc/long@4.2.2': {} - accepts@1.3.8: - dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 - accepts@2.0.0: dependencies: mime-types: 3.0.1 negotiator: 1.0.0 + acorn-import-phases@1.0.4(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + acorn-jsx@5.3.2(acorn@8.14.1): dependencies: acorn: 8.14.1 @@ -7993,6 +8345,8 @@ snapshots: acorn@8.14.1: {} + acorn@8.16.0: {} + ajv-formats@2.1.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -8048,7 +8402,7 @@ snapshots: ansi-styles@6.2.1: {} - ansis@3.17.0: {} + ansis@4.2.0: {} any-promise@1.3.0: {} @@ -8082,8 +8436,6 @@ snapshots: call-bound: 1.0.4 is-array-buffer: 3.0.5 - array-flatten@1.1.1: {} - array-includes@3.1.8: dependencies: call-bind: 1.0.8 @@ -8093,6 +8445,17 @@ snapshots: get-intrinsic: 1.3.0 is-string: 1.1.1 + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + array-timsort@1.0.3: {} array-union@2.1.0: {} @@ -8162,8 +8525,6 @@ snapshots: dependencies: retry: 0.13.1 - async@3.2.6: {} - asynckit@0.4.0: {} available-typed-arrays@1.0.7: @@ -8235,8 +8596,12 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + base64-js@1.5.1: {} + baseline-browser-mapping@2.10.12: {} + binary-extensions@2.3.0: {} bl@4.1.0: @@ -8247,33 +8612,16 @@ snapshots: bluebird@3.7.2: {} - body-parser@1.20.3: - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - on-finished: 2.4.1 - qs: 6.13.0 - raw-body: 2.5.2 - type-is: 1.6.18 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - - 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 @@ -8287,6 +8635,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -8298,6 +8650,14 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.24.4) + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.10.12 + caniuse-lite: 1.0.30001781 + electron-to-chromium: 1.5.328 + node-releases: 2.0.36 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + bs-logger@0.2.6: dependencies: fast-json-stable-stringify: 2.1.0 @@ -8313,11 +8673,6 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - buffer@6.0.3: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - busboy@1.6.0: dependencies: streamsearch: 1.1.0 @@ -8333,9 +8688,18 @@ snapshots: bytewise-core: 1.2.3 typewise: 1.0.3 - cache-manager@6.4.2: + cache-manager@7.2.8: dependencies: - keyv: 5.3.2 + '@cacheable/utils': 2.4.1 + keyv: 5.6.0 + + cacheable@2.3.4: + dependencies: + '@cacheable/memory': 2.0.8 + '@cacheable/utils': 2.4.1 + hookified: 1.15.1 + keyv: 5.6.0 + qified: 0.9.0 call-bind-apply-helpers@1.0.2: dependencies: @@ -8364,6 +8728,8 @@ snapshots: caniuse-lite@1.0.30001711: {} + caniuse-lite@1.0.30001781: {} + ccount@2.0.1: {} chalk@2.3.0: @@ -8389,6 +8755,8 @@ snapshots: chardet@0.7.0: {} + chardet@2.1.1: {} + cheap-ruler@4.0.0: {} chokidar@3.6.0: @@ -8488,13 +8856,11 @@ snapshots: commander@8.3.0: {} - comment-json@4.2.5: + comment-json@4.4.1: dependencies: array-timsort: 1.0.3 core-util-is: 1.0.3 esprima: 4.0.1 - has-own-prop: 2.0.0 - repeat-string: 1.6.1 component-emitter@1.3.1: {} @@ -8502,19 +8868,15 @@ snapshots: concat-map@0.0.1: {} - concat-stream@1.6.2: + concat-stream@2.0.0: dependencies: buffer-from: 1.1.2 inherits: 2.0.4 - readable-stream: 2.3.8 + readable-stream: 3.6.2 typedarray: 0.0.6 consola@3.4.2: {} - content-disposition@0.5.4: - dependencies: - safe-buffer: 5.2.1 - content-disposition@1.0.0: dependencies: safe-buffer: 5.2.1 @@ -8523,8 +8885,6 @@ snapshots: convert-source-map@2.0.0: {} - cookie-signature@1.0.6: {} - cookie-signature@1.2.2: {} cookie@0.7.1: {} @@ -8538,18 +8898,23 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cose-base@1.0.3: dependencies: layout-base: 1.0.2 - cosmiconfig@8.3.6(typescript@5.7.3): + cosmiconfig@8.3.6(typescript@5.9.3): dependencies: import-fresh: 3.3.1 js-yaml: 4.1.0 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: - typescript: 5.7.3 + typescript: 5.9.3 create-jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.3)): dependencies: @@ -8568,11 +8933,6 @@ snapshots: create-require@1.1.1: {} - cron@3.5.0: - dependencies: - '@types/luxon': 3.4.2 - luxon: 3.5.0 - cross-inspect@1.0.1: dependencies: tslib: 2.8.1 @@ -8800,19 +9160,17 @@ snapshots: dayjs@1.11.13: {} - debug@2.6.9: - dependencies: - ms: 2.0.0 - debug@3.2.7: dependencies: ms: 2.1.3 - debug@4.3.7: + debug@4.4.0(supports-color@5.5.0): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 5.5.0 - debug@4.4.0: + debug@4.4.3: dependencies: ms: 2.1.3 @@ -8852,8 +9210,6 @@ snapshots: dequal@2.0.3: {} - destroy@1.2.0: {} - detect-libc@2.0.3: {} detect-newline@3.1.0: {} @@ -8902,12 +9258,14 @@ snapshots: dotenv-expand@10.0.0: optional: true - dotenv-expand@12.0.1: + dotenv-expand@12.0.3: dependencies: dotenv: 16.4.7 dotenv@16.4.7: {} + dotenv@17.2.3: {} + dset@3.1.4: {} dunder-proto@1.0.1: @@ -8926,12 +9284,10 @@ snapshots: ee-first@1.1.1: {} - ejs@3.1.10: - dependencies: - jake: 10.9.2 - electron-to-chromium@1.5.132: {} + electron-to-chromium@1.5.328: {} + elkjs@0.9.3: {} emittery@0.13.1: {} @@ -8940,8 +9296,6 @@ snapshots: emoji-regex@9.2.2: {} - encodeurl@1.0.2: {} - encodeurl@2.0.0: {} enhanced-resolve@5.18.1: @@ -9009,6 +9363,63 @@ snapshots: unbox-primitive: 1.1.0 which-typed-array: 1.1.19 + es-abstract@1.24.1: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -9032,7 +9443,7 @@ snapshots: iterator.prototype: 1.1.5 safe-array-concat: 1.1.3 - es-module-lexer@1.6.0: {} + es-module-lexer@2.0.0: {} es-object-atoms@1.1.1: dependencies: @@ -9074,8 +9485,8 @@ snapshots: '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.8.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.32.0)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1) @@ -9086,10 +9497,6 @@ snapshots: - eslint-plugin-import-x - supports-color - eslint-config-prettier@9.1.0(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - eslint-import-resolver-node@0.3.9: dependencies: debug: 3.2.7 @@ -9098,10 +9505,10 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1): + eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.32.0)(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) eslint: 8.57.1 get-tsconfig: 4.10.0 is-bun-module: 2.0.0 @@ -9109,25 +9516,25 @@ snapshots: tinyglobby: 0.2.12 unrs-resolver: 1.4.1 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.32.0)(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.8.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.32.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 - array-includes: 3.1.8 + array-includes: 3.1.9 array.prototype.findlastindex: 1.2.6 array.prototype.flat: 1.3.3 array.prototype.flatmap: 1.3.3 @@ -9135,7 +9542,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.32.0)(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -9172,16 +9579,6 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-prettier@5.2.6(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.5.3): - dependencies: - eslint: 8.57.1 - prettier: 3.5.3 - prettier-linter-helpers: 1.0.0 - synckit: 0.11.2 - optionalDependencies: - '@types/eslint': 9.6.1 - eslint-config-prettier: 9.1.0(eslint@8.57.1) - eslint-plugin-react-hooks@5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1): dependencies: eslint: 8.57.1 @@ -9233,7 +9630,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -9354,51 +9751,16 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 - express@4.21.2: - dependencies: - accepts: 1.3.8 - array-flatten: 1.1.1 - body-parser: 1.20.3 - content-disposition: 0.5.4 - content-type: 1.0.5 - cookie: 0.7.1 - cookie-signature: 1.0.6 - debug: 2.6.9 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 1.3.1 - fresh: 0.5.2 - http-errors: 2.0.0 - merge-descriptors: 1.0.3 - methods: 1.1.2 - on-finished: 2.4.1 - parseurl: 1.3.3 - path-to-regexp: 0.1.12 - proxy-addr: 2.0.7 - qs: 6.13.0 - range-parser: 1.2.1 - safe-buffer: 5.2.1 - send: 0.19.0 - serve-static: 1.16.2 - setprototypeof: 1.2.0 - statuses: 2.0.1 - type-is: 1.6.18 - utils-merge: 1.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - - 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.1 cookie-signature: 1.2.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) + depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -9441,8 +9803,6 @@ snapshots: fast-deep-equal@3.1.3: {} - fast-diff@1.3.0: {} - fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -9475,29 +9835,22 @@ snapshots: dependencies: flat-cache: 3.2.0 - filelist@1.0.4: - dependencies: - minimatch: 5.1.6 - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - finalhandler@1.3.1: + file-type@21.3.2: dependencies: - debug: 2.6.9 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.1 - unpipe: 1.0.0 + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.5 + token-types: 6.1.2 + uint8array-extras: 1.5.0 transitivePeerDependencies: - supports-color + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + finalhandler@2.1.0: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -9542,12 +9895,12 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.7.3)(webpack@5.98.0): + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.104.1): dependencies: '@babel/code-frame': 7.26.2 chalk: 4.1.2 chokidar: 4.0.3 - cosmiconfig: 8.3.6(typescript@5.7.3) + cosmiconfig: 8.3.6(typescript@5.9.3) deepmerge: 4.3.1 fs-extra: 10.1.0 memfs: 3.5.3 @@ -9556,8 +9909,8 @@ snapshots: schema-utils: 3.3.0 semver: 7.7.1 tapable: 2.2.1 - typescript: 5.7.3 - webpack: 5.98.0 + typescript: 5.9.3 + webpack: 5.104.1 form-data@4.0.2: dependencies: @@ -9566,10 +9919,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: {} @@ -9592,8 +9953,6 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - fresh@0.5.2: {} - fresh@2.0.0: {} fs-extra@10.1.0: @@ -9628,8 +9987,6 @@ snapshots: functions-have-names@1.2.3: {} - generic-pool@3.9.0: {} - gensync@1.0.0-beta.2: {} geojson-vt@4.0.2: {} @@ -9712,15 +10069,18 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 - glob@11.0.1: + glob@13.0.0: dependencies: - foreground-child: 3.3.1 - jackspeak: 4.1.0 - minimatch: 10.0.1 + minimatch: 10.2.4 minipass: 7.1.2 - package-json-from-dist: 1.0.1 path-scurry: 2.0.0 + glob@13.0.6: + dependencies: + minimatch: 10.2.4 + minipass: 7.1.3 + path-scurry: 2.0.2 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -9739,7 +10099,7 @@ snapshots: globalthis@1.0.4: dependencies: define-properties: 1.2.1 - gopd: 1.1.0 + gopd: 1.2.0 globby@11.1.0: dependencies: @@ -9750,28 +10110,24 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 - gopd@1.1.0: - dependencies: - get-intrinsic: 1.3.0 - gopd@1.2.0: {} graceful-fs@4.2.11: {} graphemer@1.4.0: {} - graphql-tag@2.12.6(graphql@16.10.0): + graphql-tag@2.12.6(graphql@16.13.2): dependencies: - graphql: 16.10.0 + graphql: 16.13.2 tslib: 2.8.1 - graphql-ws@6.0.4(graphql@16.10.0)(ws@8.18.1): + graphql-ws@6.0.7(graphql@16.13.2)(ws@8.19.0): dependencies: - graphql: 16.10.0 + graphql: 16.13.2 optionalDependencies: - ws: 8.18.1 + ws: 8.19.0 - graphql@16.10.0: {} + graphql@16.13.2: {} gray-matter@4.0.3: dependencies: @@ -9782,13 +10138,22 @@ snapshots: grid-index@1.1.0: {} + handlebars@4.7.9: + 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-bigints@1.1.0: {} has-flag@2.0.0: {} - has-flag@4.0.0: {} + has-flag@3.0.0: {} - has-own-prop@2.0.0: {} + has-flag@4.0.0: {} has-property-descriptors@1.0.2: dependencies: @@ -9810,6 +10175,10 @@ snapshots: sort-keys: 5.1.0 type-fest: 1.4.0 + hashery@1.5.1: + dependencies: + hookified: 1.15.1 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -9918,7 +10287,9 @@ snapshots: property-information: 7.0.0 space-separated-tokens: 2.0.2 - hexoid@2.0.0: {} + hookified@1.15.1: {} + + hookified@2.1.0: {} html-escaper@2.0.2: {} @@ -9932,6 +10303,14 @@ 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 + human-signals@2.1.0: {} iconv-lite@0.4.24: @@ -9942,14 +10321,15 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} - ignore@5.3.2: {} + ignore-by-default@1.0.1: {} - import-fresh@3.3.0: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 + ignore@5.3.2: {} import-fresh@3.3.1: dependencies: @@ -10082,6 +10462,8 @@ snapshots: is-map@2.0.3: {} + is-negative-zero@2.0.3: {} + is-number-object@1.1.1: dependencies: call-bound: 1.0.4 @@ -10194,7 +10576,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -10230,17 +10612,6 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jackspeak@4.1.0: - dependencies: - '@isaacs/cliui': 8.0.2 - - 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 @@ -10253,7 +10624,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 @@ -10323,6 +10694,37 @@ snapshots: - babel-plugin-macros - supports-color + jest-config@29.7.0(@types/node@25.5.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.3)): + dependencies: + '@babel/core': 7.26.10 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.26.10) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 25.5.0 + ts-node: 10.9.2(@types/node@22.14.0)(typescript@5.8.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-diff@29.7.0: dependencies: chalk: 4.1.2 @@ -10347,7 +10749,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 @@ -10357,7 +10759,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 @@ -10396,7 +10798,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): @@ -10431,7 +10833,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 @@ -10459,7 +10861,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 @@ -10505,7 +10907,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 @@ -10524,7 +10926,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 @@ -10533,13 +10935,13 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 22.14.0 + '@types/node': 25.5.0 merge-stream: 2.0.0 supports-color: 8.1.1 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 @@ -10569,6 +10971,10 @@ 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: {} @@ -10614,9 +11020,9 @@ snapshots: dependencies: json-buffer: 3.0.1 - keyv@5.3.2: + keyv@5.6.0: dependencies: - '@keyv/serialize': 1.0.3 + '@keyv/serialize': 1.1.1 khroma@2.1.0: {} @@ -10626,6 +11032,8 @@ snapshots: kleur@4.1.5: {} + kysely@0.28.14: {} + language-subtag-registry@0.3.23: {} language-tags@1.0.9: @@ -10645,7 +11053,9 @@ snapshots: lines-and-columns@1.2.4: {} - loader-runner@4.3.0: {} + load-esm@1.0.3: {} + + loader-runner@4.3.1: {} locate-path@5.0.0: dependencies: @@ -10681,6 +11091,8 @@ snapshots: lodash@4.17.21: {} + lodash@4.17.23: {} + log-symbols@4.1.0: dependencies: chalk: 4.1.2 @@ -10709,10 +11121,6 @@ snapshots: dependencies: yallist: 3.1.1 - lru-cache@7.18.3: {} - - luxon@3.5.0: {} - magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -10947,8 +11355,6 @@ snapshots: dependencies: fs-monkey: 1.0.6 - merge-descriptors@1.0.3: {} - merge-descriptors@2.0.0: {} merge-stream@2.0.0: {} @@ -11254,7 +11660,7 @@ snapshots: micromark@3.2.0: dependencies: '@types/debug': 4.1.12 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) decode-named-character-reference: 1.1.0 micromark-core-commonmark: 1.1.0 micromark-factory-space: 1.1.0 @@ -11290,24 +11696,18 @@ snapshots: dependencies: mime-db: 1.54.0 - mime@1.6.0: {} - mime@2.6.0: {} mimic-fn@2.1.0: {} - minimatch@10.0.1: + minimatch@10.2.4: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 5.0.5 minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 - minimatch@5.1.6: - dependencies: - brace-expansion: 2.0.1 - minimatch@9.0.3: dependencies: brace-expansion: 2.0.1 @@ -11320,9 +11720,7 @@ snapshots: minipass@7.1.2: {} - mkdirp@0.5.6: - dependencies: - minimist: 1.2.8 + minipass@7.1.3: {} motion-dom@11.18.1: dependencies: @@ -11346,19 +11744,14 @@ snapshots: mri@1.2.0: {} - ms@2.0.0: {} - ms@2.1.3: {} - multer@1.4.5-lts.2: + multer@2.1.1: dependencies: append-field: 1.0.0 busboy: 1.6.0 - concat-stream: 1.6.2 - mkdirp: 0.5.6 - object-assign: 4.1.1 + concat-stream: 2.0.0 type-is: 1.6.18 - xtend: 4.0.2 murmurhash-js@1.0.0: {} @@ -11374,10 +11767,6 @@ snapshots: natural-compare@1.4.0: {} - negotiator@0.6.3: {} - - negotiator@0.6.4: {} - negotiator@1.0.0: {} neo-async@2.6.2: {} @@ -11486,18 +11875,31 @@ snapshots: node-abort-controller@3.1.1: {} + node-cron@4.2.1: {} + node-emoji@1.11.0: dependencies: lodash: 4.17.21 - node-fetch@2.7.0: - dependencies: - whatwg-url: 5.0.0 - node-int64@0.4.0: {} node-releases@2.0.19: {} + node-releases@2.0.36: {} + + nodemon@3.1.14: + dependencies: + chokidar: 3.6.0 + debug: 4.4.0(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 10.2.4 + pstree.remy: 1.1.8 + semver: 7.7.1 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.1 + undefsafe: 2.0.5 + non-layered-tidy-tree-layout@2.0.2: {} normalize-path@3.0.0: {} @@ -11678,10 +12080,15 @@ snapshots: lru-cache: 11.1.0 minipass: 7.1.2 - path-to-regexp@0.1.12: {} + path-scurry@2.0.2: + dependencies: + lru-cache: 11.1.0 + minipass: 7.1.3 path-to-regexp@8.2.0: {} + path-to-regexp@8.3.0: {} + path-type@4.0.0: {} pbf@3.3.0: @@ -11695,6 +12102,41 @@ snapshots: estree-walker: 3.0.3 is-reference: 3.0.3 + pg-cloudflare@1.3.0: + optional: true + + pg-connection-string@2.12.0: {} + + pg-int8@1.0.1: {} + + 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@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: {} @@ -11757,15 +12199,19 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - potpack@2.0.0: {} + postgres-array@2.0.0: {} - prelude-ls@1.2.1: {} + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} - prettier-linter-helpers@1.0.0: + postgres-interval@1.2.0: dependencies: - fast-diff: 1.3.0 + xtend: 4.0.2 + + potpack@2.0.0: {} - prettier@3.5.3: {} + prelude-ls@1.2.1: {} pretty-format@29.7.0: dependencies: @@ -11773,13 +12219,6 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 - prisma@5.22.0: - dependencies: - '@prisma/engines': 5.22.0 - optionalDependencies: - fsevents: 2.3.3 - optional: true - process-nextick-args@2.0.1: {} prompts@2.4.2: @@ -11808,19 +12247,25 @@ snapshots: pseudomap@1.0.2: {} + pstree.remy@1.1.8: {} + punycode@2.3.1: {} pure-rand@6.1.0: {} + qified@0.9.0: + dependencies: + hookified: 2.1.0 + qrcode.react@4.2.0(react@18.3.1): dependencies: react: 18.3.1 - qs@6.13.0: + qs@6.14.0: dependencies: side-channel: 1.1.0 - qs@6.14.0: + qs@6.15.0: dependencies: side-channel: 1.1.0 @@ -11828,26 +12273,15 @@ snapshots: quickselect@3.0.0: {} - radash@12.1.0: {} - - randombytes@2.1.0: - dependencies: - safe-buffer: 5.2.1 + radash@12.1.1: {} range-parser@1.2.1: {} - raw-body@2.5.2: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - unpipe: 1.0.0 - - 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 react-dom@18.3.1(react@18.3.1): @@ -11901,22 +12335,13 @@ snapshots: reading-time@1.5.0: {} - redis@4.7.0: - dependencies: - '@redis/bloom': 1.2.0(@redis/client@1.6.0) - '@redis/client': 1.6.0 - '@redis/graph': 1.1.1(@redis/client@1.6.0) - '@redis/json': 1.0.7(@redis/client@1.6.0) - '@redis/search': 1.2.0(@redis/client@1.6.0) - '@redis/time-series': 1.1.0(@redis/client@1.6.0) - reflect-metadata@0.2.2: {} reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.9 + es-abstract: 1.24.1 es-errors: 1.3.0 es-object-atoms: 1.1.1 get-intrinsic: 1.3.0 @@ -12004,8 +12429,6 @@ snapshots: remove-accents@0.5.0: {} - repeat-string@1.6.1: {} - require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -12051,16 +12474,16 @@ snapshots: dependencies: glob: 7.2.3 - rimraf@6.0.1: + rimraf@6.1.3: dependencies: - glob: 11.0.1 + glob: 13.0.6 package-json-from-dist: 1.0.1 robust-predicates@3.0.2: {} router@2.2.0: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -12121,7 +12544,7 @@ snapshots: ajv: 6.12.6 ajv-keywords: 3.5.2(ajv@6.12.6) - schema-utils@4.3.0: + schema-utils@4.3.3: dependencies: '@types/json-schema': 7.0.15 ajv: 8.17.1 @@ -12141,27 +12564,11 @@ snapshots: semver@7.7.1: {} - send@0.19.0: - dependencies: - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - encodeurl: 1.0.2 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 0.5.2 - http-errors: 2.0.0 - mime: 1.6.0 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.1 - transitivePeerDependencies: - - supports-color + semver@7.7.4: {} send@1.2.0: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -12175,21 +12582,8 @@ snapshots: transitivePeerDependencies: - supports-color - serialize-javascript@6.0.2: - dependencies: - randombytes: 2.1.0 - serialize-to-js@3.1.2: {} - serve-static@1.16.2: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 0.19.0 - transitivePeerDependencies: - - supports-color - serve-static@2.2.0: dependencies: encodeurl: 2.0.0 @@ -12316,6 +12710,10 @@ snapshots: dependencies: is-arrayish: 0.3.2 + simple-update-notifier@2.0.0: + dependencies: + semver: 7.7.1 + sisteransi@1.0.5: {} slash@3.0.0: {} @@ -12359,6 +12757,8 @@ snapshots: dependencies: extend-shallow: 3.0.2 + split2@4.2.0: {} + sprintf-js@1.0.3: {} stable-hash@0.0.5: {} @@ -12369,6 +12769,13 @@ snapshots: statuses@2.0.1: {} + statuses@2.0.2: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + streamsearch@1.1.0: {} string-length@4.0.2: @@ -12471,6 +12878,10 @@ snapshots: strip-json-comments@3.1.1: {} + strtok3@10.3.5: + dependencies: + '@tokenizer/token': 0.3.0 + style-to-object@0.4.4: dependencies: inline-style-parser: 0.1.1 @@ -12482,11 +12893,11 @@ snapshots: stylis@4.3.6: {} - subscriptions-transport-ws@0.11.0(graphql@16.10.0): + subscriptions-transport-ws@0.11.0(graphql@16.13.2): dependencies: backo2: 1.0.2 eventemitter3: 3.1.2 - graphql: 16.10.0 + graphql: 16.13.2 iterall: 1.3.0 symbol-observable: 1.2.0 ws: 7.5.10 @@ -12504,17 +12915,17 @@ snapshots: pirates: 4.0.7 ts-interface-checker: 0.1.13 - superagent@9.0.2: + superagent@10.3.0: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.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 @@ -12522,10 +12933,11 @@ snapshots: dependencies: kdbush: 4.0.2 - 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 @@ -12533,6 +12945,10 @@ snapshots: dependencies: has-flag: 2.0.0 + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -12543,7 +12959,7 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - swagger-ui-dist@5.20.5: + swagger-ui-dist@5.31.0: dependencies: '@scarf/scarf': 1.4.0 @@ -12551,11 +12967,6 @@ snapshots: symbol-observable@4.0.0: {} - synckit@0.11.2: - dependencies: - '@pkgr/core': 0.2.0 - tslib: 2.8.1 - tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.17.30)(typescript@5.8.3)): dependencies: '@alloc/quick-lru': 5.2.0 @@ -12585,19 +12996,20 @@ snapshots: tapable@2.2.1: {} - terser-webpack-plugin@5.3.14(webpack@5.98.0): + tapable@2.3.2: {} + + terser-webpack-plugin@5.4.0(webpack@5.104.1): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 - schema-utils: 4.3.0 - serialize-javascript: 6.0.2 + schema-utils: 4.3.3 terser: 5.39.0 - webpack: 5.98.0 + webpack: 5.104.1 terser@5.39.0: dependencies: '@jridgewell/source-map': 0.3.6 - acorn: 8.14.1 + acorn: 8.16.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -12645,9 +13057,13 @@ snapshots: toidentifier@1.0.1: {} - tr46@0.0.3: {} + token-types@6.1.2: + dependencies: + '@borewit/text-codec': 0.2.2 + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 - tree-kill@1.2.2: {} + touch@3.1.1: {} trim-lines@3.0.1: {} @@ -12661,18 +13077,17 @@ snapshots: ts-interface-checker@0.1.13: {} - 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))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.3)))(typescript@5.8.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@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.3)))(typescript@5.8.3): dependencies: bs-logger: 0.2.6 - ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.9 jest: 29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.3)) - jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.7.1 - type-fest: 4.39.1 + semver: 7.7.4 + type-fest: 4.41.0 typescript: 5.8.3 yargs-parser: 21.1.1 optionalDependencies: @@ -12680,8 +13095,9 @@ snapshots: '@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 - ts-loader@9.5.2(typescript@5.8.3)(webpack@5.98.0): + ts-loader@9.5.4(typescript@5.8.3)(webpack@5.104.1): dependencies: chalk: 4.1.2 enhanced-resolve: 5.18.1 @@ -12689,7 +13105,7 @@ snapshots: semver: 7.7.1 source-map: 0.7.4 typescript: 5.8.3 - webpack: 5.98.0 + webpack: 5.104.1 ts-morph@25.0.1: dependencies: @@ -12733,6 +13149,24 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + ts-node@10.9.2(@types/node@25.5.0)(typescript@5.8.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 25.5.0 + acorn: 8.14.1 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.8.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + tsconfig-paths-webpack-plugin@4.2.0: dependencies: chalk: 4.1.2 @@ -12767,7 +13201,7 @@ snapshots: type-fest@1.4.0: {} - type-fest@4.39.1: {} + type-fest@4.41.0: {} type-is@1.6.18: dependencies: @@ -12815,20 +13249,25 @@ snapshots: typedarray@0.0.6: {} - typescript@5.7.3: {} - typescript@5.8.3: {} + typescript@5.9.3: {} + typewise-core@1.2.0: {} typewise@1.0.3: dependencies: typewise-core: 1.2.0 + uglify-js@3.19.3: + optional: true + uid@2.0.2: dependencies: '@lukeed/csprng': 1.1.0 + uint8array-extras@1.5.0: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -12836,10 +13275,14 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 + undefsafe@2.0.5: {} + undici-types@6.19.8: {} undici-types@6.21.0: {} + undici-types@7.18.2: {} + unified@10.1.2: dependencies: '@types/unist': 2.0.11 @@ -12977,13 +13420,19 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 util-deprecate@1.0.2: {} - utils-merge@1.0.1: {} + uuid@11.1.0: {} uuid@9.0.1: {} @@ -13002,8 +13451,6 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 - value-or-promise@1.0.12: {} - vary@1.1.2: {} vfile-location@5.0.3: @@ -13053,7 +13500,7 @@ snapshots: dependencies: makeerror: 1.0.12 - watchpack@2.4.2: + watchpack@2.5.1: dependencies: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 @@ -13066,48 +13513,43 @@ snapshots: web-worker@1.5.0: {} - webidl-conversions@3.0.1: {} - webpack-node-externals@3.0.0: {} - webpack-sources@3.2.3: {} + webpack-sources@3.3.4: {} - webpack@5.98.0: + webpack@5.104.1: dependencies: '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.14.1 - browserslist: 4.24.4 + acorn: 8.16.0 + acorn-import-phases: 1.0.4(acorn@8.16.0) + browserslist: 4.28.1 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.1 - es-module-lexer: 1.6.0 + es-module-lexer: 2.0.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.0 + loader-runner: 4.3.1 mime-types: 2.1.35 neo-async: 2.6.2 - schema-utils: 4.3.0 - tapable: 2.2.1 - terser-webpack-plugin: 5.3.14(webpack@5.98.0) - watchpack: 2.4.2 - webpack-sources: 3.2.3 + schema-utils: 4.3.3 + tapable: 2.3.2 + terser-webpack-plugin: 5.4.0(webpack@5.104.1) + watchpack: 2.5.1 + webpack-sources: 3.3.4 transitivePeerDependencies: - '@swc/core' - esbuild - uglify-js - whatwg-mimetype@3.0.0: {} - - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 + whatwg-mimetype@4.0.0: {} which-boxed-primitive@1.1.1: dependencies: @@ -13160,6 +13602,8 @@ snapshots: word-wrap@1.2.5: {} + wordwrap@1.0.0: {} + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -13187,7 +13631,7 @@ snapshots: ws@7.5.10: {} - ws@8.18.1: {} + ws@8.19.0: {} xss@1.0.15: dependencies: @@ -13202,8 +13646,6 @@ snapshots: yallist@3.1.1: {} - yallist@4.0.0: {} - yaml@2.7.1: {} yargs-parser@21.1.1: {} @@ -13224,6 +13666,8 @@ snapshots: yoctocolors-cjs@2.1.2: {} + yoctocolors-cjs@2.1.3: {} + zod@3.24.2: {} zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c0329a9e..76ac7813 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,3 @@ packages: - "apps/*" + - "lib/*" diff --git a/prettier.config.js b/prettier.config.js deleted file mode 100644 index 07e559c0..00000000 --- a/prettier.config.js +++ /dev/null @@ -1,8 +0,0 @@ -/** @type {import('prettier').Config} */ -module.exports = { - trailingComma: "all", - semi: true, - endOfLine: "lf", - tabWidth: 4, - useTabs: false, -};