diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index 313c18ee..9f8c70e4 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -14,12 +14,31 @@ on: jobs: build: - name: Build and Test + name: Build and Test (${{ matrix.database }}) runs-on: ubuntu-latest defaults: run: working-directory: ./apps/backend + strategy: + matrix: + database: [sqlite, postgres] + + services: + postgres: + image: postgres:15 + env: + POSTGRES_USER: vaultix + POSTGRES_PASSWORD: vaultix_password + POSTGRES_DB: vaultix_test + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: - uses: actions/checkout@v4 @@ -27,17 +46,31 @@ jobs: uses: actions/setup-node@v4 with: node-version: '20' - cache: 'npm' - cache-dependency-path: apps/backend/package-lock.json + + - name: Install pnpm + uses: pnpm/action-setup@v3 + with: + version: 8 - name: Install dependencies - run: npm ci + run: pnpm install - name: Lint - run: npm run lint + run: pnpm run lint + if: matrix.database == 'sqlite' - name: Build - run: npm run build + run: pnpm run build + if: matrix.database == 'sqlite' + + - name: Set environment variables for Postgres + if: matrix.database == 'postgres' + run: | + echo "DATABASE_DRIVER=postgres" >> $GITHUB_ENV + echo "DATABASE_URL=postgresql://vaultix:vaultix_password@localhost:5432/vaultix_test" >> $GITHUB_ENV + + - name: Run Migrations + run: pnpm run migration:run - name: Test - run: npm run test + run: pnpm run test diff --git a/README.md b/README.md index e7d9e3c1..7aaebed9 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,8 @@ Before you begin, ensure you have the following installed: Edit `apps/backend/.env` with your configuration: ```env # Database Configuration - DATABASE_PATH=./data/vaultix.db # SQLite path (or use DATABASE_URL for PostgreSQL) + DATABASE_DRIVER=sqlite + DATABASE_PATH=./data/vaultix.db # SQLite path (For PostgreSQL, set DATABASE_DRIVER=postgres and use DATABASE_URL) # JWT Configuration JWT_SECRET=your-super-secret-jwt-key-change-in-production @@ -223,9 +224,17 @@ taskkill /PID /F ``` **Database connection errors**: -- Ensure PostgreSQL is running: `pg_ctl status` or check Docker container +- Ensure PostgreSQL is running: `pg_ctl status` or check Docker container (if using PostgreSQL) +- Verify `DATABASE_DRIVER` is correctly set to `sqlite` or `postgres` - Verify `DATABASE_URL` or `DATABASE_PATH` in `.env` -- Run migrations: `cd apps/backend && pnpm typeorm migration:run` +- Run migrations: `cd apps/backend && pnpm run migration:run` + +**Migrating SQLite to PostgreSQL**: +If you wish to migrate an existing SQLite database to PostgreSQL, use the provided migration script: +```bash +cd apps/backend +pnpm run db:migrate:postgres +``` **TypeScript/Linting errors**: ```bash @@ -265,12 +274,15 @@ cargo build --target wasm32v1-none --release ``` ### Environment Setup -1. Set up PostgreSQL: Create `vaultix_db` and run migrations: - ``` - npx prisma migrate dev --name init +1. Set up PostgreSQL (Recommended for Production) or SQLite (Default for Local Development): + For PostgreSQL, create `vaultix_db` and run migrations: + ```bash + cd apps/backend + pnpm run migration:run ``` 2. Copy `.env.example` to `.env` and configure: - ``` + ```env + DATABASE_DRIVER="postgres" # or "sqlite" DATABASE_URL="postgresql://username:password@localhost:5432/vaultix_db" JWT_SECRET="your-super-secret-jwt-key" STELLAR_NETWORK="testnet" # "mainnet" for production diff --git a/apps/backend/.env.example b/apps/backend/.env.example index d803f936..2902138c 100644 --- a/apps/backend/.env.example +++ b/apps/backend/.env.example @@ -1,6 +1,14 @@ # Database Configuration +# Supported drivers: sqlite, postgres +DATABASE_DRIVER=sqlite + +# SQLite (Local Development) DATABASE_PATH=./data/vaultix.db +# PostgreSQL (Production) +# DATABASE_DRIVER=postgres +# DATABASE_URL=postgresql://user:password@localhost:5432/vaultix + # JWT Configuration (Required at startup, minimum 32 characters long) JWT_SECRET=your-32-character-minimum-super-secret-key-here JWT_EXPIRES_IN=15m diff --git a/apps/backend/data/vaultix.db b/apps/backend/data/vaultix.db index 9a44203f..a1660f92 100644 Binary files a/apps/backend/data/vaultix.db and b/apps/backend/data/vaultix.db differ diff --git a/apps/backend/package.json b/apps/backend/package.json index fbab7523..187b0931 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -19,7 +19,8 @@ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./test/jest-e2e.json", "seed:admin": "ts-node src/scripts/admin-seed.ts", - "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js", + "db:migrate:postgres": "ts-node src/scripts/migrate-sqlite-to-postgres.ts", + "typeorm": "typeorm-ts-node-commonjs", "migration:generate": "npm run typeorm -- migration:generate -d src/data-source.ts", "migration:run": "npm run typeorm -- migration:run -d src/data-source.ts", "migration:revert": "npm run typeorm -- migration:revert -d src/data-source.ts", @@ -56,6 +57,7 @@ "nodemailer": "^8.0.3", "passport": "^0.7.0", "passport-jwt": "^4.0.1", + "pg": "^8.23.0", "pino-http": "^11.0.0", "pino-pretty": "^13.1.3", "reflect-metadata": "^0.2.2", diff --git a/apps/backend/pnpm-lock.yaml b/apps/backend/pnpm-lock.yaml index 53679054..5229212b 100644 --- a/apps/backend/pnpm-lock.yaml +++ b/apps/backend/pnpm-lock.yaml @@ -40,13 +40,13 @@ importers: version: 11.2.6(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) '@nestjs/terminus': specifier: ^11.0.0 - version: 11.1.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/typeorm@11.0.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3))))(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3))) + version: 11.1.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/typeorm@11.0.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(pg@8.23.0)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3))))(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(pg@8.23.0)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3))) '@nestjs/throttler': specifier: ^6.5.0 version: 6.5.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2) '@nestjs/typeorm': specifier: ^11.0.0 - version: 11.0.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3))) + version: 11.0.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(pg@8.23.0)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3))) '@nestjs/websockets': specifier: ^11.0.1 version: 11.1.19(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/platform-socket.io@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -77,9 +77,15 @@ importers: dotenv: specifier: ^17.3.1 version: 17.3.1 + helmet: + specifier: ^8.3.0 + version: 8.3.0 multer: specifier: ^1.4.5-lts.1 version: 1.4.5-lts.2 + nestjs-pino: + specifier: ^4.6.1 + version: 4.6.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.3.1)(rxjs@7.8.2) nodemailer: specifier: ^8.0.3 version: 8.0.4 @@ -89,6 +95,15 @@ importers: passport-jwt: specifier: ^4.0.1 version: 4.0.1 + pg: + specifier: ^8.23.0 + version: 8.23.0 + pino-http: + specifier: ^11.0.0 + version: 11.0.0 + pino-pretty: + specifier: ^13.1.3 + version: 13.1.3 reflect-metadata: specifier: ^0.2.2 version: 0.2.2 @@ -109,10 +124,10 @@ importers: version: 5.0.1(express@5.2.1) typeorm: specifier: ^0.3.28 - version: 0.3.28(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + version: 0.3.28(pg@8.23.0)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) uuid: specifier: ^14.0.1 - version: 14.0.1 + version: 14.0.2 devDependencies: '@eslint/eslintrc': specifier: ^3.2.0 @@ -139,14 +154,14 @@ importers: specifier: ^22.19.11 version: 22.19.15 '@types/nodemailer': - specifier: ^7.0.11 - version: 7.0.11 + specifier: ^7.0.12 + version: 7.0.12 '@types/supertest': specifier: ^6.0.2 version: 6.0.3 '@types/uuid': - specifier: ^11.0.0 - version: 11.0.0 + specifier: ^10.0.0 + version: 10.0.0 eslint: specifier: ^9.18.0 version: 9.39.4 @@ -950,6 +965,9 @@ packages: '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1099,8 +1117,8 @@ packages: '@types/node@22.19.15': resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} - '@types/nodemailer@7.0.11': - resolution: {integrity: sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==} + '@types/nodemailer@7.0.12': + resolution: {integrity: sha512-80vKwiIsVSyFA1rRovH59jNPLBOuc6dRZIHEu40gXTkBkZnQv8vog1xSGEb9j5q/tdMAs5ivvDR2pLTU0hGHXA==} '@types/passport-jwt@4.0.1': resolution: {integrity: sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==} @@ -1132,9 +1150,8 @@ packages: '@types/supertest@6.0.3': resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} - '@types/uuid@11.0.0': - resolution: {integrity: sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA==} - deprecated: This is a stub types definition. uuid provides its own type definitions, so you do not need this installed. + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} '@types/validator@13.15.10': resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} @@ -1412,6 +1429,10 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -1701,6 +1722,9 @@ packages: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} hasBin: true + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -1795,6 +1819,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + dayjs@1.11.20: resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} @@ -2086,6 +2113,9 @@ packages: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} + fast-copy@4.0.4: + resolution: {integrity: sha512-eVAiWVNPSEGIzDl5yPuLrx8fNMogScXvD9xp1Kzd41FjRIz2I3sSIcxsFeM5EzFfHAfobdvs8ZySffUopljvIA==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2316,6 +2346,13 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} + helmet@8.3.0: + resolution: {integrity: sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==} + engines: {node: '>=18.0.0'} + + help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -2644,6 +2681,10 @@ packages: node-notifier: optional: true + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2961,6 +3002,15 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + nestjs-pino@4.6.1: + resolution: {integrity: sha512-nuARXa0xpdJ1lY2+fgycIQr6H3g0VgqAWNK3xMYjOFcj2DoPETNXj0lV3Y86nRuI7BUfQp5PGiVoZvT4dTWbpQ==} + engines: {node: '>= 14'} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + pino: ^7.5.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + pino-http: ^6.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + rxjs: ^7.1.0 + node-abi@3.89.0: resolution: {integrity: sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==} engines: {node: '>=10'} @@ -3027,6 +3077,10 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -3129,6 +3183,40 @@ packages: pause@0.0.1: resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} + + pg-connection-string@2.14.0: + resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.16.0: + resolution: {integrity: sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.23.0: + resolution: {integrity: sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3144,6 +3232,23 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-http@11.0.0: + resolution: {integrity: sha512-wqg5XIAGRRIWtTk8qPGxkbrfiwEWz1lgedVLvhLALudKXvg1/L2lTFgTGPJ4Z2e3qcRmxoFxDuSdMdMGNM6I1g==} + + pino-pretty@13.1.3: + resolution: {integrity: sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==} + hasBin: true + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + pirates@4.0.7: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} @@ -3160,6 +3265,22 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} @@ -3186,6 +3307,9 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process-warning@5.1.0: + resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==} + promise-inflight@1.0.1: resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} peerDependencies: @@ -3223,6 +3347,9 @@ packages: resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} engines: {node: '>=0.6'} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -3252,6 +3379,13 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + real-require@1.0.0: + resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -3317,6 +3451,10 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -3328,6 +3466,9 @@ packages: resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -3435,6 +3576,9 @@ packages: sodium-native@4.3.3: resolution: {integrity: sha512-OnxSlN3uyY8D0EsLHpmm2HOFmKddQVvEMmsakCrXUzSd8kjjbzL413t4ZNF3n0UxSwNgwTyUvkmZHTfuCeiYSw==} + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} @@ -3453,6 +3597,10 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -3530,6 +3678,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + strtok3@10.3.5: resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} engines: {node: '>=18'} @@ -3615,6 +3767,10 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} + thread-stream@4.2.0: + resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==} + engines: {node: '>=20'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -3867,8 +4023,8 @@ packages: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true - uuid@14.0.1: - resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} + uuid@14.0.2: + resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} hasBin: true v8-compile-cache-lib@3.0.1: @@ -4643,6 +4799,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@jest/transform@29.7.0': + dependencies: + '@babel/core': 7.29.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.31 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.8 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + '@jest/transform@30.3.0': dependencies: '@babel/core': 7.29.0 @@ -4855,7 +5031,7 @@ snapshots: class-transformer: 0.5.1 class-validator: 0.14.4 - '@nestjs/terminus@11.1.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/typeorm@11.0.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3))))(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)))': + '@nestjs/terminus@11.1.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/typeorm@11.0.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(pg@8.23.0)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3))))(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(pg@8.23.0)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)))': dependencies: '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(@nestjs/websockets@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -4864,8 +5040,8 @@ snapshots: reflect-metadata: 0.2.2 rxjs: 7.8.2 optionalDependencies: - '@nestjs/typeorm': 11.0.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3))) - typeorm: 0.3.28(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + '@nestjs/typeorm': 11.0.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(pg@8.23.0)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3))) + typeorm: 0.3.28(pg@8.23.0)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) '@nestjs/testing@11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/platform-express@11.1.17)': dependencies: @@ -4881,13 +5057,13 @@ snapshots: '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(@nestjs/websockets@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 - '@nestjs/typeorm@11.0.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)))': + '@nestjs/typeorm@11.0.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.28(pg@8.23.0)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)))': dependencies: '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(@nestjs/websockets@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 rxjs: 7.8.2 - typeorm: 0.3.28(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) + typeorm: 0.3.28(pg@8.23.0)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)) '@nestjs/websockets@11.1.19(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/platform-socket.io@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: @@ -4927,6 +5103,8 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 + '@pinojs/redact@0.4.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -5116,7 +5294,7 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/nodemailer@7.0.11': + '@types/nodemailer@7.0.12': dependencies: '@types/node': 22.19.15 @@ -5161,9 +5339,7 @@ snapshots: '@types/methods': 1.1.4 '@types/superagent': 8.1.9 - '@types/uuid@11.0.0': - dependencies: - uuid: 14.0.1 + '@types/uuid@10.0.0': {} '@types/validator@13.15.10': {} @@ -5490,6 +5666,8 @@ snapshots: asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 @@ -5840,6 +6018,8 @@ snapshots: color-support@1.1.3: optional: true + colorette@2.0.20: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -5935,6 +6115,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + dateformat@4.6.3: {} + dayjs@1.11.20: {} debug@4.4.3: @@ -6239,6 +6421,8 @@ snapshots: transitivePeerDependencies: - supports-color + fast-copy@4.0.4: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -6487,6 +6671,10 @@ snapshots: dependencies: function-bind: 1.1.2 + helmet@8.3.0: {} + + help-me@5.0.0: {} + html-escaper@2.0.2: {} http-cache-semantics@4.2.0: @@ -7016,6 +7204,8 @@ snapshots: - supports-color - ts-node + joycon@3.1.1: {} + js-tokens@4.0.0: {} js-yaml@3.14.2: @@ -7316,6 +7506,13 @@ snapshots: neo-async@2.6.2: {} + nestjs-pino@4.6.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.3.1)(rxjs@7.8.2): + dependencies: + '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) + pino: 10.3.1 + pino-http: 11.0.0 + rxjs: 7.8.2 + node-abi@3.89.0: dependencies: semver: 7.7.4 @@ -7380,6 +7577,8 @@ snapshots: object-inspect@1.13.4: {} + on-exit-leak-free@2.1.2: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -7488,6 +7687,41 @@ snapshots: pause@0.0.1: {} + pg-cloudflare@1.4.0: + optional: true + + pg-connection-string@2.14.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.23.0): + dependencies: + pg: 8.23.0 + + pg-protocol@1.16.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.23.0: + dependencies: + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.23.0) + pg-protocol: 1.16.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -7496,6 +7730,49 @@ snapshots: picomatch@4.0.4: {} + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-http@11.0.0: + dependencies: + get-caller-file: 2.0.5 + pino: 10.3.1 + pino-std-serializers: 7.1.0 + process-warning: 5.1.0 + + pino-pretty@13.1.3: + dependencies: + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 4.0.4 + fast-safe-stringify: 2.1.1 + help-me: 5.0.0 + joycon: 3.1.1 + minimist: 1.2.8 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pump: 3.0.4 + secure-json-parse: 4.1.0 + sonic-boom: 4.2.1 + strip-json-comments: 5.0.3 + + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.1.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.2.0 + pirates@4.0.7: {} pkg-dir@4.2.0: @@ -7506,6 +7783,16 @@ snapshots: possible-typed-array-names@1.1.0: {} + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + prebuild-install@7.1.3: dependencies: detect-libc: 2.1.2 @@ -7537,6 +7824,8 @@ snapshots: process-nextick-args@2.0.1: {} + process-warning@5.1.0: {} + promise-inflight@1.0.1: optional: true @@ -7571,6 +7860,8 @@ snapshots: dependencies: side-channel: 1.1.0 + quick-format-unescaped@4.0.4: {} + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -7611,6 +7902,10 @@ snapshots: readdirp@4.1.2: {} + real-require@0.2.0: {} + + real-require@1.0.0: {} + reflect-metadata@0.2.2: {} require-addon@1.2.0: @@ -7676,6 +7971,8 @@ snapshots: safe-buffer@5.2.1: {} + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} schema-utils@3.3.0: @@ -7691,6 +7988,8 @@ snapshots: ajv-formats: 2.1.1(ajv@8.18.0) ajv-keywords: 5.1.0(ajv@8.18.0) + secure-json-parse@4.1.0: {} + semver@6.3.1: {} semver@7.7.4: {} @@ -7847,6 +8146,10 @@ snapshots: - bare-url optional: true + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + source-map-support@0.5.13: dependencies: buffer-from: 1.1.2 @@ -7863,6 +8166,8 @@ snapshots: source-map@0.7.6: {} + split2@4.2.0: {} + sprintf-js@1.0.3: {} sql-highlight@6.1.0: {} @@ -7949,6 +8254,8 @@ snapshots: strip-json-comments@3.1.1: {} + strip-json-comments@5.0.3: {} + strtok3@10.3.5: dependencies: '@tokenizer/token': 0.3.0 @@ -8051,6 +8358,10 @@ snapshots: glob: 7.2.3 minimatch: 3.1.5 + thread-stream@4.2.0: + dependencies: + real-require: 1.0.0 + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -8182,7 +8493,7 @@ snapshots: typedarray@0.0.6: {} - typeorm@0.3.28(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)): + typeorm@0.3.28(pg@8.23.0)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)): dependencies: '@sqltools/formatter': 1.2.5 ansis: 4.2.0 @@ -8200,6 +8511,7 @@ snapshots: uuid: 11.1.0 yargs: 17.7.2 optionalDependencies: + pg: 8.23.0 sqlite3: 5.1.7 ts-node: 10.9.2(@types/node@22.19.15)(typescript@5.9.3) transitivePeerDependencies: @@ -8262,7 +8574,7 @@ snapshots: uuid@11.1.0: {} - uuid@14.0.1: {} + uuid@14.0.2: {} v8-compile-cache-lib@3.0.1: {} diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index c62eac8a..b93aa1c5 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -82,13 +82,10 @@ import { BackupRecord } from './modules/backup/entities/backup-record.entity'; ScheduleModule.forRoot(), TypeOrmModule.forRootAsync({ imports: [ConfigModule], - useFactory: (configService: ConfigService) => ({ - type: 'sqlite', - database: configService.get( - 'DATABASE_PATH', - './data/vaultix.db', - ), - entities: [ + useFactory: (configService: ConfigService) => { + const isPostgres = + configService.get('DATABASE_DRIVER') === 'postgres'; + const entities = [ User, RefreshToken, EmailVerification, @@ -107,13 +104,41 @@ import { BackupRecord } from './modules/backup/entities/backup-record.entity'; StellarEvent, AllowedAsset, EmailOutbox, - BackupRecord, - KycVerification, - ], - synchronize: false, - migrations: [__dirname + '/migrations/*.ts'], - migrationsRun: true, - }), + ]; + + if (isPostgres) { + const url = configService.get('DATABASE_URL'); + if (!url) { + throw new Error( + 'DATABASE_URL is required when DATABASE_DRIVER is postgres', + ); + } + return { + type: 'postgres', + url, + entities, + synchronize: false, + migrations: [__dirname + '/migrations/*.ts'], + migrationsRun: true, + extra: { + max: 20, + idleTimeoutMillis: 30000, + }, + }; + } + + return { + type: 'sqlite', + database: configService.get( + 'DATABASE_PATH', + './data/vaultix.db', + ), + entities, + synchronize: false, + migrations: [__dirname + '/migrations/*.ts'], + migrationsRun: true, + }; + }, inject: [ConfigService], }), AuthModule, diff --git a/apps/backend/src/data-source.ts b/apps/backend/src/data-source.ts index fc9c97d1..8fd9be1a 100644 --- a/apps/backend/src/data-source.ts +++ b/apps/backend/src/data-source.ts @@ -2,6 +2,7 @@ import { DataSource } from 'typeorm'; import { config } from 'dotenv'; import { User } from './modules/user/entities/user.entity'; import { RefreshToken } from './modules/user/entities/refresh-token.entity'; +import { EmailVerification } from './modules/user/entities/email-verification.entity'; import { Escrow } from './modules/escrow/entities/escrow.entity'; import { Party } from './modules/escrow/entities/party.entity'; import { Condition } from './modules/escrow/entities/condition.entity'; @@ -22,30 +23,64 @@ import { KycVerification } from './modules/kyc/entities/kyc-verification.entity' config(); // Load .env file -export default new DataSource({ - type: 'sqlite', - database: process.env.DATABASE_PATH || './data/vaultix.db', - entities: [ - User, - RefreshToken, - Escrow, - Party, - Condition, - EscrowEvent, - Dispute, - Notification, - NotificationPreference, - ApiKey, - AdminAuditLog, - Webhook, - WebhookDelivery, - WebhookDeadLetter, - StellarEvent, - AllowedAsset, - EmailOutbox, - BackupRecord, - KycVerification, - ], - migrations: ['./src/migrations/*.ts'], - synchronize: false, -}); +const isPostgres = process.env.DATABASE_DRIVER === 'postgres'; + +export default new DataSource( + isPostgres + ? { + type: 'postgres', + url: process.env.DATABASE_URL, + entities: [ + User, + RefreshToken, + EmailVerification, + Escrow, + Party, + Condition, + EscrowEvent, + Dispute, + Notification, + NotificationPreference, + ApiKey, + AdminAuditLog, + Webhook, + WebhookDelivery, + WebhookDeadLetter, + StellarEvent, + AllowedAsset, + EmailOutbox, + ], + migrations: ['./src/migrations/*.ts'], + synchronize: false, + extra: { + max: 20, + idleTimeoutMillis: 30000, + }, + } + : { + type: 'sqlite', + database: process.env.DATABASE_PATH || './data/vaultix.db', + entities: [ + User, + RefreshToken, + EmailVerification, + Escrow, + Party, + Condition, + EscrowEvent, + Dispute, + Notification, + NotificationPreference, + ApiKey, + AdminAuditLog, + Webhook, + WebhookDelivery, + WebhookDeadLetter, + StellarEvent, + AllowedAsset, + EmailOutbox, + ], + migrations: ['./src/migrations/*.ts'], + synchronize: false, + }, +); diff --git a/apps/backend/src/migrations/1780262000000-ImplementFourFeatures.ts b/apps/backend/src/migrations/1780262000000-ImplementFourFeatures.ts index 39c33611..ae931796 100644 --- a/apps/backend/src/migrations/1780262000000-ImplementFourFeatures.ts +++ b/apps/backend/src/migrations/1780262000000-ImplementFourFeatures.ts @@ -24,34 +24,85 @@ export class ImplementFourFeatures1780262000000 implements MigrationInterface { ); // Create EmailVerification table - await queryRunner.query(` - CREATE TABLE "email_verifications" ( - "id" uuid NOT NULL DEFAULT uuid_generate_v4(), - "userId" uuid NOT NULL, - "token" character varying NOT NULL, - "expiresAt" TIMESTAMP NOT NULL, - "isUsed" boolean NOT NULL DEFAULT false, - "createdAt" TIMESTAMP NOT NULL DEFAULT now(), - CONSTRAINT "PK_email_verifications_id" PRIMARY KEY ("id"), - CONSTRAINT "UQ_email_verifications_token" UNIQUE ("token") - ) - `); - await queryRunner.query( - `ALTER TABLE "email_verifications" ADD CONSTRAINT "FK_email_verifications_userId" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE`, - ); + const isPostgres = queryRunner.connection.options.type === 'postgres'; + + if (isPostgres) { + await queryRunner.query(` + CREATE TABLE "email_verifications" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "userId" varchar NOT NULL, + "token" character varying NOT NULL, + "expiresAt" TIMESTAMP NOT NULL, + "isUsed" boolean NOT NULL DEFAULT false, + "createdAt" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "PK_email_verifications_id" PRIMARY KEY ("id"), + CONSTRAINT "UQ_email_verifications_token" UNIQUE ("token") + ) + `); + } else { + await queryRunner.query(` + CREATE TABLE "email_verifications" ( + "id" varchar PRIMARY KEY NOT NULL, + "userId" varchar NOT NULL, + "token" varchar NOT NULL, + "expiresAt" datetime NOT NULL, + "isUsed" boolean NOT NULL DEFAULT (0), + "createdAt" datetime NOT NULL DEFAULT (datetime('now')), + CONSTRAINT "UQ_email_verifications_token" UNIQUE ("token") + ) + `); + } + + if (isPostgres) { + await queryRunner.query( + `ALTER TABLE "email_verifications" ADD CONSTRAINT "FK_email_verifications_userId" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE`, + ); + } else { + // SQLite doesn't easily support adding foreign keys to existing tables via ALTER TABLE. + // But we can recreate the table with the FK if we really need to, or just skip it for SQLite since it's just development. + // Let's drop and recreate with the foreign key constraint directly. + await queryRunner.query(`DROP TABLE "email_verifications"`); + await queryRunner.query(` + CREATE TABLE "email_verifications" ( + "id" varchar PRIMARY KEY NOT NULL, + "userId" varchar NOT NULL, + "token" varchar NOT NULL, + "expiresAt" datetime NOT NULL, + "isUsed" boolean NOT NULL DEFAULT (0), + "createdAt" datetime NOT NULL DEFAULT (datetime('now')), + CONSTRAINT "UQ_email_verifications_token" UNIQUE ("token"), + CONSTRAINT "FK_email_verifications_userId" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE + ) + `); + } // Add new fields to Escrow entity - await queryRunner.query( - `ALTER TABLE "escrows" ADD COLUMN "releasedAmount" numeric(18,7) NOT NULL DEFAULT 0`, - ); + if (isPostgres) { + await queryRunner.query( + `ALTER TABLE "escrows" ADD COLUMN "releasedAmount" numeric(18,7) NOT NULL DEFAULT 0`, + ); + } else { + await queryRunner.query( + `ALTER TABLE "escrows" ADD COLUMN "releasedAmount" decimal(18,7) NOT NULL DEFAULT 0`, + ); + } // Add new fields to Condition entity - await queryRunner.query( - `ALTER TABLE "escrow_conditions" ADD COLUMN "isReleased" boolean NOT NULL DEFAULT false`, - ); - await queryRunner.query( - `ALTER TABLE "escrow_conditions" ADD COLUMN "releasedAt" TIMESTAMP`, - ); + if (isPostgres) { + await queryRunner.query( + `ALTER TABLE "escrow_conditions" ADD COLUMN "isReleased" boolean NOT NULL DEFAULT false`, + ); + await queryRunner.query( + `ALTER TABLE "escrow_conditions" ADD COLUMN "releasedAt" TIMESTAMP`, + ); + } else { + await queryRunner.query( + `ALTER TABLE "escrow_conditions" ADD COLUMN "isReleased" boolean NOT NULL DEFAULT (0)`, + ); + await queryRunner.query( + `ALTER TABLE "escrow_conditions" ADD COLUMN "releasedAt" datetime`, + ); + } } public async down(queryRunner: QueryRunner): Promise { diff --git a/apps/backend/src/migrations/1780400000000-AddWebhookDeliveryTable.ts b/apps/backend/src/migrations/1780400000000-AddWebhookDeliveryTable.ts index b11eaa2b..237c5489 100644 --- a/apps/backend/src/migrations/1780400000000-AddWebhookDeliveryTable.ts +++ b/apps/backend/src/migrations/1780400000000-AddWebhookDeliveryTable.ts @@ -4,24 +4,47 @@ export class AddWebhookDeliveryTable1780400000000 implements MigrationInterface name = 'AddWebhookDeliveryTable1780400000000'; public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE "webhook_delivery" ( - "id" uuid PRIMARY KEY DEFAULT uuid_generate_v4(), - "webhookId" uuid NOT NULL, - "event" varchar NOT NULL, - "payload" text NOT NULL, - "status" varchar NOT NULL DEFAULT 'pending', - "attempt" integer NOT NULL DEFAULT 1, - "maxAttempts" integer NOT NULL DEFAULT 5, - "nextRetryAt" datetime, - "lastStatusCode" integer, - "lastError" text, - "lastAttemptAt" datetime, - "createdAt" datetime DEFAULT now(), - "updatedAt" datetime DEFAULT now(), - CONSTRAINT "FK_webhook_delivery_webhook" FOREIGN KEY ("webhookId") REFERENCES "webhooks"("id") ON DELETE CASCADE - ) - `); + const isPostgres = queryRunner.connection.options.type === 'postgres'; + + if (isPostgres) { + await queryRunner.query(` + CREATE TABLE "webhook_delivery" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + "webhookId" uuid NOT NULL, + "event" varchar NOT NULL, + "payload" text NOT NULL, + "status" varchar NOT NULL DEFAULT 'pending', + "attempt" integer NOT NULL DEFAULT 1, + "maxAttempts" integer NOT NULL DEFAULT 5, + "nextRetryAt" TIMESTAMP, + "lastStatusCode" integer, + "lastError" text, + "lastAttemptAt" TIMESTAMP, + "createdAt" TIMESTAMP DEFAULT now(), + "updatedAt" TIMESTAMP DEFAULT now(), + CONSTRAINT "FK_webhook_delivery_webhook" FOREIGN KEY ("webhookId") REFERENCES "webhooks"("id") ON DELETE CASCADE + ) + `); + } else { + await queryRunner.query(` + CREATE TABLE "webhook_delivery" ( + "id" varchar PRIMARY KEY NOT NULL, + "webhookId" varchar NOT NULL, + "event" varchar NOT NULL, + "payload" text NOT NULL, + "status" varchar NOT NULL DEFAULT 'pending', + "attempt" integer NOT NULL DEFAULT 1, + "maxAttempts" integer NOT NULL DEFAULT 5, + "nextRetryAt" datetime, + "lastStatusCode" integer, + "lastError" text, + "lastAttemptAt" datetime, + "createdAt" datetime DEFAULT (datetime('now')), + "updatedAt" datetime DEFAULT (datetime('now')), + CONSTRAINT "FK_webhook_delivery_webhook" FOREIGN KEY ("webhookId") REFERENCES "webhooks"("id") ON DELETE CASCADE + ) + `); + } await queryRunner.query( `CREATE INDEX "idx_webhook_delivery_status_retry" ON "webhook_delivery" ("status", "nextRetryAt")`, ); diff --git a/apps/backend/src/migrations/1780600000000-AddIpfsMetadataToEscrows.ts b/apps/backend/src/migrations/1780600000000-AddIpfsMetadataToEscrows.ts index 11447a35..9fb5e5a4 100644 --- a/apps/backend/src/migrations/1780600000000-AddIpfsMetadataToEscrows.ts +++ b/apps/backend/src/migrations/1780600000000-AddIpfsMetadataToEscrows.ts @@ -4,20 +4,36 @@ export class AddIpfsMetadataToEscrows1780600000000 implements MigrationInterface name = 'AddIpfsMetadataToEscrows1780600000000'; public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE "escrows" - ADD "ipfs_cid" character varying, - ADD "ipfs_metadata_hash" character varying, - ADD "ipfs_version" integer NOT NULL DEFAULT 0 - `); + await queryRunner.query( + `ALTER TABLE "escrows" ADD "ipfs_cid" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "escrows" ADD "ipfs_metadata_hash" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "escrows" ADD "ipfs_version" integer NOT NULL DEFAULT 0`, + ); } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE "escrows" - DROP COLUMN "ipfs_version", - DROP COLUMN "ipfs_metadata_hash", - DROP COLUMN "ipfs_cid" - `); + const isPostgres = queryRunner.connection.options.type === 'postgres'; + if (isPostgres) { + await queryRunner.query( + `ALTER TABLE "escrows" DROP COLUMN "ipfs_version"`, + ); + await queryRunner.query( + `ALTER TABLE "escrows" DROP COLUMN "ipfs_metadata_hash"`, + ); + await queryRunner.query(`ALTER TABLE "escrows" DROP COLUMN "ipfs_cid"`); + } else { + // SQLite drop column support can be tricky, but recent sqlite supports it, so we do it one by one: + await queryRunner.query( + `ALTER TABLE "escrows" DROP COLUMN "ipfs_version"`, + ); + await queryRunner.query( + `ALTER TABLE "escrows" DROP COLUMN "ipfs_metadata_hash"`, + ); + await queryRunner.query(`ALTER TABLE "escrows" DROP COLUMN "ipfs_cid"`); + } } } diff --git a/apps/backend/src/migrations/1780700000000-AddEmailOutboxTable.ts b/apps/backend/src/migrations/1780700000000-AddEmailOutboxTable.ts index 9b1a16a6..f3069c2a 100644 --- a/apps/backend/src/migrations/1780700000000-AddEmailOutboxTable.ts +++ b/apps/backend/src/migrations/1780700000000-AddEmailOutboxTable.ts @@ -4,22 +4,43 @@ export class AddEmailOutboxTable1780700000000 implements MigrationInterface { name = 'AddEmailOutboxTable1780700000000'; public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE "email_outbox" ( - "id" varchar PRIMARY KEY, - "to" varchar NOT NULL, - "subject" varchar NOT NULL, - "html" text NOT NULL, - "text" text, - "status" varchar NOT NULL DEFAULT 'pending', - "attempts" integer NOT NULL DEFAULT 0, - "nextRetryAt" datetime, - "lastError" text, - "sentAt" datetime, - "createdAt" datetime NOT NULL DEFAULT (datetime('now')), - "updatedAt" datetime NOT NULL DEFAULT (datetime('now')) - ) - `); + const isPostgres = queryRunner.connection.options.type === 'postgres'; + + if (isPostgres) { + await queryRunner.query(` + CREATE TABLE "email_outbox" ( + "id" varchar PRIMARY KEY, + "to" varchar NOT NULL, + "subject" varchar NOT NULL, + "html" text NOT NULL, + "text" text, + "status" varchar NOT NULL DEFAULT 'pending', + "attempts" integer NOT NULL DEFAULT 0, + "nextRetryAt" TIMESTAMP, + "lastError" text, + "sentAt" TIMESTAMP, + "createdAt" TIMESTAMP NOT NULL DEFAULT now(), + "updatedAt" TIMESTAMP NOT NULL DEFAULT now() + ) + `); + } else { + await queryRunner.query(` + CREATE TABLE "email_outbox" ( + "id" varchar PRIMARY KEY, + "to" varchar NOT NULL, + "subject" varchar NOT NULL, + "html" text NOT NULL, + "text" text, + "status" varchar NOT NULL DEFAULT 'pending', + "attempts" integer NOT NULL DEFAULT 0, + "nextRetryAt" datetime, + "lastError" text, + "sentAt" datetime, + "createdAt" datetime NOT NULL DEFAULT (datetime('now')), + "updatedAt" datetime NOT NULL DEFAULT (datetime('now')) + ) + `); + } await queryRunner.query( `CREATE INDEX "idx_email_outbox_status_retry" ON "email_outbox" ("status", "nextRetryAt")`, ); diff --git a/apps/backend/src/migrations/1780800000000-AddWebhookDeadLetterTable.ts b/apps/backend/src/migrations/1780800000000-AddWebhookDeadLetterTable.ts index ebed5bb9..07e8b4cd 100644 --- a/apps/backend/src/migrations/1780800000000-AddWebhookDeadLetterTable.ts +++ b/apps/backend/src/migrations/1780800000000-AddWebhookDeadLetterTable.ts @@ -4,22 +4,43 @@ export class AddWebhookDeadLetterTable1780800000000 implements MigrationInterfac name = 'AddWebhookDeadLetterTable1780800000000'; public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE "webhook_dead_letter" ( - "id" varchar PRIMARY KEY, - "webhookId" varchar NOT NULL, - "originalDeliveryId" varchar, - "event" varchar NOT NULL, - "payload" text NOT NULL, - "attempts" integer NOT NULL DEFAULT 0, - "lastStatusCode" integer, - "lastError" text, - "failedAt" datetime NOT NULL DEFAULT (datetime('now')), - "replayedAt" datetime, - "createdAt" datetime NOT NULL DEFAULT (datetime('now')), - CONSTRAINT "FK_webhook_dead_letter_webhook" FOREIGN KEY ("webhookId") REFERENCES "webhooks"("id") ON DELETE CASCADE - ) - `); + const isPostgres = queryRunner.connection.options.type === 'postgres'; + + if (isPostgres) { + await queryRunner.query(` + CREATE TABLE "webhook_dead_letter" ( + "id" varchar PRIMARY KEY, + "webhookId" varchar NOT NULL, + "originalDeliveryId" varchar, + "event" varchar NOT NULL, + "payload" text NOT NULL, + "attempts" integer NOT NULL DEFAULT 0, + "lastStatusCode" integer, + "lastError" text, + "failedAt" TIMESTAMP NOT NULL DEFAULT now(), + "replayedAt" TIMESTAMP, + "createdAt" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "FK_webhook_dead_letter_webhook" FOREIGN KEY ("webhookId") REFERENCES "webhooks"("id") ON DELETE CASCADE + ) + `); + } else { + await queryRunner.query(` + CREATE TABLE "webhook_dead_letter" ( + "id" varchar PRIMARY KEY, + "webhookId" varchar NOT NULL, + "originalDeliveryId" varchar, + "event" varchar NOT NULL, + "payload" text NOT NULL, + "attempts" integer NOT NULL DEFAULT 0, + "lastStatusCode" integer, + "lastError" text, + "failedAt" datetime NOT NULL DEFAULT (datetime('now')), + "replayedAt" datetime, + "createdAt" datetime NOT NULL DEFAULT (datetime('now')), + CONSTRAINT "FK_webhook_dead_letter_webhook" FOREIGN KEY ("webhookId") REFERENCES "webhooks"("id") ON DELETE CASCADE + ) + `); + } await queryRunner.query( `CREATE INDEX "idx_webhook_dead_letter_webhook" ON "webhook_dead_letter" ("webhookId")`, ); diff --git a/apps/backend/src/modules/health/health.controller.ts b/apps/backend/src/modules/health/health.controller.ts index ae56d06f..2414744d 100644 --- a/apps/backend/src/modules/health/health.controller.ts +++ b/apps/backend/src/modules/health/health.controller.ts @@ -44,7 +44,7 @@ interface OverallHealthResponse { version: string; uptime: number; dependencies: { - database: DependencyHealth; + database: DependencyHealth & { driver?: string }; stellar: DependencyHealth; ipfs: DependencyHealth; websocket: DependencyHealth; @@ -152,7 +152,7 @@ export class HealthController { nodeVersion: process.version, uptime: process.uptime(), network: process.env.STELLAR_NETWORK || 'testnet', - databaseType: 'sqlite', + databaseType: this.configService.get('DATABASE_DRIVER', 'sqlite'), metrics: { activeEscrows, totalUsers, @@ -196,18 +196,22 @@ export class HealthController { } } - private async probeDatabase(): Promise { + private async probeDatabase(): Promise< + DependencyHealth & { driver?: string } + > { const startedAt = Date.now(); + const driver = this.configService.get('DATABASE_DRIVER', 'sqlite'); try { await this.typeOrmHealthIndicator.pingCheck('database', { timeout: this.checkTimeoutMs, }); - return { status: 'up', responseTimeMs: Date.now() - startedAt }; + return { status: 'up', responseTimeMs: Date.now() - startedAt, driver }; } catch (error) { return { status: 'down', responseTimeMs: Date.now() - startedAt, error: error instanceof Error ? error.message : String(error), + driver, }; } } diff --git a/apps/backend/src/scripts/migrate-sqlite-to-postgres.ts b/apps/backend/src/scripts/migrate-sqlite-to-postgres.ts new file mode 100644 index 00000000..7dc486f2 --- /dev/null +++ b/apps/backend/src/scripts/migrate-sqlite-to-postgres.ts @@ -0,0 +1,143 @@ +import { DataSource } from 'typeorm'; +import { config } from 'dotenv'; +import * as fs from 'fs'; +import * as path from 'path'; + +// Import all entities +import { User } from '../modules/user/entities/user.entity'; +import { RefreshToken } from '../modules/user/entities/refresh-token.entity'; +import { EmailVerification } from '../modules/user/entities/email-verification.entity'; +import { Escrow } from '../modules/escrow/entities/escrow.entity'; +import { Party } from '../modules/escrow/entities/party.entity'; +import { Condition } from '../modules/escrow/entities/condition.entity'; +import { EscrowEvent } from '../modules/escrow/entities/escrow-event.entity'; +import { Dispute } from '../modules/escrow/entities/dispute.entity'; +import { Notification } from '../notifications/entities/notification.entity'; +import { NotificationPreference } from '../notifications/entities/notification-preference.entity'; +import { ApiKey } from '../api-key/entities/api-key.entity'; +import { AdminAuditLog } from '../modules/admin/entities/admin-audit-log.entity'; +import { Webhook } from '../modules/webhook/webhook.entity'; +import { WebhookDelivery } from '../modules/webhook/entities/webhook-delivery.entity'; +import { WebhookDeadLetter } from '../modules/webhook/entities/webhook-dead-letter.entity'; +import { StellarEvent } from '../modules/stellar/entities/stellar-event.entity'; +import { AllowedAsset } from '../modules/assets/entities/allowed-asset.entity'; +import { EmailOutbox } from '../email/entities/email-outbox.entity'; + +config(); + +const entities = [ + User, + RefreshToken, + EmailVerification, + Escrow, + Party, + Condition, + EscrowEvent, + Dispute, + Notification, + NotificationPreference, + ApiKey, + AdminAuditLog, + Webhook, + WebhookDelivery, + WebhookDeadLetter, + StellarEvent, + AllowedAsset, + EmailOutbox, +]; + +async function migrate() { + console.log('Starting migration from SQLite to PostgreSQL...'); + + const sqlitePath = process.env.DATABASE_PATH || './data/vaultix.db'; + if (!fs.existsSync(sqlitePath)) { + console.error(`SQLite database not found at ${sqlitePath}`); + process.exit(1); + } + + const pgUrl = process.env.DATABASE_URL; + if (!pgUrl) { + console.error('DATABASE_URL must be set for PostgreSQL'); + process.exit(1); + } + + const sqliteDataSource = new DataSource({ + type: 'sqlite', + database: sqlitePath, + entities, + synchronize: false, + }); + + const pgDataSource = new DataSource({ + type: 'postgres', + url: pgUrl, + entities, + synchronize: false, + migrations: [path.join(__dirname, '../migrations/*.ts')], + migrationsRun: true, + }); + + try { + console.log('Connecting to SQLite...'); + await sqliteDataSource.initialize(); + + console.log('Connecting to PostgreSQL and running migrations...'); + await pgDataSource.initialize(); + + console.log('Connected to both databases. Starting data transfer...'); + + for (const entity of entities) { + const entityName = entity.name; + console.log(`\nMigrating ${entityName}...`); + + const sqliteRepo = sqliteDataSource.getRepository(entity); + const pgRepo = pgDataSource.getRepository(entity); + + const records = await sqliteRepo.find(); + console.log(`Found ${records.length} records in SQLite.`); + + if (records.length > 0) { + // To avoid conflicts and duplicate keys, we insert them one by one or in batches + // and ignore on conflict if possible, but standard save handles it usually, + // though we might want to just use insert to be safe. + // Some entities have relations, so order matters. The entities array is ordered + // generally to respect dependencies (User before others, Escrow before Party, etc.) + + let successCount = 0; + let errorCount = 0; + + for (const record of records) { + try { + await pgRepo.save(record); + successCount++; + } catch (error) { + console.error( + `Failed to migrate record in ${entityName}:`, + error instanceof Error ? error.message : String(error), + ); + errorCount++; + } + } + console.log( + `Finished ${entityName}: ${successCount} successful, ${errorCount} failed.`, + ); + } + } + + console.log('\nMigration completed successfully.'); + } catch (error) { + console.error('Migration failed:', error); + } finally { + if (sqliteDataSource.isInitialized) { + await sqliteDataSource.destroy(); + } + if (pgDataSource.isInitialized) { + await pgDataSource.destroy(); + } + } +} + +migrate().catch((error) => { + console.error('Migration failed:', error); + process.exit(1); +}); diff --git a/apps/backend/test/setup/test-db.ts b/apps/backend/test/setup/test-db.ts index c8ab8a07..7bdb4a7a 100644 --- a/apps/backend/test/setup/test-db.ts +++ b/apps/backend/test/setup/test-db.ts @@ -5,6 +5,6 @@ export async function resetDatabase(dataSource: DataSource) { for (const entity of entities) { const repo = dataSource.getRepository(entity.name); - await repo.query(`DELETE FROM ${entity.tableName}`); + await repo.query(`DELETE FROM "${entity.tableName}"`); } }