diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml index 23eaa52..3f2d312 100644 --- a/.github/workflows/server.yml +++ b/.github/workflows/server.yml @@ -32,9 +32,24 @@ jobs: # Both are read at module load, so they must be set for every step that # boots the Nest application (unit tests, e2e tests). CORS_ALLOWED_ORIGINS: http://localhost:5173 - # libSQL creates this file on first connection — no migration or service - # container is needed for the health check's `SELECT 1`. - DATABASE_URL: file:./dev.db + # Points at the Postgres service below. The e2e health check runs a real + # `SELECT 1`, so a live database is required. + DATABASE_URL: postgresql://agrocylo:agrocylo@localhost:5432/agrocylo_pip_test?schema=public + + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: agrocylo + POSTGRES_PASSWORD: agrocylo + POSTGRES_DB: agrocylo_pip_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U agrocylo -d agrocylo_pip_test" + --health-interval 5s + --health-timeout 5s + --health-retries 10 steps: - uses: actions/checkout@v4 @@ -66,6 +81,9 @@ jobs: - name: Build run: npm run build + - name: Apply database migrations + run: npm run db:migrate:deploy + - name: Run unit tests run: npm test diff --git a/server/.env.example b/server/.env.example index b1ae642..07d2625 100644 --- a/server/.env.example +++ b/server/.env.example @@ -21,8 +21,9 @@ WS_AUTH_SECRET= # One of: trace | debug | info | warn | error | fatal LOG_LEVEL=info -# Database -DATABASE_URL=file:./dev.db +# Database (PostgreSQL) +# Format: postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=public +DATABASE_URL=postgresql://agrocylo:agrocylo@localhost:55432/agrocylo_pip_dev?schema=public # Soroban RPC Configuration SOROBAN_RPC_URL=https://soroban-testnet.stellar.org diff --git a/server/docker-compose.yml b/server/docker-compose.yml new file mode 100644 index 0000000..c7c8abd --- /dev/null +++ b/server/docker-compose.yml @@ -0,0 +1,21 @@ +services: + postgres: + image: postgres:16-alpine + container_name: agrocylo-pg + restart: unless-stopped + environment: + POSTGRES_USER: agrocylo + POSTGRES_PASSWORD: agrocylo + POSTGRES_DB: agrocylo_pip_dev + ports: + - '55432:5432' + volumes: + - agrocylo-pg-data:/var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U agrocylo -d agrocylo_pip_dev'] + interval: 5s + timeout: 5s + retries: 10 + +volumes: + agrocylo-pg-data: diff --git a/server/package-lock.json b/server/package-lock.json index c638444..bec10de 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -10,7 +10,6 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@libsql/client": "^0.17.4", "@nestjs/common": "^10.4.4", "@nestjs/config": "^4.0.4", "@nestjs/core": "^10.4.4", @@ -19,7 +18,7 @@ "@nestjs/terminus": "^10.2.3", "@nestjs/throttler": "^6.5.0", "@nestjs/websockets": "^10.4.22", - "@prisma/adapter-libsql": "^7.8.0", + "@prisma/adapter-pg": "^7.9.1", "@prisma/client": "^7.8.0", "@stellar/stellar-sdk": "^16.0.1", "class-transformer": "^0.5.1", @@ -27,6 +26,7 @@ "helmet": "^8.3.0", "joi": "^17.13.3", "nestjs-pino": "^4.1.0", + "pg": "^8.23.0", "pino-http": "^10.3.0", "pino-pretty": "^11.2.2", "prisma": "^7.8.0", @@ -41,6 +41,7 @@ "@types/express": "^4.17.21", "@types/jest": "^29.5.13", "@types/node": "^22.7.4", + "@types/pg": "^8.23.1", "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^8.8.0", "@typescript-eslint/parser": "^8.8.0", @@ -1567,165 +1568,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@libsql/client": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@libsql/client/-/client-0.17.4.tgz", - "integrity": "sha512-lYayFWasDV78A+TjlEhr6ubb3odBV6OHjb+wdp8VQcyWWAEIjuwbCHaraEUS4m4yWoo0BvZo96It4VdzZRmRWw==", - "license": "MIT", - "dependencies": { - "@libsql/core": "^0.17.4", - "@libsql/hrana-client": "^0.10.0", - "js-base64": "^3.7.5", - "libsql": "^0.5.28", - "promise-limit": "^2.7.0" - } - }, - "node_modules/@libsql/core": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@libsql/core/-/core-0.17.4.tgz", - "integrity": "sha512-LqF9gIvnJ38nmAH1y/ChizHqDO/MO1wLgA96XrraulEEbqXxLjleSH92YWTolbuJKgPUmGu4aJk9W3UnAcxLOQ==", - "license": "MIT", - "dependencies": { - "js-base64": "^3.7.5" - } - }, - "node_modules/@libsql/darwin-arm64": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@libsql/darwin-arm64/-/darwin-arm64-0.5.29.tgz", - "integrity": "sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@libsql/darwin-x64": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@libsql/darwin-x64/-/darwin-x64-0.5.29.tgz", - "integrity": "sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@libsql/hrana-client": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@libsql/hrana-client/-/hrana-client-0.10.0.tgz", - "integrity": "sha512-OoA4EMqRAC7kn7V2P6EQqRcpZf2W+AjsNIyCizBg339Tq/aMC7sRnzs3SklderhmQWAqEzvv8A2vhxVmWpkVvw==", - "license": "MIT", - "dependencies": { - "@libsql/isomorphic-ws": "^0.1.5", - "js-base64": "^3.7.5" - } - }, - "node_modules/@libsql/isomorphic-ws": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@libsql/isomorphic-ws/-/isomorphic-ws-0.1.5.tgz", - "integrity": "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==", - "license": "MIT", - "dependencies": { - "@types/ws": "^8.5.4", - "ws": "^8.13.0" - } - }, - "node_modules/@libsql/linux-arm-gnueabihf": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@libsql/linux-arm-gnueabihf/-/linux-arm-gnueabihf-0.5.29.tgz", - "integrity": "sha512-CD4n4zj7SJTHso4nf5cuMoWoMSS7asn5hHygsDuhRl8jjjCTT3yE+xdUvI4J7zsyb53VO5ISh4cwwOtf6k2UhQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@libsql/linux-arm-musleabihf": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@libsql/linux-arm-musleabihf/-/linux-arm-musleabihf-0.5.29.tgz", - "integrity": "sha512-2Z9qBVpEJV7OeflzIR3+l5yAd4uTOLxklScYTwpZnkm2vDSGlC1PRlueLaufc4EFITkLKXK2MWBpexuNJfMVcg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@libsql/linux-arm64-gnu": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@libsql/linux-arm64-gnu/-/linux-arm64-gnu-0.5.29.tgz", - "integrity": "sha512-gURBqaiXIGGwFNEaUj8Ldk7Hps4STtG+31aEidCk5evMMdtsdfL3HPCpvys+ZF/tkOs2MWlRWoSq7SOuCE9k3w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@libsql/linux-arm64-musl": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@libsql/linux-arm64-musl/-/linux-arm64-musl-0.5.29.tgz", - "integrity": "sha512-fwgYZ0H8mUkyVqXZHF3mT/92iIh1N94Owi/f66cPVNsk9BdGKq5gVpoKO+7UxaNzuEH1roJp2QEwsCZMvBLpqg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@libsql/linux-x64-gnu": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@libsql/linux-x64-gnu/-/linux-x64-gnu-0.5.29.tgz", - "integrity": "sha512-y14V0vY0nmMC6G0pHeJcEarcnGU2H6cm21ZceRkacWHvQAEhAG0latQkCtoS2njFOXiYIg+JYPfAoWKbi82rkg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@libsql/linux-x64-musl": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@libsql/linux-x64-musl/-/linux-x64-musl-0.5.29.tgz", - "integrity": "sha512-gquqwA/39tH4pFl+J9n3SOMSymjX+6kZ3kWgY3b94nXFTwac9bnFNMffIomgvlFaC4ArVqMnOZD3nuJ3H3VO1w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@libsql/win32-x64-msvc": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@libsql/win32-x64-msvc/-/win32-x64-msvc-0.5.29.tgz", - "integrity": "sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@ljharb/through": { "version": "2.3.14", "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.14.tgz", @@ -1746,12 +1588,6 @@ "node": ">=8" } }, - "node_modules/@neon-rs/load": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@neon-rs/load/-/load-0.0.4.tgz", - "integrity": "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==", - "license": "MIT" - }, "node_modules/@nestjs/cli": { "version": "10.4.9", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.9.tgz", @@ -2299,15 +2135,31 @@ "url": "https://opencollective.com/pkgr" } }, - "node_modules/@prisma/adapter-libsql": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@prisma/adapter-libsql/-/adapter-libsql-7.8.0.tgz", - "integrity": "sha512-WnBGVMMlaehdVtWyUydCopL9WjGO77FZaaX0NWbW/Puu7iom3xNM9njpz57QBySbwJSLJMqoIbBPVGcjZ6jk/A==", + "node_modules/@prisma/adapter-pg": { + "version": "7.9.1", + "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.9.1.tgz", + "integrity": "sha512-Ho2RK1KanQxLNSC0sR5bpiiVep10sWPLXCcxK+KXfI/Q69TMRbiafSvLPv3V9snimX72rMCqGlyJ4sBO4lKTAw==", "license": "Apache-2.0", "dependencies": { - "@libsql/client": "^0.17.0", - "@prisma/driver-adapter-utils": "7.8.0", - "async-mutex": "0.5.0" + "@prisma/driver-adapter-utils": "7.9.1", + "@types/pg": "^8.16.0", + "pg": "^8.16.3", + "postgres-array": "3.0.4" + } + }, + "node_modules/@prisma/adapter-pg/node_modules/@prisma/debug": { + "version": "7.9.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.9.1.tgz", + "integrity": "sha512-/cpVZ4itxtcgB8GHBvZtcmuEjq+lWsLrRJxFMbwZrT1RIdtuKmUm7PPGo/wzfbYpBrk+9WmmBE8CHJw2rybKDQ==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/adapter-pg/node_modules/@prisma/driver-adapter-utils": { + "version": "7.9.1", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.9.1.tgz", + "integrity": "sha512-vmHehG7nn/heW32DXXpp13DxxAxVVe6n250oEt3dOL2E/4bt3olktKZN0mzSuxMMronyMSkbeW2uCOn3F4g8RQ==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.9.1" } }, "node_modules/@prisma/client": { @@ -2352,12 +2204,6 @@ "empathic": "2.0.0" } }, - "node_modules/@prisma/debug": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.8.0.tgz", - "integrity": "sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA==", - "license": "Apache-2.0" - }, "node_modules/@prisma/dev": { "version": "0.24.17", "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.17.tgz", @@ -2381,15 +2227,6 @@ "zeptomatch": "2.1.0" } }, - "node_modules/@prisma/driver-adapter-utils": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.8.0.tgz", - "integrity": "sha512-/Q13o0ZT0rjc1Xk0Q9KhZYwuq2EW/vSbWUBKfgEKkaCuB/Sg6bqnjmTZqC5cD4d6y1vfFAEwBRzfzoSMIVJ55A==", - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "7.8.0" - } - }, "node_modules/@prisma/engines": { "version": "7.9.1", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.9.1.tgz", @@ -3120,6 +2957,17 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/pg": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.23.1.tgz", + "integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/@types/qs": { "version": "6.15.1", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", @@ -4005,15 +3853,6 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true }, - "node_modules/async-mutex": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", - "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -5322,15 +5161,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -8034,12 +7864,6 @@ "node": ">=10" } }, - "node_modules/js-base64": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.8.0.tgz", - "integrity": "sha512-65kvbemyZhj+ExQt1PEFyBEjL5vAHysu1lJdW1AwhhChkO8ZBPizYk/m9GVrpbS2Je1hF+UYZ+6KywqtZV8mHw==", - "license": "BSD-3-Clause" - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -8180,38 +8004,6 @@ "integrity": "sha512-VNS5vWMM7r0P66BYv+TQJATxExEgLxN+34hfHDVhDkUsGAE4cRg0shCNSLTXNKm7nIUscC7AfB51TjxEeF7msQ==", "license": "MIT" }, - "node_modules/libsql": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/libsql/-/libsql-0.5.29.tgz", - "integrity": "sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg==", - "cpu": [ - "x64", - "arm64", - "wasm32", - "arm" - ], - "license": "MIT", - "os": [ - "darwin", - "linux", - "win32" - ], - "dependencies": { - "@neon-rs/load": "^0.0.4", - "detect-libc": "2.0.2" - }, - "optionalDependencies": { - "@libsql/darwin-arm64": "0.5.29", - "@libsql/darwin-x64": "0.5.29", - "@libsql/linux-arm-gnueabihf": "0.5.29", - "@libsql/linux-arm-musleabihf": "0.5.29", - "@libsql/linux-arm64-gnu": "0.5.29", - "@libsql/linux-arm64-musl": "0.5.29", - "@libsql/linux-x64-gnu": "0.5.29", - "@libsql/linux-x64-musl": "0.5.29", - "@libsql/win32-x64-msvc": "0.5.29" - } - }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -8977,6 +8769,104 @@ "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", "license": "MIT" }, + "node_modules/pg": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz", + "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz", + "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-types/node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -9255,6 +9145,45 @@ "url": "https://github.com/sponsors/porsager" } }, + "node_modules/postgres-array": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", + "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -9373,12 +9302,6 @@ } ] }, - "node_modules/promise-limit": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz", - "integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==", - "license": "ISC" - }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -12829,98 +12752,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "@libsql/client": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@libsql/client/-/client-0.17.4.tgz", - "integrity": "sha512-lYayFWasDV78A+TjlEhr6ubb3odBV6OHjb+wdp8VQcyWWAEIjuwbCHaraEUS4m4yWoo0BvZo96It4VdzZRmRWw==", - "requires": { - "@libsql/core": "^0.17.4", - "@libsql/hrana-client": "^0.10.0", - "js-base64": "^3.7.5", - "libsql": "^0.5.28", - "promise-limit": "^2.7.0" - } - }, - "@libsql/core": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@libsql/core/-/core-0.17.4.tgz", - "integrity": "sha512-LqF9gIvnJ38nmAH1y/ChizHqDO/MO1wLgA96XrraulEEbqXxLjleSH92YWTolbuJKgPUmGu4aJk9W3UnAcxLOQ==", - "requires": { - "js-base64": "^3.7.5" - } - }, - "@libsql/darwin-arm64": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@libsql/darwin-arm64/-/darwin-arm64-0.5.29.tgz", - "integrity": "sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A==", - "optional": true - }, - "@libsql/darwin-x64": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@libsql/darwin-x64/-/darwin-x64-0.5.29.tgz", - "integrity": "sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ==", - "optional": true - }, - "@libsql/hrana-client": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@libsql/hrana-client/-/hrana-client-0.10.0.tgz", - "integrity": "sha512-OoA4EMqRAC7kn7V2P6EQqRcpZf2W+AjsNIyCizBg339Tq/aMC7sRnzs3SklderhmQWAqEzvv8A2vhxVmWpkVvw==", - "requires": { - "@libsql/isomorphic-ws": "^0.1.5", - "js-base64": "^3.7.5" - } - }, - "@libsql/isomorphic-ws": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@libsql/isomorphic-ws/-/isomorphic-ws-0.1.5.tgz", - "integrity": "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==", - "requires": { - "@types/ws": "^8.5.4", - "ws": "^8.13.0" - } - }, - "@libsql/linux-arm-gnueabihf": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@libsql/linux-arm-gnueabihf/-/linux-arm-gnueabihf-0.5.29.tgz", - "integrity": "sha512-CD4n4zj7SJTHso4nf5cuMoWoMSS7asn5hHygsDuhRl8jjjCTT3yE+xdUvI4J7zsyb53VO5ISh4cwwOtf6k2UhQ==", - "optional": true - }, - "@libsql/linux-arm-musleabihf": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@libsql/linux-arm-musleabihf/-/linux-arm-musleabihf-0.5.29.tgz", - "integrity": "sha512-2Z9qBVpEJV7OeflzIR3+l5yAd4uTOLxklScYTwpZnkm2vDSGlC1PRlueLaufc4EFITkLKXK2MWBpexuNJfMVcg==", - "optional": true - }, - "@libsql/linux-arm64-gnu": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@libsql/linux-arm64-gnu/-/linux-arm64-gnu-0.5.29.tgz", - "integrity": "sha512-gURBqaiXIGGwFNEaUj8Ldk7Hps4STtG+31aEidCk5evMMdtsdfL3HPCpvys+ZF/tkOs2MWlRWoSq7SOuCE9k3w==", - "optional": true - }, - "@libsql/linux-arm64-musl": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@libsql/linux-arm64-musl/-/linux-arm64-musl-0.5.29.tgz", - "integrity": "sha512-fwgYZ0H8mUkyVqXZHF3mT/92iIh1N94Owi/f66cPVNsk9BdGKq5gVpoKO+7UxaNzuEH1roJp2QEwsCZMvBLpqg==", - "optional": true - }, - "@libsql/linux-x64-gnu": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@libsql/linux-x64-gnu/-/linux-x64-gnu-0.5.29.tgz", - "integrity": "sha512-y14V0vY0nmMC6G0pHeJcEarcnGU2H6cm21ZceRkacWHvQAEhAG0latQkCtoS2njFOXiYIg+JYPfAoWKbi82rkg==", - "optional": true - }, - "@libsql/linux-x64-musl": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@libsql/linux-x64-musl/-/linux-x64-musl-0.5.29.tgz", - "integrity": "sha512-gquqwA/39tH4pFl+J9n3SOMSymjX+6kZ3kWgY3b94nXFTwac9bnFNMffIomgvlFaC4ArVqMnOZD3nuJ3H3VO1w==", - "optional": true - }, - "@libsql/win32-x64-msvc": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@libsql/win32-x64-msvc/-/win32-x64-msvc-0.5.29.tgz", - "integrity": "sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg==", - "optional": true - }, "@ljharb/through": { "version": "2.3.14", "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.14.tgz", @@ -12935,11 +12766,6 @@ "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==" }, - "@neon-rs/load": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@neon-rs/load/-/load-0.0.4.tgz", - "integrity": "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==" - }, "@nestjs/cli": { "version": "10.4.9", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.9.tgz", @@ -13236,14 +13062,30 @@ "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", "dev": true }, - "@prisma/adapter-libsql": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@prisma/adapter-libsql/-/adapter-libsql-7.8.0.tgz", - "integrity": "sha512-WnBGVMMlaehdVtWyUydCopL9WjGO77FZaaX0NWbW/Puu7iom3xNM9njpz57QBySbwJSLJMqoIbBPVGcjZ6jk/A==", + "@prisma/adapter-pg": { + "version": "7.9.1", + "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.9.1.tgz", + "integrity": "sha512-Ho2RK1KanQxLNSC0sR5bpiiVep10sWPLXCcxK+KXfI/Q69TMRbiafSvLPv3V9snimX72rMCqGlyJ4sBO4lKTAw==", "requires": { - "@libsql/client": "^0.17.0", - "@prisma/driver-adapter-utils": "7.8.0", - "async-mutex": "0.5.0" + "@prisma/driver-adapter-utils": "7.9.1", + "@types/pg": "^8.16.0", + "pg": "^8.16.3", + "postgres-array": "3.0.4" + }, + "dependencies": { + "@prisma/debug": { + "version": "7.9.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.9.1.tgz", + "integrity": "sha512-/cpVZ4itxtcgB8GHBvZtcmuEjq+lWsLrRJxFMbwZrT1RIdtuKmUm7PPGo/wzfbYpBrk+9WmmBE8CHJw2rybKDQ==" + }, + "@prisma/driver-adapter-utils": { + "version": "7.9.1", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.9.1.tgz", + "integrity": "sha512-vmHehG7nn/heW32DXXpp13DxxAxVVe6n250oEt3dOL2E/4bt3olktKZN0mzSuxMMronyMSkbeW2uCOn3F4g8RQ==", + "requires": { + "@prisma/debug": "7.9.1" + } + } } }, "@prisma/client": { @@ -13270,11 +13112,6 @@ "empathic": "2.0.0" } }, - "@prisma/debug": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.8.0.tgz", - "integrity": "sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA==" - }, "@prisma/dev": { "version": "0.24.17", "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.17.tgz", @@ -13297,14 +13134,6 @@ "zeptomatch": "2.1.0" } }, - "@prisma/driver-adapter-utils": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.8.0.tgz", - "integrity": "sha512-/Q13o0ZT0rjc1Xk0Q9KhZYwuq2EW/vSbWUBKfgEKkaCuB/Sg6bqnjmTZqC5cD4d6y1vfFAEwBRzfzoSMIVJ55A==", - "requires": { - "@prisma/debug": "7.8.0" - } - }, "@prisma/engines": { "version": "7.9.1", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.9.1.tgz", @@ -13882,6 +13711,16 @@ "undici-types": "~6.21.0" } }, + "@types/pg": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.23.1.tgz", + "integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==", + "requires": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "@types/qs": { "version": "6.15.1", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", @@ -14575,14 +14414,6 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true }, - "async-mutex": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", - "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", - "requires": { - "tslib": "^2.4.0" - } - }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -15468,11 +15299,6 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, - "detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==" - }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -17462,11 +17288,6 @@ "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==" }, - "js-base64": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.8.0.tgz", - "integrity": "sha512-65kvbemyZhj+ExQt1PEFyBEjL5vAHysu1lJdW1AwhhChkO8ZBPizYk/m9GVrpbS2Je1hF+UYZ+6KywqtZV8mHw==" - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -17569,24 +17390,6 @@ "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.9.tgz", "integrity": "sha512-VNS5vWMM7r0P66BYv+TQJATxExEgLxN+34hfHDVhDkUsGAE4cRg0shCNSLTXNKm7nIUscC7AfB51TjxEeF7msQ==" }, - "libsql": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/libsql/-/libsql-0.5.29.tgz", - "integrity": "sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg==", - "requires": { - "@libsql/darwin-arm64": "0.5.29", - "@libsql/darwin-x64": "0.5.29", - "@libsql/linux-arm-gnueabihf": "0.5.29", - "@libsql/linux-arm-musleabihf": "0.5.29", - "@libsql/linux-arm64-gnu": "0.5.29", - "@libsql/linux-arm64-musl": "0.5.29", - "@libsql/linux-x64-gnu": "0.5.29", - "@libsql/linux-x64-musl": "0.5.29", - "@libsql/win32-x64-msvc": "0.5.29", - "@neon-rs/load": "^0.0.4", - "detect-libc": "2.0.2" - } - }, "lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -18129,6 +17932,73 @@ "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==" }, + "pg": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz", + "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==", + "requires": { + "pg-cloudflare": "^1.4.0", + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + } + }, + "pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "optional": true + }, + "pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==" + }, + "pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" + }, + "pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "requires": {} + }, + "pg-protocol": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz", + "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==" + }, + "pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "requires": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "dependencies": { + "postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==" + } + } + }, + "pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "requires": { + "split2": "^4.1.0" + } + }, "picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -18348,6 +18218,29 @@ "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==" }, + "postgres-array": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", + "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==" + }, + "postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==" + }, + "postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==" + }, + "postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "requires": { + "xtend": "^4.0.0" + } + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -18411,11 +18304,6 @@ "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==" }, - "promise-limit": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz", - "integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==" - }, "prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", diff --git a/server/package.json b/server/package.json index 65fa0bf..42fd927 100644 --- a/server/package.json +++ b/server/package.json @@ -17,6 +17,11 @@ "dev": "nest start --watch", "db:push": "prisma db push", "db:generate": "prisma generate", + "db:migrate": "prisma migrate dev", + "db:migrate:deploy": "prisma migrate deploy", + "db:migrate:status": "prisma migrate status", + "db:migrate:reset": "prisma migrate reset", + "db:studio": "prisma studio", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,test}/**/*.ts\"", @@ -25,7 +30,7 @@ "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --coverage", - "test:e2e": "jest --config ./test/jest-e2e.json" + "test:e2e": "jest --config ./test/jest-e2e.json --runInBand" }, "overrides": { "@stellar/stellar-sdk": { @@ -36,7 +41,6 @@ } }, "dependencies": { - "@libsql/client": "^0.17.4", "@nestjs/common": "^10.4.4", "@nestjs/config": "^4.0.4", "@nestjs/core": "^10.4.4", @@ -45,7 +49,7 @@ "@nestjs/terminus": "^10.2.3", "@nestjs/throttler": "^6.5.0", "@nestjs/websockets": "^10.4.22", - "@prisma/adapter-libsql": "^7.8.0", + "@prisma/adapter-pg": "^7.9.1", "@prisma/client": "^7.8.0", "@stellar/stellar-sdk": "^16.0.1", "class-transformer": "^0.5.1", @@ -53,6 +57,7 @@ "helmet": "^8.3.0", "joi": "^17.13.3", "nestjs-pino": "^4.1.0", + "pg": "^8.23.0", "pino-http": "^10.3.0", "pino-pretty": "^11.2.2", "prisma": "^7.8.0", @@ -67,6 +72,7 @@ "@types/express": "^4.17.21", "@types/jest": "^29.5.13", "@types/node": "^22.7.4", + "@types/pg": "^8.23.1", "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^8.8.0", "@typescript-eslint/parser": "^8.8.0", diff --git a/server/prisma/migrations/20260723224032_add_indexer_cursor_dispute_tranche_and_campaign_fields/migration.sql b/server/prisma/migrations/20260723224032_add_indexer_cursor_dispute_tranche_and_campaign_fields/migration.sql deleted file mode 100644 index 5684498..0000000 --- a/server/prisma/migrations/20260723224032_add_indexer_cursor_dispute_tranche_and_campaign_fields/migration.sql +++ /dev/null @@ -1,109 +0,0 @@ --- CreateTable -CREATE TABLE "Campaign" ( - "id" TEXT NOT NULL PRIMARY KEY, - "farmer" TEXT NOT NULL, - "title" TEXT NOT NULL DEFAULT '', - "description" TEXT NOT NULL DEFAULT '', - "targetAmount" BIGINT, - "tokenAddress" TEXT, - "deadline" BIGINT, - "status" TEXT NOT NULL DEFAULT 'Active', - "totalFunded" BIGINT NOT NULL DEFAULT 0, - "escrowContract" TEXT NOT NULL DEFAULT '', - "trancheCount" INTEGER, - "harvestOutcome" TEXT, - "harvestReportedAt" BIGINT, - "refundable" BIGINT, - "createdAt" BIGINT NOT NULL, - "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -); - --- CreateTable -CREATE TABLE "Investment" ( - "id" TEXT NOT NULL PRIMARY KEY, - "campaignId" TEXT NOT NULL, - "investor" TEXT NOT NULL, - "amount" BIGINT NOT NULL, - "txHash" TEXT, - "timestamp" BIGINT, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT "Investment_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "Campaign" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, - CONSTRAINT "Investment_investor_fkey" FOREIGN KEY ("investor") REFERENCES "User" ("address") ON DELETE RESTRICT ON UPDATE CASCADE -); - --- CreateTable -CREATE TABLE "Order" ( - "id" TEXT NOT NULL PRIMARY KEY, - "campaignId" TEXT, - "buyer" TEXT NOT NULL, - "amount" BIGINT, - "status" TEXT NOT NULL DEFAULT 'Pending', - "confirmedAt" BIGINT, - "txHash" TEXT, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL, - CONSTRAINT "Order_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "Campaign" ("id") ON DELETE SET NULL ON UPDATE CASCADE -); - --- CreateTable -CREATE TABLE "User" ( - "address" TEXT NOT NULL PRIMARY KEY, - "name" TEXT, - "firstSeenAt" BIGINT, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL -); - --- CreateTable -CREATE TABLE "Transaction" ( - "id" TEXT NOT NULL PRIMARY KEY, - "type" TEXT NOT NULL, - "campaignId" TEXT, - "userId" TEXT, - "amount" BIGINT, - "txHash" TEXT, - "status" TEXT NOT NULL DEFAULT 'Pending', - "timestamp" BIGINT, - "data" TEXT NOT NULL DEFAULT '{}', - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT "Transaction_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "Campaign" ("id") ON DELETE SET NULL ON UPDATE CASCADE, - CONSTRAINT "Transaction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("address") ON DELETE SET NULL ON UPDATE CASCADE -); - --- CreateTable -CREATE TABLE "Dispute" ( - "id" TEXT NOT NULL PRIMARY KEY, - "campaignId" TEXT NOT NULL, - "opener" TEXT NOT NULL, - "reason" TEXT NOT NULL, - "status" TEXT NOT NULL DEFAULT 'Open', - "resolution" TEXT, - "admin" TEXT, - "payoutToFarmer" BIGINT, - "refundableToInvestors" BIGINT, - "openedAt" BIGINT NOT NULL, - "resolvedAt" BIGINT, - "ledgerSequence" INTEGER, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL, - CONSTRAINT "Dispute_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "Campaign" ("id") ON DELETE RESTRICT ON UPDATE CASCADE -); - --- CreateTable -CREATE TABLE "Tranche" ( - "id" TEXT NOT NULL PRIMARY KEY, - "campaignId" TEXT NOT NULL, - "recipient" TEXT NOT NULL, - "amount" BIGINT NOT NULL, - "releasedAt" BIGINT NOT NULL, - "txHash" TEXT, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT "Tranche_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "Campaign" ("id") ON DELETE RESTRICT ON UPDATE CASCADE -); - --- CreateTable -CREATE TABLE "IndexerCursor" ( - "contractId" TEXT NOT NULL PRIMARY KEY, - "lastProcessedLedger" INTEGER NOT NULL, - "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -); diff --git a/server/prisma/migrations/20260819164517_init/migration.sql b/server/prisma/migrations/20260819164517_init/migration.sql new file mode 100644 index 0000000..2ba4922 --- /dev/null +++ b/server/prisma/migrations/20260819164517_init/migration.sql @@ -0,0 +1,139 @@ +-- CreateTable +CREATE TABLE "Campaign" ( + "id" TEXT NOT NULL, + "farmer" TEXT NOT NULL, + "title" TEXT NOT NULL DEFAULT '', + "description" TEXT NOT NULL DEFAULT '', + "targetAmount" BIGINT, + "tokenAddress" TEXT, + "deadline" BIGINT, + "status" TEXT NOT NULL DEFAULT 'Active', + "totalFunded" BIGINT NOT NULL DEFAULT 0, + "escrowContract" TEXT NOT NULL DEFAULT '', + "trancheCount" INTEGER, + "harvestOutcome" TEXT, + "harvestReportedAt" BIGINT, + "refundable" BIGINT NOT NULL DEFAULT 0, + "createdAt" BIGINT NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Campaign_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Investment" ( + "id" TEXT NOT NULL, + "campaignId" TEXT NOT NULL, + "investor" TEXT NOT NULL, + "amount" BIGINT NOT NULL, + "txHash" TEXT, + "timestamp" BIGINT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Investment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Order" ( + "id" TEXT NOT NULL, + "campaignId" TEXT, + "buyer" TEXT NOT NULL, + "amount" BIGINT, + "status" TEXT NOT NULL DEFAULT 'Pending', + "confirmedAt" BIGINT, + "txHash" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Order_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "User" ( + "address" TEXT NOT NULL, + "name" TEXT, + "firstSeenAt" BIGINT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("address") +); + +-- CreateTable +CREATE TABLE "Transaction" ( + "id" TEXT NOT NULL, + "type" TEXT NOT NULL, + "campaignId" TEXT, + "userId" TEXT, + "amount" BIGINT, + "txHash" TEXT, + "status" TEXT NOT NULL DEFAULT 'Pending', + "timestamp" BIGINT, + "data" TEXT NOT NULL DEFAULT '{}', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Transaction_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Dispute" ( + "id" TEXT NOT NULL, + "campaignId" TEXT NOT NULL, + "opener" TEXT NOT NULL, + "reason" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'Open', + "resolution" TEXT, + "admin" TEXT, + "payoutToFarmer" BIGINT, + "refundableToInvestors" BIGINT, + "openedAt" BIGINT NOT NULL, + "resolvedAt" BIGINT, + "ledgerSequence" INTEGER, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Dispute_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Tranche" ( + "id" TEXT NOT NULL, + "campaignId" TEXT NOT NULL, + "recipient" TEXT NOT NULL, + "amount" BIGINT NOT NULL, + "releasedAt" BIGINT NOT NULL, + "txHash" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Tranche_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "IndexerCursor" ( + "contractId" TEXT NOT NULL, + "lastProcessedLedger" INTEGER NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "IndexerCursor_pkey" PRIMARY KEY ("contractId") +); + +-- AddForeignKey +ALTER TABLE "Investment" ADD CONSTRAINT "Investment_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "Campaign"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Investment" ADD CONSTRAINT "Investment_investor_fkey" FOREIGN KEY ("investor") REFERENCES "User"("address") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Order" ADD CONSTRAINT "Order_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "Campaign"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Transaction" ADD CONSTRAINT "Transaction_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "Campaign"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Transaction" ADD CONSTRAINT "Transaction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("address") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Dispute" ADD CONSTRAINT "Dispute_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "Campaign"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Tranche" ADD CONSTRAINT "Tranche_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "Campaign"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/server/prisma/migrations/20260820115803_add_campaign_returnable/migration.sql b/server/prisma/migrations/20260820115803_add_campaign_returnable/migration.sql index cef3081..795070f 100644 --- a/server/prisma/migrations/20260820115803_add_campaign_returnable/migration.sql +++ b/server/prisma/migrations/20260820115803_add_campaign_returnable/migration.sql @@ -1,27 +1,2 @@ --- RedefineTables -PRAGMA defer_foreign_keys=ON; -PRAGMA foreign_keys=OFF; -CREATE TABLE "new_Campaign" ( - "id" TEXT NOT NULL PRIMARY KEY, - "farmer" TEXT NOT NULL, - "title" TEXT NOT NULL DEFAULT '', - "description" TEXT NOT NULL DEFAULT '', - "targetAmount" BIGINT, - "tokenAddress" TEXT, - "deadline" BIGINT, - "status" TEXT NOT NULL DEFAULT 'Active', - "totalFunded" BIGINT NOT NULL DEFAULT 0, - "escrowContract" TEXT NOT NULL DEFAULT '', - "trancheCount" INTEGER, - "harvestOutcome" TEXT, - "harvestReportedAt" BIGINT, - "refundable" BIGINT NOT NULL DEFAULT 0, - "returnable" BIGINT NOT NULL DEFAULT 0, - "createdAt" BIGINT NOT NULL, - "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -); -INSERT INTO "new_Campaign" ("createdAt", "deadline", "description", "escrowContract", "farmer", "harvestOutcome", "harvestReportedAt", "id", "refundable", "status", "targetAmount", "title", "tokenAddress", "totalFunded", "trancheCount", "updatedAt") SELECT "createdAt", "deadline", "description", "escrowContract", "farmer", "harvestOutcome", "harvestReportedAt", "id", coalesce("refundable", 0) AS "refundable", "status", "targetAmount", "title", "tokenAddress", "totalFunded", "trancheCount", "updatedAt" FROM "Campaign"; -DROP TABLE "Campaign"; -ALTER TABLE "new_Campaign" RENAME TO "Campaign"; -PRAGMA foreign_keys=ON; -PRAGMA defer_foreign_keys=OFF; +-- AlterTable +ALTER TABLE "Campaign" ADD COLUMN "returnable" BIGINT NOT NULL DEFAULT 0; diff --git a/server/prisma/migrations/migration_lock.toml b/server/prisma/migrations/migration_lock.toml index 2a5a444..044d57c 100644 --- a/server/prisma/migrations/migration_lock.toml +++ b/server/prisma/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually # It should be added in your version-control system (e.g., Git) -provider = "sqlite" +provider = "postgresql" diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index e493bb9..5d70420 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -4,7 +4,7 @@ generator client { } datasource db { - provider = "sqlite" + provider = "postgresql" } model Campaign { diff --git a/server/src/config/configuration.ts b/server/src/config/configuration.ts index 07dcc0a..eaa0fee 100644 --- a/server/src/config/configuration.ts +++ b/server/src/config/configuration.ts @@ -58,7 +58,7 @@ export default (): { authSecret: process.env.WS_AUTH_SECRET ?? '', }, db: { - url: process.env.DATABASE_URL ?? 'file:./dev.db', + url: process.env.DATABASE_URL ?? '', }, soroban: { rpcUrl: diff --git a/server/src/config/env.validation.spec.ts b/server/src/config/env.validation.spec.ts index 2d2e3af..6f96778 100644 --- a/server/src/config/env.validation.spec.ts +++ b/server/src/config/env.validation.spec.ts @@ -1,8 +1,15 @@ import { envValidationSchema } from './env.validation'; +// Minimal set of otherwise-required vars, so each test isolates the one +// rule it exercises instead of tripping over an unrelated "is required". +const baseEnv = { + DATABASE_URL: 'postgresql://user:pass@localhost:5432/agrocylo', +}; + describe('environment validation', () => { it('accepts a comma-separated CORS origin allowlist', () => { const result = envValidationSchema.validate({ + ...baseEnv, CORS_ALLOWED_ORIGINS: 'https://app.agrocylo.example,https://admin.agrocylo.example', }); @@ -14,6 +21,7 @@ describe('environment validation', () => { 'rejects a non-explicit CORS allowlist: %s', (corsAllowedOrigins) => { const result = envValidationSchema.validate({ + ...baseEnv, CORS_ALLOWED_ORIGINS: corsAllowedOrigins, }); @@ -22,4 +30,22 @@ describe('environment validation', () => { ); }, ); + + it('requires a postgres DATABASE_URL', () => { + const result = envValidationSchema.validate({ + CORS_ALLOWED_ORIGINS: 'https://app.agrocylo.example', + }); + + expect(result.error?.message).toContain('DATABASE_URL'); + }); + + it('rejects a non-postgres DATABASE_URL scheme', () => { + const result = envValidationSchema.validate({ + ...baseEnv, + DATABASE_URL: 'file:./dev.db', + CORS_ALLOWED_ORIGINS: 'https://app.agrocylo.example', + }); + + expect(result.error?.message).toContain('DATABASE_URL'); + }); }); diff --git a/server/src/config/env.validation.ts b/server/src/config/env.validation.ts index ea7d54a..4212da1 100644 --- a/server/src/config/env.validation.ts +++ b/server/src/config/env.validation.ts @@ -12,7 +12,9 @@ export const envValidationSchema = Joi.object({ LOG_LEVEL: Joi.string() .valid('trace', 'debug', 'info', 'warn', 'error', 'fatal') .default('info'), - DATABASE_URL: Joi.string().default('file:./dev.db'), + DATABASE_URL: Joi.string() + .uri({ scheme: ['postgres', 'postgresql'] }) + .required(), SOROBAN_RPC_URL: Joi.string() .uri() .default('https://soroban-testnet.stellar.org'), diff --git a/server/src/database/database.module.spec.ts b/server/src/database/database.module.spec.ts index 4ca4e09..237bc13 100644 --- a/server/src/database/database.module.spec.ts +++ b/server/src/database/database.module.spec.ts @@ -46,7 +46,8 @@ describe('DatabaseModule', () => { it('allows a non-file DATABASE_URL when NODE_ENV=production', async () => { process.env.NODE_ENV = 'production'; - process.env.DATABASE_URL = 'libsql://prod-db.example.com?authToken=secret'; + process.env.DATABASE_URL = + 'postgresql://user:secret@prod-db.example.com:5432/agrocylo'; const moduleRef = await buildModule(); diff --git a/server/src/database/database.module.ts b/server/src/database/database.module.ts index b9afa93..31d2316 100644 --- a/server/src/database/database.module.ts +++ b/server/src/database/database.module.ts @@ -1,7 +1,7 @@ import { Global, Module, OnModuleDestroy } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { PrismaClient } from '../../generated/prisma/client'; -import { PrismaLibSql } from '@prisma/adapter-libsql'; +import { PrismaPg } from '@prisma/adapter-pg'; /** * Refuses a `file:`-scheme DATABASE_URL (the local SQLite default) in @@ -27,7 +27,7 @@ export function assertDatabaseUrlAllowed(url: string, nodeEnv: string): void { const url = config.get('db.url')!; const nodeEnv = config.get('app.nodeEnv')!; assertDatabaseUrlAllowed(url, nodeEnv); - const adapter = new PrismaLibSql({ url }); + const adapter = new PrismaPg({ connectionString: url }); return new PrismaClient({ adapter }); }, }, diff --git a/server/src/indexer/soroban-event-listener.service.spec.ts b/server/src/indexer/soroban-event-listener.service.spec.ts index 6118128..023c71f 100644 --- a/server/src/indexer/soroban-event-listener.service.spec.ts +++ b/server/src/indexer/soroban-event-listener.service.spec.ts @@ -263,7 +263,9 @@ describe('SorobanEventListenerService', () => { // continuation cursor), then a short 20-event page that signals the // range is exhausted. const page1Events = Array.from({ length: 100 }, (_, i) => makeEvent(i)); - const page2Events = Array.from({ length: 20 }, (_, i) => makeEvent(100 + i)); + const page2Events = Array.from({ length: 20 }, (_, i) => + makeEvent(100 + i), + ); const getEvents = jest .fn() diff --git a/server/test/apply-migrations.ts b/server/test/apply-migrations.ts deleted file mode 100644 index aa550b5..0000000 --- a/server/test/apply-migrations.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { readdirSync, readFileSync } from 'node:fs'; -import path from 'node:path'; -import { PrismaClient } from '../generated/prisma/client'; - -const MIGRATIONS_DIR = path.join(__dirname, '..', 'prisma', 'migrations'); - -/** - * Materialise the full Prisma schema on a fresh SQLite database by replaying - * every checked-in migration in filename order. CI runs the e2e suite against - * an unmigrated database file, so specs that need real tables call this first. - * - * The libsql adapter executes only the first statement of a multi-statement - * string, so each migration is applied one statement at a time. - */ -export async function applyMigrations(client: PrismaClient): Promise { - const dirs = readdirSync(MIGRATIONS_DIR, { withFileTypes: true }) - .filter((entry) => entry.isDirectory()) - .map((entry) => entry.name) - .sort(); - - for (const dir of dirs) { - const sql = readFileSync( - path.join(MIGRATIONS_DIR, dir, 'migration.sql'), - 'utf8', - ); - const statements = sql - .split(';') - .map((statement) => statement.trim()) - .filter( - (statement) => statement.replace(/--.*$/gm, '').trim().length > 0, - ); - for (const statement of statements) { - await client.$executeRawUnsafe(statement); - } - } -} diff --git a/server/test/campaigns.e2e-spec.ts b/server/test/campaigns.e2e-spec.ts index ba18cb7..15776bb 100644 --- a/server/test/campaigns.e2e-spec.ts +++ b/server/test/campaigns.e2e-spec.ts @@ -1,14 +1,10 @@ -import { rmSync } from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import request from 'supertest'; -import { PrismaLibSql } from '@prisma/adapter-libsql'; import { AppModule } from './../src/app.module'; import { configureApp } from './../src/setup-app'; import { PrismaClient } from './../generated/prisma/client'; -import { applyMigrations } from './apply-migrations'; +import { resetDatabase } from './e2e-database'; describe('Campaigns & Investors API (e2e)', () => { let app: INestApplication; @@ -19,24 +15,7 @@ describe('Campaigns & Investors API (e2e)', () => { const campaignId = 'e2e-campaign-1'; const settledCampaignId = 'e2e-campaign-2'; - const dbPath = path.join( - os.tmpdir(), - `agro-campaigns-e2e-${process.pid}-${Date.now()}.db`, - ); - const previousDatabaseUrl = process.env.DATABASE_URL; - beforeAll(async () => { - // Point both this bootstrap and the app's own PrismaClient at an isolated, - // freshly migrated database so the assertions never see leftover state. - rmSync(dbPath, { force: true }); - process.env.DATABASE_URL = `file:${dbPath}`; - - const migrator = new PrismaClient({ - adapter: new PrismaLibSql({ url: `file:${dbPath}` }), - }); - await applyMigrations(migrator); - await migrator.$disconnect(); - const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); @@ -47,6 +26,10 @@ describe('Campaigns & Investors API (e2e)', () => { prisma = moduleFixture.get(PrismaClient); + // e2e specs share one Postgres schema, so start from a clean slate rather + // than assuming another file left the tables empty. + await resetDatabase(prisma); + await prisma.user.upsert({ where: { address: investor }, update: {}, @@ -138,12 +121,6 @@ describe('Campaigns & Investors API (e2e)', () => { afterAll(async () => { await app.close(); - rmSync(dbPath, { force: true }); - if (previousDatabaseUrl === undefined) { - delete process.env.DATABASE_URL; - } else { - process.env.DATABASE_URL = previousDatabaseUrl; - } }); it('GET /campaigns is paginated and filters by status', async () => { diff --git a/server/test/e2e-database.ts b/server/test/e2e-database.ts new file mode 100644 index 0000000..52e5cb5 --- /dev/null +++ b/server/test/e2e-database.ts @@ -0,0 +1,38 @@ +import { PrismaPg } from '@prisma/adapter-pg'; +import { PrismaClient } from '../generated/prisma/client'; + +// Child tables first so TRUNCATE ... CASCADE has nothing to complain about even +// if foreign-key checks are strict. +const TABLES = [ + 'Investment', + 'Tranche', + 'Dispute', + 'Order', + 'Transaction', + 'Campaign', + 'User', + 'IndexerCursor', +]; + +/** + * A `PrismaClient` bound to `DATABASE_URL` through the pg driver adapter — the + * same wiring `DatabaseModule` uses, so seeding and assertions hit exactly the + * database the application under test does. + */ +export function createTestPrismaClient(): PrismaClient { + return new PrismaClient({ + adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }), + }); +} + +/** + * Empties every table so a spec starts from a known-clean database. e2e specs + * share one Postgres schema and jest runs them serially (`--runInBand`), so a + * reset in `beforeAll` is enough to keep files from seeing each other's rows. + */ +export async function resetDatabase(prisma: PrismaClient): Promise { + const list = TABLES.map((table) => `"${table}"`).join(', '); + await prisma.$executeRawUnsafe( + `TRUNCATE TABLE ${list} RESTART IDENTITY CASCADE`, + ); +} diff --git a/server/test/global-setup.ts b/server/test/global-setup.ts new file mode 100644 index 0000000..c4a225c --- /dev/null +++ b/server/test/global-setup.ts @@ -0,0 +1,15 @@ +import { execFileSync } from 'node:child_process'; +import path from 'node:path'; + +/** + * Runs once before the e2e suite. Brings the Postgres database named by + * `DATABASE_URL` up to the latest schema so every spec can assume its tables + * exist. `prisma migrate deploy` is idempotent, so this is a no-op when CI has + * already run the migration step. + */ +export default function globalSetup(): void { + execFileSync('npx', ['prisma', 'migrate', 'deploy'], { + cwd: path.join(__dirname, '..'), + stdio: 'inherit', + }); +} diff --git a/server/test/indexer-lifecycle.e2e-spec.ts b/server/test/indexer-lifecycle.e2e-spec.ts index 5796c22..9185c84 100644 --- a/server/test/indexer-lifecycle.e2e-spec.ts +++ b/server/test/indexer-lifecycle.e2e-spec.ts @@ -1,11 +1,7 @@ -import { rmSync } from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; import { PrismaClient } from '../generated/prisma/client'; -import { PrismaLibSql } from '@prisma/adapter-libsql'; import { EventParserService } from '../src/indexer/parsers/event-parser.service'; import type { RawSorobanEvent } from '../src/indexer/types/soroban-events.types'; -import { applyMigrations } from './apply-migrations'; +import { createTestPrismaClient, resetDatabase } from './e2e-database'; const CAMPAIGN_ID = 'lifecycle-campaign-1'; const FARMER = 'GFARMER_LIFECYCLE'; @@ -30,25 +26,17 @@ function rawEvent( describe('Indexer campaign lifecycle (e2e)', () => { let prisma: PrismaClient; let parser: EventParserService; - const dbPath = path.join( - os.tmpdir(), - `agro-indexer-e2e-${process.pid}-${Date.now()}.db`, - ); beforeAll(async () => { - // Fresh SQLite file per run, so the lifecycle assertions below never see - // state left behind by a previous run or another test file. - rmSync(dbPath, { force: true }); - prisma = new PrismaClient({ - adapter: new PrismaLibSql({ url: `file:${dbPath}` }), - }); - await applyMigrations(prisma); + prisma = createTestPrismaClient(); + // e2e specs share one Postgres schema, so clear it before seeding rather + // than assuming another file left the tables empty. + await resetDatabase(prisma); parser = new EventParserService(prisma); }); afterAll(async () => { await prisma.$disconnect(); - rmSync(dbPath, { force: true }); }); const status = async (): Promise => { diff --git a/server/test/jest-e2e.json b/server/test/jest-e2e.json index e9d912f..f455511 100644 --- a/server/test/jest-e2e.json +++ b/server/test/jest-e2e.json @@ -3,6 +3,7 @@ "rootDir": ".", "testEnvironment": "node", "testRegex": ".e2e-spec.ts$", + "globalSetup": "/global-setup.ts", "transform": { "^.+\\.(t|j)s$": "ts-jest" }