From 314eb833e2b50f98bbc207be5e579d584c452799 Mon Sep 17 00:00:00 2001 From: mustafazeydani Date: Thu, 17 Sep 2026 22:55:26 +0300 Subject: [PATCH] fix(deploy): bound production resources and support restricted hosts --- .env.example | 4 + .github/workflows/deploy-production.yml | 10 +- README.md | 2 + devops/production/.infisical.env.example | 2 +- devops/production/README.md | 25 ++- devops/production/bin/backup-postgres.sh | 6 + devops/production/bin/deploy.sh | 9 +- devops/production/docker-compose.app.yml | 20 ++- docs/deployment.md | 38 ++++- docs/read-model-architecture.md | 2 + pnpm-lock.yaml | 172 ++++++++++---------- pnpm-workspace.yaml | 13 +- src/config/database/database-config.type.ts | 1 + src/config/database/database.config.ts | 2 + src/database/prisma.service.ts | 2 + 15 files changed, 197 insertions(+), 111 deletions(-) diff --git a/.env.example b/.env.example index 4ec0a10..b50285a 100644 --- a/.env.example +++ b/.env.example @@ -33,3 +33,7 @@ REDIS_REQUIRED=false CACHE_TTL_SECONDS=300 THROTTLE_FREE_TIER_DAILY_LIMIT=500 THROTTLE_FREE_TIER_DAILY_TTL_SECONDS=86400 + +# Production container limits and SSH destination are managed by deployment +# Compose and GitHub production-environment settings, not public app config. +DATABASE_POOL_MAX=4 diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 3335dd8..559e25b 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -207,7 +207,7 @@ jobs: echo "runtime-image=${IMAGE}@${DIGEST}" >> "$GITHUB_OUTPUT" deploy: - name: Deploy to syr-prod + name: Deploy to configured production host needs: - deploy-policy - build @@ -242,8 +242,8 @@ jobs: DEPLOY_SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }} DEPLOY_SSH_KNOWN_HOSTS: ${{ secrets.DEPLOY_SSH_KNOWN_HOSTS }} run: | - test "$DEPLOY_HOST" = "syr-prod" - test "$DEPLOY_USER" = "mustafa" + [[ "$DEPLOY_HOST" =~ ^[a-zA-Z0-9][a-zA-Z0-9.-]*$ ]] + [[ "$DEPLOY_USER" =~ ^[a-z_][a-z0-9_-]*$ ]] test "${CONFIGURED_DEPLOY_ROOT:-$DEPLOY_ROOT}" = "$DEPLOY_ROOT" [[ "$RELEASE_SHA" =~ ^[0-9a-f]{40}$ ]] test -n "$DEPLOY_SSH_PRIVATE_KEY" @@ -256,7 +256,7 @@ jobs: oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} audience: ${{ secrets.TS_AUDIENCE }} tags: tag:ci - ping: syr-prod + ping: ${{ vars.DEPLOY_HOST }} - name: Install SSH credentials shell: bash @@ -282,7 +282,7 @@ jobs: -o IdentitiesOnly=yes \ -o StrictHostKeyChecking=yes \ "${DEPLOY_USER}@${DEPLOY_HOST}" \ - 'sudo -n /usr/bin/install -d -m 0750 -o mustafa -g mustafa /opt/syr/apps/opensyria /opt/syr/apps/opensyria/production /opt/syr/apps/opensyria/production/website /opt/syr/apps/opensyria/production/datasets-api' + 'root=/opt/syr/apps/opensyria/production/datasets-api; test -d "$root" && test ! -L "$root" && test -w "$root"' ssh -i ~/.ssh/opensyria_deploy \ -o BatchMode=yes \ -o IdentitiesOnly=yes \ diff --git a/README.md b/README.md index 1aade0e..fbd5bdd 100644 --- a/README.md +++ b/README.md @@ -376,3 +376,5 @@ Community contribution is intended primarily for the dataset repositories, where ## License MIT + +`DATABASE_POOL_MAX` bounds each API process and importer to 1–20 PostgreSQL connections (default 4). Connection acquisition times out after five seconds, so pool exhaustion fails promptly instead of accumulating unbounded waits. Production fixes the API pool at four connections. diff --git a/devops/production/.infisical.env.example b/devops/production/.infisical.env.example index c40f600..bacc94e 100644 --- a/devops/production/.infisical.env.example +++ b/devops/production/.infisical.env.example @@ -1,4 +1,4 @@ -# Raw loopback API on syr-prod. The browser-facing endpoint is protected by +# Raw loopback API on the configured production host. The browser-facing endpoint is protected by # nginx Basic Auth and must not be used by host automation. INFISICAL_API_URL=http://127.0.0.1:14001 INFISICAL_CLIENT_ID=enter-production-api-client-id diff --git a/devops/production/README.md b/devops/production/README.md index 98d91b8..f79c5b1 100644 --- a/devops/production/README.md +++ b/devops/production/README.md @@ -38,7 +38,7 @@ commit SHA. Do not run server-side builds or write runtime secrets into changes wait for concurrent OpenSyria rollouts, and private verification retries briefly while old nginx workers drain after a graceful reload. -Every pre-migration backup is validated with `pg_restore --list` and receives +On unmanaged legacy hosts, each pre-migration backup is validated with `pg_restore --list` and receives checksum and recovery sidecars. Destructive recovery is deliberately separate: ```bash @@ -54,3 +54,26 @@ explicit confirmation it resets only `opensyria_datasets_production`, restores its isolation and PostGIS extension, then restores the archive as the application role. This removes post-backup objects instead of leaving them behind beside older Prisma migration history. + +## Restricted production host deployment + +The production GitHub environment selects `DEPLOY_HOST`, `DEPLOY_USER`, the SSH +key and its pinned known-hosts entry. The host must be provisioned in advance; +CI only verifies the application directory and cannot create directories with +unrestricted sudo. The deployment identity must have only the fixed Docker +operations for this application. Keep automatic deployment paused while moving +data and use `VERIFY_PUBLIC_DEPLOYMENT=false` for the private cutover checks. +Set it back to `true` when the public route points to the prepared destination. + +The long-running application has a 1 CPU burst ceiling and 512 MiB memory/swap +ceiling, with Node heap capped at 320 MiB. These limits apply to each blue/green +slot; allow temporary overlap during a rollout. + +The API PostgreSQL pool defaults to four connections per process; +`DATABASE_POOL_MAX` accepts 1–20 and production Compose pins it to four. +Connection acquisition is bounded to five seconds. Database readiness uses a +fixed host operation, so its container name need not match the application DNS +alias. Migration jobs are capped at 1 CPU/512 MiB, sync/import at 1 CPU/768 MiB. +On a managed host, the fixed `opensyria-production-backup` sudo hook performs an +encrypted, verified off-host pre-deployment backup. Its absence retains the +legacy local dump path, which is not sufficient by itself for disaster recovery. diff --git a/devops/production/bin/backup-postgres.sh b/devops/production/bin/backup-postgres.sh index 7d70c3a..c3812e7 100755 --- a/devops/production/bin/backup-postgres.sh +++ b/devops/production/bin/backup-postgres.sh @@ -36,6 +36,12 @@ cleanup() { trap cleanup EXIT main() { + # Managed hosts supply an encrypted off-host backup with fixed root authority. + # No arguments or credentials are accepted by this hook. + if [[ -x /usr/local/sbin/opensyria-production-backup ]]; then + sudo -n /usr/local/sbin/opensyria-production-backup + return + fi command -v flock >/dev/null 2>&1 || fail "flock is required" command -v sha256sum >/dev/null 2>&1 || fail "sha256sum is required" [[ -f "${DOCKER_WRAPPER}" && -x "${DOCKER_WRAPPER}" && ! -L "${DOCKER_WRAPPER}" ]] \ diff --git a/devops/production/bin/deploy.sh b/devops/production/bin/deploy.sh index 715847b..2a5fd2b 100755 --- a/devops/production/bin/deploy.sh +++ b/devops/production/bin/deploy.sh @@ -17,10 +17,9 @@ ACTIVE_RELEASE_FILE="${STATE_DIR}/active-release" PENDING_FILE="${STATE_DIR}/pending.env" PREVIOUS_UPSTREAM_FILE="${STATE_DIR}/previous-upstream.conf" DEPLOY_LOCK_FILE="${ROOT_DIR}/.deploy.lock" -NGINX_DEPLOY_LOCK_FILE="${SERVER_SERVICES_ROOT}/.nginx-deploy.lock" -NGINX_ACTIVE_INCLUDE="${SERVER_SERVICES_ROOT}/infrastructure/nginx/conf.d/includes/opensyria-production-api-active.conf" +NGINX_DEPLOY_LOCK_FILE="${SERVER_SERVICES_ROOT}/infrastructure/nginx/conf.d/includes/opensyria/.deploy.lock" +NGINX_ACTIVE_INCLUDE="${SERVER_SERVICES_ROOT}/infrastructure/nginx/conf.d/includes/opensyria/opensyria-production-api-active.conf" NGINX_CONTAINER="infra-nginx" -POSTGRES_CONTAINER="infra-postgres" REDIS_CONTAINER="opensyria-production-redis" EDGE_NETWORK="syr-staging-edge" DATA_NETWORK="opensyria-production-data" @@ -551,8 +550,8 @@ prepare_release() { || fail "External Docker network ${EDGE_NETWORK} is missing" docker_cmd network-exists "${DATA_NETWORK}" >/dev/null \ || fail "External Docker network ${DATA_NETWORK} is missing" - container_is_running "${POSTGRES_CONTAINER}" \ - || fail "Shared PostgreSQL container is missing" + docker_cmd opensyria-database-state >/dev/null \ + || fail "OpenSyria PostgreSQL database is unavailable" container_is_running "${REDIS_CONTAINER}" \ || fail "Dedicated OpenSyria Redis container is missing" diff --git a/devops/production/docker-compose.app.yml b/devops/production/docker-compose.app.yml index e24c1a0..d14bc37 100644 --- a/devops/production/docker-compose.app.yml +++ b/devops/production/docker-compose.app.yml @@ -26,7 +26,8 @@ x-api-defaults: &api-defaults env_file: - ./env/api.env cpus: "1.0" - mem_limit: 1g + mem_limit: 512m + memswap_limit: 512m volumes: - ./data/releases:/app/data/releases:ro networks: @@ -50,6 +51,8 @@ services: container_name: opensyria-production-api-blue environment: APP_RELEASE: ${API_BLUE_RELEASE:?API_BLUE_RELEASE is required} + NODE_OPTIONS: --max-old-space-size=320 + DATABASE_POOL_MAX: "4" HOME: /tmp XDG_CACHE_HOME: /tmp/.cache networks: @@ -66,6 +69,8 @@ services: container_name: opensyria-production-api-green environment: APP_RELEASE: ${API_GREEN_RELEASE:?API_GREEN_RELEASE is required} + NODE_OPTIONS: --max-old-space-size=320 + DATABASE_POOL_MAX: "4" HOME: /tmp XDG_CACHE_HOME: /tmp/.cache networks: @@ -83,7 +88,8 @@ services: - ./env/migrate.env restart: "no" cpus: "1.0" - mem_limit: 1g + mem_limit: 512m + memswap_limit: 512m networks: - data environment: @@ -101,8 +107,9 @@ services: <<: *service-defaults image: ${OPERATIONS_IMAGE:?OPERATIONS_IMAGE is required} restart: "no" - cpus: "2.0" - mem_limit: 1536m + cpus: "1.0" + mem_limit: 768m + memswap_limit: 768m pids_limit: 384 env_file: - ./env/datasets.env @@ -119,8 +126,9 @@ services: <<: *service-defaults image: ${OPERATIONS_IMAGE:?OPERATIONS_IMAGE is required} restart: "no" - cpus: "2.0" - mem_limit: 1536m + cpus: "1.0" + mem_limit: 768m + memswap_limit: 768m pids_limit: 384 env_file: - ./env/import.env diff --git a/docs/deployment.md b/docs/deployment.md index 1811484..a539dca 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -2,7 +2,7 @@ `datasets-api` is deployed directly to production at `api.opensyria.org`. The application bundle is separate from the website bundle, while both use the -shared host platform on `syr-prod`. +shared production host selected by the GitHub environment. ## Architecture @@ -64,7 +64,7 @@ read-only, path-scoped Universal Auth identity stored at: /opt/syr/apps/opensyria/production/datasets-api/.infisical.env ``` -The file must be owned by `mustafa`, mode `0600`, and contain only Infisical +The file must be owned by the configured deployment account, mode `0600`, and contain only Infisical connection/identity settings. `bin/deploy.sh` obtains a short-lived token from the loopback Infisical API and exports `/datasets-api` to a mode-`0600` runtime env file. It parses `.infisical.env` with an exact key allowlist instead of @@ -84,8 +84,8 @@ DEPLOY_SSH_KNOWN_HOSTS Required GitHub environment variables: ```text -DEPLOY_HOST=syr-prod -DEPLOY_USER=mustafa +DEPLOY_HOST= +DEPLOY_USER= DEPLOY_ROOT=/opt/syr/apps/opensyria/production/datasets-api ``` @@ -165,11 +165,12 @@ Do not grant the application role superuser or extension-creation privileges. The workflow and `devops/production/bin/deploy.sh` perform these steps: -1. Validate the exact host, user, path, protected Infisical file, networks, and +1. Validate the configured host/user, fixed path, protected Infisical file, networks, and infrastructure containers. 2. Pull the immutable image digest with short-lived GHCR authentication. -3. Take a custom-format pre-migration dump in - `/opt/syr/backups/production/opensyria/postgres`. +3. Run the fixed managed-host backup hook and require verified encrypted off-host + recovery artifacts before any migration. The legacy unmanaged-host fallback + writes a custom-format dump in `/opt/syr/backups/production/opensyria/postgres`. 4. Run `prisma migrate deploy` from the runtime image. 5. Sync every exact pin in `dataset-releases.json`; the GitHub token is scoped to this job only. @@ -251,3 +252,26 @@ Tunnel. Nginx supplies production security headers, preserves the client IP contract, and marks the API host `noindex`. API documentation and OpenAPI routes remain public by design. Do not cache health, API JSON, documentation, or OpenAPI responses at Cloudflare; cache immutable website assets separately. + +## Restricted production host deployment + +The production GitHub environment selects `DEPLOY_HOST`, `DEPLOY_USER`, the SSH +key and its pinned known-hosts entry. The host must be provisioned in advance; +CI only verifies the application directory and cannot create directories with +unrestricted sudo. The deployment identity must have only the fixed Docker +operations for this application. Keep automatic deployment paused while moving +data and use `VERIFY_PUBLIC_DEPLOYMENT=false` for the private cutover checks. +Set it back to `true` when the public route points to the prepared destination. + +The long-running application has a 1 CPU burst ceiling and 512 MiB memory/swap +ceiling, with Node heap capped at 320 MiB. These limits apply to each blue/green +slot; allow temporary overlap during a rollout. + +The API PostgreSQL pool defaults to four connections per process; +`DATABASE_POOL_MAX` accepts 1–20 and production Compose pins it to four. +Connection acquisition is bounded to five seconds. Database readiness uses a +fixed host operation, so its container name need not match the application DNS +alias. Migration jobs are capped at 1 CPU/512 MiB, sync/import at 1 CPU/768 MiB. +On a managed host, the fixed `opensyria-production-backup` sudo hook performs an +encrypted, verified off-host pre-deployment backup. Its absence retains the +legacy local dump path, which is not sufficient by itself for disaster recovery. diff --git a/docs/read-model-architecture.md b/docs/read-model-architecture.md index e884278..50897e3 100644 --- a/docs/read-model-architecture.md +++ b/docs/read-model-architecture.md @@ -130,3 +130,5 @@ For already-built Docker/runtime environments, use the `:prod` scripts so the co pnpm run datasets:sync:prod DATABASE_ENABLED=true pnpm run read-model:import:geography:prod ``` + +`DATABASE_POOL_MAX` bounds each API process and importer to 1–20 PostgreSQL connections (default 4). Connection acquisition times out after five seconds, so pool exhaustion fails promptly instead of accumulating unbounded waits. Production fixes the API pool at four connections. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee17f3a..13a2af0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,14 +5,19 @@ settings: excludeLinksFromLockfile: false overrides: + baseline-browser-mapping@>=2.0.0 <2.11.0: 2.11.0 + browserslist@<=4.28.6: 4.28.7 + mysql2@<3.23.1: 3.23.1 + qs@>=6.0.0 <6.16.0: 6.16.0 + fastify@>=5.0.0 <5.12.1: 5.12.1 brace-expansion: 5.0.9 deepmerge-ts: 8.0.0 - fast-uri: 3.1.5 + fast-uri: 3.1.6 find-my-way@<=9.6.0: 9.7.0 - js-yaml@>=3.0.0 <3.15.1: 3.15.1 - js-yaml@>=4.0.0 <4.3.1: 4.3.1 + js-yaml@>=3.0.0 <3.15.2: 3.15.2 + js-yaml@>=4.0.0 <4.3.2: 4.3.2 js-yaml@>=5.0.0 <=5.2.1: 5.2.2 - multer: 2.2.0 + multer: 2.3.0 picomatch@4.0.5: 4.0.4 type-fest@5.8.0: 5.7.0 valibot@<=1.4.1: 1.4.2 @@ -59,7 +64,7 @@ importers: version: 7.9.0 '@prisma/client': specifier: ^7.9.0 - version: 7.9.0(prisma@7.9.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(typescript@6.0.3) + version: 7.9.0(prisma@7.9.0(@types/node@26.1.0)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(typescript@6.0.3) '@scalar/nestjs-api-reference': specifier: ^1.2.11 version: 1.2.11 @@ -70,8 +75,8 @@ importers: specifier: ^17.4.2 version: 17.4.2 fastify: - specifier: ^5.10.0 - version: 5.10.0 + specifier: 5.12.1 + version: 5.12.1 ioredis: specifier: ^5.11.1 version: 5.11.1 @@ -98,7 +103,7 @@ importers: version: 11.0.0 prisma: specifier: 7.9.0 - version: 7.9.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + version: 7.9.0(@types/node@26.1.0)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) reflect-metadata: specifier: ^0.2.2 version: 0.2.2 @@ -1777,8 +1782,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.42: - resolution: {integrity: sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==} + baseline-browser-mapping@2.11.0: + resolution: {integrity: sha512-oCu2wfipvX3AePSgmOuKkIywOu+8n9psz7hXYmk56ghpu3+7KzNIBopaOs4c9BrtdnTtW30unG9GTfHo7EwERQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -1804,8 +1809,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.4: - resolution: {integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==} + browserslist@4.28.7: + resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -1861,8 +1866,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001800: - resolution: {integrity: sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==} + caniuse-lite@1.0.30001810: + resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -2221,8 +2226,8 @@ packages: effect@3.20.0: resolution: {integrity: sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==} - electron-to-chromium@1.5.387: - resolution: {integrity: sha512-TaxwufTFDufvPEoXdhwVrA3UdFWBeWGkYoJ1K8ldF1xe6gKfth6iRNS5lTQ5JPNOHdGQm8PT1QYKUqFLCiUefQ==} + electron-to-chromium@1.5.430: + resolution: {integrity: sha512-e1QEj72Y4zd8RlNZVmoTg+iCOSVwpk05IOiiQwdrkwCSVlZfPthevErhE+nckGd2YbsXfp1SkisznhGVIXP2NQ==} elkjs@0.11.1: resolution: {integrity: sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==} @@ -2377,8 +2382,8 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fast-uri@3.1.5: - resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} + fast-uri@3.1.6: + resolution: {integrity: sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==} fastify-plugin@5.1.0: resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==} @@ -2386,8 +2391,8 @@ packages: fastify-plugin@6.0.0: resolution: {integrity: sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==} - fastify@5.10.0: - resolution: {integrity: sha512-A9L0ziuWGQHgEEVgF3davQ9vbD93IuX+lo2IsxapQmu5b/Y/ynn9m9K5JHt9dvyJXOFc5iN0Zk5GHEOqnzhWjg==} + fastify@5.12.1: + resolution: {integrity: sha512-FWi+tQvwxR/PeRX7Z2mhfEF5ozJ3jn9asiiclzKXNSzJRHAYcU924aIOKAdHFJ+YIKieh3cqr1IwCOvTr41B3Q==} fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -2862,12 +2867,12 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@3.15.1: - resolution: {integrity: sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==} + js-yaml@3.15.2: + resolution: {integrity: sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==} hasBin: true - js-yaml@4.3.1: - resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true js-yaml@5.2.2: @@ -3076,17 +3081,19 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - multer@2.2.0: - resolution: {integrity: sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==} + multer@2.3.0: + resolution: {integrity: sha512-cjNbm3sttszgZeGfJR124D+jFEfkXCVAsoPBmFn9X7UxmDSFHWqE2CoEj0vrmSpuAFnqWR1Szcm9QTsiHr60Xw==} engines: {node: '>= 10.16.0'} mute-stream@2.0.0: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} - mysql2@3.15.3: - resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==} + mysql2@3.23.1: + resolution: {integrity: sha512-tTuRnC7qCet2IOfSNMYZ5SwXuBnfvBPAcIA28P0gtruXyZlU1LMxA6uha32kYypoFgyYklMqhLWwt4laYwXR/Q==} engines: {node: '>= 8.0'} + peerDependencies: + '@types/node': '>= 8' named-placeholders@1.1.6: resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} @@ -3161,8 +3168,8 @@ packages: node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - node-releases@2.0.50: - resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==} + node-releases@2.0.55: + resolution: {integrity: sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==} engines: {node: '>=18'} normalize-path@3.0.0: @@ -3403,6 +3410,9 @@ packages: process-warning@5.0.0: resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + process-warning@5.1.0: + resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==} + proper-lockfile@4.1.2: resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} @@ -3420,8 +3430,8 @@ packages: pure-rand@7.0.1: resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} - qs@6.15.3: - resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + qs@6.16.0: + resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==} engines: {node: '>=0.6'} quick-format-unescaped@4.0.4: @@ -3597,9 +3607,6 @@ packages: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} - seq-queue@0.0.5: - resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} - serve-static@2.2.1: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} @@ -3681,9 +3688,9 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - sqlstring@2.3.3: - resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} - engines: {node: '>= 0.6'} + sql-escaper@1.5.2: + resolution: {integrity: sha512-6CKD38c31SENivxOADeMNLdukOnUxUcflKtzVWzace7Riv1v7cAEym5Cx9Q7gYZ3ezIJI7ZpqecQq8cqNeSSFg==} + engines: {bun: '>=1.0.0', deno: '>=2.0.0', node: '>=12.0.0'} stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} @@ -4010,7 +4017,7 @@ packages: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: - browserslist: '>= 4.21.0' + browserslist: 4.28.7 uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -4246,7 +4253,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.7 '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.4 + browserslist: 4.28.7 lru-cache: 5.1.1 semver: 6.3.1 @@ -4595,7 +4602,7 @@ snapshots: dependencies: ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) - fast-uri: 3.1.5 + fast-uri: 3.1.6 '@fastify/cors@11.2.0': dependencies: @@ -4803,7 +4810,7 @@ snapshots: camelcase: 5.3.1 find-up: 4.1.0 get-package-type: 0.1.0 - js-yaml: 3.15.1 + js-yaml: 3.15.2 resolve-from: 5.0.0 '@istanbuljs/schema@0.1.6': {} @@ -5158,7 +5165,7 @@ snapshots: '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2) cors: 2.8.6 express: 5.2.1 - multer: 2.2.0 + multer: 2.3.0 path-to-regexp: 8.4.2 tslib: 2.8.1 transitivePeerDependencies: @@ -5172,7 +5179,7 @@ snapshots: '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2) fast-querystring: 1.1.2 - fastify: 5.10.0 + fastify: 5.12.1 fastify-plugin: 6.0.0 find-my-way: 9.7.0 light-my-request: 6.6.0 @@ -5256,11 +5263,11 @@ snapshots: '@prisma/client-runtime-utils@7.9.0': {} - '@prisma/client@7.9.0(prisma@7.9.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(typescript@6.0.3)': + '@prisma/client@7.9.0(prisma@7.9.0(@types/node@26.1.0)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(typescript@6.0.3)': dependencies: '@prisma/client-runtime-utils': 7.9.0 optionalDependencies: - prisma: 7.9.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + prisma: 7.9.0(@types/node@26.1.0)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) typescript: 6.0.3 '@prisma/config@7.9.0': @@ -5916,14 +5923,14 @@ snapshots: ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.5 + fast-uri: 3.1.6 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.5 + fast-uri: 3.1.6 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -6038,7 +6045,7 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.10.42: {} + baseline-browser-mapping@2.11.0: {} better-result@2.9.2: {} @@ -6058,7 +6065,7 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.7.3 on-finished: 2.4.1 - qs: 6.15.3 + qs: 6.16.0 raw-body: 3.0.2 type-is: 2.1.0 transitivePeerDependencies: @@ -6073,13 +6080,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.4: + browserslist@4.28.7: dependencies: - baseline-browser-mapping: 2.10.42 - caniuse-lite: 1.0.30001800 - electron-to-chromium: 1.5.387 - node-releases: 2.0.50 - update-browserslist-db: 1.2.3(browserslist@4.28.4) + baseline-browser-mapping: 2.11.0 + caniuse-lite: 1.0.30001810 + electron-to-chromium: 1.5.430 + node-releases: 2.0.55 + update-browserslist-db: 1.2.3(browserslist@4.28.7) bs-logger@0.2.6: dependencies: @@ -6140,7 +6147,7 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001800: {} + caniuse-lite@1.0.30001810: {} chalk@4.1.2: dependencies: @@ -6312,7 +6319,7 @@ snapshots: cosmiconfig@8.3.6(typescript@5.9.3): dependencies: import-fresh: 3.3.1 - js-yaml: 4.3.1 + js-yaml: 4.3.2 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: @@ -6322,7 +6329,7 @@ snapshots: dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 - js-yaml: 4.3.1 + js-yaml: 4.3.2 parse-json: 5.2.0 optionalDependencies: typescript: 6.0.3 @@ -6450,7 +6457,7 @@ snapshots: '@standard-schema/spec': 1.1.0 fast-check: 3.23.2 - electron-to-chromium@1.5.387: {} + electron-to-chromium@1.5.430: {} elkjs@0.11.1: {} @@ -6574,7 +6581,7 @@ snapshots: once: 1.4.0 parseurl: 1.3.3 proxy-addr: 2.0.7 - qs: 6.15.3 + qs: 6.16.0 range-parser: 1.3.0 router: 2.2.0 send: 1.2.1 @@ -6603,7 +6610,7 @@ snapshots: '@fastify/merge-json-schemas': 0.2.1 ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) - fast-uri: 3.1.5 + fast-uri: 3.1.6 json-schema-ref-resolver: 3.0.0 rfdc: 1.4.1 @@ -6615,13 +6622,13 @@ snapshots: fast-safe-stringify@2.1.1: {} - fast-uri@3.1.5: {} + fast-uri@3.1.6: {} fastify-plugin@5.1.0: {} fastify-plugin@6.0.0: {} - fastify@5.10.0: + fastify@5.12.1: dependencies: '@fastify/ajv-compiler': 4.0.5 '@fastify/error': 4.2.0 @@ -6633,7 +6640,7 @@ snapshots: find-my-way: 9.7.0 light-my-request: 6.6.0 pino: 10.3.1 - process-warning: 5.0.0 + process-warning: 5.1.0 rfdc: 1.4.1 secure-json-parse: 4.1.0 semver: 7.8.5 @@ -7320,12 +7327,12 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@3.15.1: + js-yaml@3.15.2: dependencies: argparse: 1.0.10 esprima: 4.0.1 - js-yaml@4.3.1: + js-yaml@4.3.2: dependencies: argparse: 2.0.1 @@ -7503,7 +7510,7 @@ snapshots: ms@2.1.3: {} - multer@2.2.0: + multer@2.3.0: dependencies: append-field: 1.0.0 busboy: 1.6.0 @@ -7513,8 +7520,9 @@ snapshots: mute-stream@2.0.0: {} - mysql2@3.15.3: + mysql2@3.23.1(@types/node@26.1.0): dependencies: + '@types/node': 26.1.0 aws-ssl-profiles: 1.1.2 denque: 2.1.0 generate-function: 2.3.1 @@ -7522,8 +7530,7 @@ snapshots: long: 5.3.2 lru.min: 1.1.4 named-placeholders: 1.1.6 - seq-queue: 0.0.5 - sqlstring: 2.3.3 + sql-escaper: 1.5.2 named-placeholders@1.1.6: dependencies: @@ -7580,7 +7587,7 @@ snapshots: node-int64@0.4.0: {} - node-releases@2.0.50: {} + node-releases@2.0.55: {} normalize-path@3.0.0: {} @@ -7793,17 +7800,18 @@ snapshots: react-is-18: react-is@18.3.1 react-is-19: react-is@19.2.7 - prisma@7.9.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3): + prisma@7.9.0(@types/node@26.1.0)(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3): dependencies: '@prisma/config': 7.9.0 '@prisma/dev': 0.24.14(typescript@6.0.3) '@prisma/engines': 7.9.0 '@prisma/studio-core': 0.33.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - mysql2: 3.15.3 + mysql2: 3.23.1(@types/node@26.1.0) postgres: 3.4.7 optionalDependencies: typescript: 6.0.3 transitivePeerDependencies: + - '@types/node' - '@types/react' - '@types/react-dom' - magicast @@ -7814,6 +7822,8 @@ snapshots: process-warning@5.0.0: {} + process-warning@5.1.0: {} + proper-lockfile@4.1.2: dependencies: graceful-fs: 4.2.11 @@ -7832,7 +7842,7 @@ snapshots: pure-rand@7.0.1: {} - qs@6.15.3: + qs@6.16.0: dependencies: es-define-property: 1.0.1 side-channel: 1.1.1 @@ -8003,8 +8013,6 @@ snapshots: - supports-color optional: true - seq-queue@0.0.5: {} - serve-static@2.2.1: dependencies: encodeurl: 2.0.0 @@ -8093,7 +8101,7 @@ snapshots: sprintf-js@1.0.3: {} - sqlstring@2.3.3: {} + sql-escaper@1.5.2: {} stack-utils@2.0.6: dependencies: @@ -8174,7 +8182,7 @@ snapshots: formidable: 3.5.4 methods: 1.1.2 mime: 2.6.0 - qs: 6.15.3 + qs: 6.16.0 transitivePeerDependencies: - supports-color @@ -8387,9 +8395,9 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 - update-browserslist-db@1.2.3(browserslist@4.28.4): + update-browserslist-db@1.2.3(browserslist@4.28.7): dependencies: - browserslist: 4.28.4 + browserslist: 4.28.7 escalade: 3.2.0 picocolors: 1.1.1 @@ -8440,7 +8448,7 @@ snapshots: '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.17.0 acorn-import-phases: 1.0.4(acorn@8.17.0) - browserslist: 4.28.4 + browserslist: 4.28.7 chrome-trace-event: 1.0.4 enhanced-resolve: 5.24.1 es-module-lexer: 2.3.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index de1f845..3faecf7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,14 +2,19 @@ packages: - . overrides: + 'baseline-browser-mapping@>=2.0.0 <2.11.0': 2.11.0 + 'browserslist@<=4.28.6': 4.28.7 + 'mysql2@<3.23.1': 3.23.1 + 'qs@>=6.0.0 <6.16.0': 6.16.0 + 'fastify@>=5.0.0 <5.12.1': 5.12.1 brace-expansion: 5.0.9 deepmerge-ts: 8.0.0 - fast-uri: 3.1.5 + fast-uri: 3.1.6 'find-my-way@<=9.6.0': 9.7.0 - 'js-yaml@>=3.0.0 <3.15.1': 3.15.1 - 'js-yaml@>=4.0.0 <4.3.1': 4.3.1 + 'js-yaml@>=3.0.0 <3.15.2': 3.15.2 + 'js-yaml@>=4.0.0 <4.3.2': 4.3.2 'js-yaml@>=5.0.0 <=5.2.1': 5.2.2 - multer: 2.2.0 + multer: 2.3.0 picomatch@4.0.5: 4.0.4 type-fest@5.8.0: 5.7.0 'valibot@<=1.4.1': 1.4.2 diff --git a/src/config/database/database-config.type.ts b/src/config/database/database-config.type.ts index d380b5e..f06be56 100644 --- a/src/config/database/database-config.type.ts +++ b/src/config/database/database-config.type.ts @@ -3,4 +3,5 @@ export type DatabaseConfig = { required: boolean; url: string | null; logQueries: boolean; + poolMax: number; }; diff --git a/src/config/database/database.config.ts b/src/config/database/database.config.ts index c53950c..01c1802 100644 --- a/src/config/database/database.config.ts +++ b/src/config/database/database.config.ts @@ -13,6 +13,7 @@ const envSchema = z.object({ DATABASE_ENABLED: booleanEnvSchema.optional(), DATABASE_REQUIRED: booleanEnvSchema.optional(), DATABASE_LOG_QUERIES: booleanEnvSchema.optional(), + DATABASE_POOL_MAX: z.coerce.number().int().min(1).max(20).default(4), }); function parseEnv() { @@ -43,6 +44,7 @@ export function getConfig(): DatabaseConfig { required, url: env.DATABASE_URL ?? null, logQueries: env.DATABASE_LOG_QUERIES ?? false, + poolMax: env.DATABASE_POOL_MAX, }; } diff --git a/src/database/prisma.service.ts b/src/database/prisma.service.ts index b5c0c84..f565791 100644 --- a/src/database/prisma.service.ts +++ b/src/database/prisma.service.ts @@ -136,6 +136,8 @@ export class PrismaService implements OnModuleInit, OnModuleDestroy { const client = new PrismaClient({ adapter: new PrismaPg({ connectionString: this.databaseConfig.url, + max: this.databaseConfig.poolMax, + connectionTimeoutMillis: 5_000, }), log: this.databaseConfig.logQueries ? ['query', 'warn', 'error'] : ['warn', 'error'], });