From 0815f702d2851cdabe49454c234542b4dff23e44 Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Mon, 17 Aug 2026 13:39:15 +0200 Subject: [PATCH] feat: add api liveness and readiness checks --- backend/apps/api/src/api.module.ts | 3 +- .../api/src/health/health.controller.spec.ts | 29 +++ .../apps/api/src/health/health.controller.ts | 17 ++ backend/apps/api/src/health/health.module.ts | 14 ++ .../apps/api/src/health/readiness.service.ts | 22 +++ backend/apps/api/src/main.ts | 8 +- backend/apps/worker/src/worker.module.ts | 3 +- backend/libs/infrastructure/src/index.ts | 2 + .../src/postgres/postgres.module.ts | 27 +++ .../infrastructure/src/redis/redis.module.ts | 27 +++ backend/package.json | 12 +- pnpm-lock.yaml | 168 ++++++++++++++++++ 12 files changed, 327 insertions(+), 5 deletions(-) create mode 100644 backend/apps/api/src/health/health.controller.spec.ts create mode 100644 backend/apps/api/src/health/health.controller.ts create mode 100644 backend/apps/api/src/health/health.module.ts create mode 100644 backend/apps/api/src/health/readiness.service.ts create mode 100644 backend/libs/infrastructure/src/index.ts create mode 100644 backend/libs/infrastructure/src/postgres/postgres.module.ts create mode 100644 backend/libs/infrastructure/src/redis/redis.module.ts diff --git a/backend/apps/api/src/api.module.ts b/backend/apps/api/src/api.module.ts index effae60..0f36524 100644 --- a/backend/apps/api/src/api.module.ts +++ b/backend/apps/api/src/api.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { ConfigurationModule } from '../../../libs/configuration/src'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { HealthModule } from './health/health.module'; @Module({ - imports: [ConfigurationModule], + imports: [ConfigurationModule, HealthModule], controllers: [AppController], providers: [AppService], }) diff --git a/backend/apps/api/src/health/health.controller.spec.ts b/backend/apps/api/src/health/health.controller.spec.ts new file mode 100644 index 0000000..2d0ee50 --- /dev/null +++ b/backend/apps/api/src/health/health.controller.spec.ts @@ -0,0 +1,29 @@ +import { Test } from '@nestjs/testing'; +import { HealthController } from './health.controller'; +import { ReadinessService } from './readiness.service'; + +describe('HealthController', () => { + it('returns liveness without dependency checks', async () => { + const readiness = { check: jest.fn() }; + const moduleRef = await Test.createTestingModule({ + controllers: [HealthController], + providers: [{ provide: ReadinessService, useValue: readiness }], + }).compile(); + + expect(moduleRef.get(HealthController).live()).toEqual({ status: 'ok' }); + expect(readiness.check).not.toHaveBeenCalled(); + }); + + it('delegates readiness to dependency checks', async () => { + const readiness = { check: jest.fn().mockResolvedValue({ status: 'ok' }) }; + const moduleRef = await Test.createTestingModule({ + controllers: [HealthController], + providers: [{ provide: ReadinessService, useValue: readiness }], + }).compile(); + + await expect(moduleRef.get(HealthController).ready()).resolves.toEqual({ + status: 'ok', + }); + expect(readiness.check).toHaveBeenCalledTimes(1); + }); +}); diff --git a/backend/apps/api/src/health/health.controller.ts b/backend/apps/api/src/health/health.controller.ts new file mode 100644 index 0000000..402c72d --- /dev/null +++ b/backend/apps/api/src/health/health.controller.ts @@ -0,0 +1,17 @@ +import { Controller, Get } from '@nestjs/common'; +import { ReadinessService } from './readiness.service'; + +@Controller() +export class HealthController { + constructor(private readonly readiness: ReadinessService) {} + + @Get('health/live') + live(): { status: 'ok' } { + return { status: 'ok' }; + } + + @Get('health/ready') + ready(): Promise<{ status: 'ok' }> { + return this.readiness.check(); + } +} diff --git a/backend/apps/api/src/health/health.module.ts b/backend/apps/api/src/health/health.module.ts new file mode 100644 index 0000000..d4af8b6 --- /dev/null +++ b/backend/apps/api/src/health/health.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { + PostgresModule, + RedisModule, +} from '../../../../libs/infrastructure/src'; +import { HealthController } from './health.controller'; +import { ReadinessService } from './readiness.service'; + +@Module({ + imports: [PostgresModule, RedisModule], + controllers: [HealthController], + providers: [ReadinessService], +}) +export class HealthModule {} diff --git a/backend/apps/api/src/health/readiness.service.ts b/backend/apps/api/src/health/readiness.service.ts new file mode 100644 index 0000000..a06de45 --- /dev/null +++ b/backend/apps/api/src/health/readiness.service.ts @@ -0,0 +1,22 @@ +import { Inject, Injectable } from '@nestjs/common'; +import type { Pool } from 'pg'; +import type Redis from 'ioredis'; +import { + POSTGRES_POOL, + REDIS_CLIENT, +} from '../../../../libs/infrastructure/src'; + +@Injectable() +export class ReadinessService { + constructor( + @Inject(POSTGRES_POOL) private readonly postgresPool: Pool, + @Inject(REDIS_CLIENT) private readonly redis: Redis, + ) {} + + async check(): Promise<{ status: 'ok' }> { + await this.postgresPool.query('SELECT 1'); + const pong = await this.redis.ping(); + if (pong !== 'PONG') throw new Error('Redis ping failed'); + return { status: 'ok' as const }; + } +} diff --git a/backend/apps/api/src/main.ts b/backend/apps/api/src/main.ts index 152170b..e5a0b4b 100644 --- a/backend/apps/api/src/main.ts +++ b/backend/apps/api/src/main.ts @@ -1,10 +1,16 @@ +import { RequestMethod } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { ApiModule } from './api.module'; export async function bootstrapApi(): Promise { const app = await NestFactory.create(ApiModule); app.enableShutdownHooks(); - app.setGlobalPrefix('api/v1'); + app.setGlobalPrefix('api/v1', { + exclude: [ + { path: 'health/live', method: RequestMethod.GET }, + { path: 'health/ready', method: RequestMethod.GET }, + ], + }); await app.listen(3000, '0.0.0.0'); } diff --git a/backend/apps/worker/src/worker.module.ts b/backend/apps/worker/src/worker.module.ts index 9a91269..e7d0b79 100644 --- a/backend/apps/worker/src/worker.module.ts +++ b/backend/apps/worker/src/worker.module.ts @@ -1,7 +1,8 @@ import { Module } from '@nestjs/common'; import { ConfigurationModule } from '../../../libs/configuration/src'; +import { PostgresModule, RedisModule } from '../../../libs/infrastructure/src'; @Module({ - imports: [ConfigurationModule], + imports: [ConfigurationModule, PostgresModule, RedisModule], }) export class WorkerModule {} diff --git a/backend/libs/infrastructure/src/index.ts b/backend/libs/infrastructure/src/index.ts new file mode 100644 index 0000000..20f67d2 --- /dev/null +++ b/backend/libs/infrastructure/src/index.ts @@ -0,0 +1,2 @@ +export * from './postgres/postgres.module'; +export * from './redis/redis.module'; diff --git a/backend/libs/infrastructure/src/postgres/postgres.module.ts b/backend/libs/infrastructure/src/postgres/postgres.module.ts new file mode 100644 index 0000000..b9b3036 --- /dev/null +++ b/backend/libs/infrastructure/src/postgres/postgres.module.ts @@ -0,0 +1,27 @@ +import { Inject, Injectable, Module, OnModuleDestroy } from '@nestjs/common'; +import { Pool } from 'pg'; +import { APP_ENVIRONMENT, AppEnvironment } from '../../../configuration/src'; + +export const POSTGRES_POOL = Symbol('POSTGRES_POOL'); + +export const postgresPoolProvider = { + provide: POSTGRES_POOL, + inject: [APP_ENVIRONMENT], + useFactory: (env: AppEnvironment) => + new Pool({ connectionString: env.databaseUrl }), +}; + +@Injectable() +class PostgresLifecycle implements OnModuleDestroy { + constructor(@Inject(POSTGRES_POOL) private readonly pool: Pool) {} + + async onModuleDestroy(): Promise { + await this.pool.end(); + } +} + +@Module({ + providers: [postgresPoolProvider, PostgresLifecycle], + exports: [postgresPoolProvider], +}) +export class PostgresModule {} diff --git a/backend/libs/infrastructure/src/redis/redis.module.ts b/backend/libs/infrastructure/src/redis/redis.module.ts new file mode 100644 index 0000000..9b87211 --- /dev/null +++ b/backend/libs/infrastructure/src/redis/redis.module.ts @@ -0,0 +1,27 @@ +import { Inject, Injectable, Module, OnModuleDestroy } from '@nestjs/common'; +import Redis from 'ioredis'; +import { APP_ENVIRONMENT, AppEnvironment } from '../../../configuration/src'; + +export const REDIS_CLIENT = Symbol('REDIS_CLIENT'); + +export const redisClientProvider = { + provide: REDIS_CLIENT, + inject: [APP_ENVIRONMENT], + useFactory: (env: AppEnvironment) => + new Redis(env.redisUrl, { lazyConnect: false }), +}; + +@Injectable() +class RedisLifecycle implements OnModuleDestroy { + constructor(@Inject(REDIS_CLIENT) private readonly client: Redis) {} + + async onModuleDestroy(): Promise { + await this.client.quit(); + } +} + +@Module({ + providers: [redisClientProvider, RedisLifecycle], + exports: [redisClientProvider], +}) +export class RedisModule {} diff --git a/backend/package.json b/backend/package.json index 77a5fb3..9fd959b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -26,6 +26,8 @@ "@nestjs/common": "^11.0.1", "@nestjs/core": "^11.0.1", "@nestjs/platform-express": "^11.0.1", + "ioredis": "^6.0.0", + "pg": "^8.23.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" }, @@ -38,6 +40,7 @@ "@types/express": "^5.0.0", "@types/jest": "^30.0.0", "@types/node": "^24.0.0", + "@types/pg": "^8.21.0", "@types/supertest": "^7.0.0", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", @@ -62,8 +65,13 @@ ], "rootDir": ".", "testRegex": ".*\\.spec\\.ts$", - "testPathIgnorePatterns": ["/node_modules/", "/dist/"], - "setupFiles": ["/test/setup-env.ts"], + "testPathIgnorePatterns": [ + "/node_modules/", + "/dist/" + ], + "setupFiles": [ + "/test/setup-env.ts" + ], "transform": { "^.+\\.(t|j)s$": "ts-jest" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 607bd49..cc0c36f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,6 +19,12 @@ importers: '@nestjs/platform-express': specifier: ^11.0.1 version: 11.2.1(@nestjs/common@11.2.1(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.2.1) + ioredis: + specifier: ^6.0.0 + version: 6.0.0 + pg: + specifier: ^8.23.0 + version: 8.23.0 reflect-metadata: specifier: ^0.2.2 version: 0.2.2 @@ -50,6 +56,9 @@ importers: '@types/node': specifier: ^24.0.0 version: 24.13.3 + '@types/pg': + specifier: ^8.21.0 + version: 8.21.0 '@types/supertest': specifier: ^7.0.0 version: 7.2.1 @@ -1011,6 +1020,9 @@ packages: '@types/node': optional: true + '@ioredis/commands@2.0.0': + resolution: {integrity: sha512-vrx0AE/T0h7cRZwfo1M39Cr+ZhZrkf0V8mQN75wucKCxCLD9l/VX6no3gFvrLqD1IlG/1LtzWovqEw3t0Vr9zg==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1880,6 +1892,9 @@ packages: '@types/node@24.13.3': resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + '@types/pg@8.21.0': + resolution: {integrity: sha512-AYdtudzabjLZgVgRZmAnU8bAnVUXzuJX2IYHeSIiIHm68olD+LgQYCGWdtcNYnP0uq9c4S4NibVG3Ni7VbKW7Q==} + '@types/qs@6.15.1': resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} @@ -2506,6 +2521,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + cluster-key-slot@1.1.1: + resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==} + engines: {node: '>=0.10.0'} + co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -2650,6 +2669,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -3185,6 +3208,10 @@ packages: resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} engines: {node: ^20.17.0 || >=22.9.0} + ioredis@6.0.0: + resolution: {integrity: sha512-f+Dtubxfpf6KYFq7WVXJoOLn0bk4TJrMrN9SzeE+jrWrCWj7XX3fA6vkryafhADX+GMymRxgDJDOI33COkJc0w==} + engines: {node: '>=20.0.0'} + ip-address@10.5.0: resolution: {integrity: sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==} engines: {node: '>= 12'} @@ -3906,6 +3933,40 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} + + pg-connection-string@2.14.0: + resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.16.0: + resolution: {integrity: sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.23.0: + resolution: {integrity: sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==} + 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==} @@ -3954,6 +4015,22 @@ packages: resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} 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'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -4016,6 +4093,10 @@ packages: resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} engines: {node: '>= 20.19.0'} + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -4211,6 +4292,10 @@ packages: spdx-license-ids@3.0.23: resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -4225,6 +4310,9 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -4781,6 +4869,10 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -5720,6 +5812,8 @@ snapshots: optionalDependencies: '@types/node': 24.13.3 + '@ioredis/commands@2.0.0': {} + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -6614,6 +6708,12 @@ snapshots: dependencies: undici-types: 7.18.2 + '@types/pg@8.21.0': + dependencies: + '@types/node': 24.13.3 + pg-protocol: 1.16.0 + pg-types: 2.2.0 + '@types/qs@6.15.1': {} '@types/range-parser@1.2.7': {} @@ -7305,6 +7405,8 @@ snapshots: clone@1.0.4: {} + cluster-key-slot@1.1.1: {} + co@4.6.0: {} collect-v8-coverage@1.0.3: {} @@ -7426,6 +7528,8 @@ snapshots: delayed-stream@1.0.0: {} + denque@2.1.0: {} + depd@2.0.0: {} detect-libc@2.1.2: @@ -8025,6 +8129,17 @@ snapshots: ini@6.0.0: {} + ioredis@6.0.0: + dependencies: + '@ioredis/commands': 2.0.0 + cluster-key-slot: 1.1.1 + debug: 4.4.3 + denque: 2.1.0 + redis-errors: 1.2.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + ip-address@10.5.0: {} ipaddr.js@1.9.1: {} @@ -8971,6 +9086,41 @@ snapshots: pathe@2.0.3: {} + pg-cloudflare@1.4.0: + optional: true + + pg-connection-string@2.14.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.23.0): + dependencies: + pg: 8.23.0 + + pg-protocol@1.16.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.23.0: + dependencies: + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.23.0) + pg-protocol: 1.16.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -9005,6 +9155,16 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.1: @@ -9059,6 +9219,8 @@ snapshots: readdirp@5.1.1: {} + redis-errors@1.2.0: {} + reflect-metadata@0.2.2: {} require-directory@2.1.1: {} @@ -9324,6 +9486,8 @@ snapshots: spdx-license-ids@3.0.23: {} + split2@4.2.0: {} + sprintf-js@1.0.3: {} ssri@13.0.1: @@ -9336,6 +9500,8 @@ snapshots: stackback@0.0.2: {} + standard-as-callback@2.1.0: {} + statuses@2.0.2: {} std-env@4.2.0: {} @@ -9858,6 +10024,8 @@ snapshots: xmlchars@2.2.0: {} + xtend@4.0.2: {} + y18n@5.0.8: {} yallist@3.1.1: {}