Spaces:
Runtime error
Runtime error
| import { MigrationInterface, QueryRunner } from 'typeorm'; | |
| /** | |
| * Adds `gen_random_uuid()::varchar` DEFAULT to every `id` column on Postgres. | |
| * | |
| * Why this is needed: | |
| * The initial schema migration (1770108659848-AddMessageStatus) created the | |
| * `id` columns on Postgres as `varchar PRIMARY KEY NOT NULL` without a | |
| * DEFAULT. The TypeORM Postgres driver emits `INSERT ... VALUES (DEFAULT, ...)` | |
| * for `@PrimaryGeneratedColumn('uuid')` columns and expects the database to | |
| * supply the value. Without a column DEFAULT this fails with: | |
| * null value in column "id" of relation "<table>" violates not-null constraint | |
| * | |
| * This migration is a no-op on SQLite (TypeORM generates the UUID in the | |
| * driver layer there, so no DB default is needed). | |
| * | |
| * `gen_random_uuid()` is a core built-in from PostgreSQL 13; on PG <= 12 it lives | |
| * in the pgcrypto extension. We therefore version-gate: on PG 13+ no extension is | |
| * touched at all (the common case, and CREATE EXTENSION needs a privilege managed | |
| * Postgres often withholds), and only on PG <= 12 do we ensure pgcrypto β with a | |
| * clear, actionable error if the role cannot create it, instead of a boot crash-loop. | |
| * | |
| * NOTE: editing an already-recorded migration body only benefits new / currently | |
| * crash-looping deployments; healthy ones that already ran it are unaffected. | |
| */ | |
| export class AddUuidDefaultsForPostgres1779235200000 implements MigrationInterface { | |
| name = 'AddUuidDefaultsForPostgres1779235200000'; | |
| // Data-connection tables only β api_keys/audit_logs live on the separate 'main' connection. | |
| private readonly tables = ['sessions', 'webhooks', 'messages', 'message_batches']; | |
| public async up(queryRunner: QueryRunner): Promise<void> { | |
| if (queryRunner.dataSource.options.type !== 'postgres') return; | |
| // gen_random_uuid() is a core built-in from PostgreSQL 13+, so on any modern server we need NO | |
| // extension β and CREATE EXTENSION requires a privilege that managed Postgres (RDS/Cloud SQL/β¦) | |
| // often withholds, so running it unconditionally crash-loops boot there. Probe the version with a | |
| // non-erroring catalog query (never a caught SQL exception, which would poison the migration tx) | |
| // and only touch pgcrypto on PG <= 12. | |
| const versionRows = (await queryRunner.query(`SELECT current_setting('server_version_num')::int AS num`)) as | |
| { num: number | string }[] | undefined; | |
| const versionNum = Number(versionRows?.[0]?.num ?? 0); | |
| if (versionNum > 0 && versionNum < 130000) { | |
| // PG <= 12: gen_random_uuid() lives in pgcrypto. Check the catalog first (readable by any role); | |
| // only attempt CREATE EXTENSION when it is genuinely missing, and if the role can't create it, | |
| // fail with a clear, actionable error rather than a raw permission-denied crash-loop. | |
| const installed = (await queryRunner.query(`SELECT 1 FROM pg_extension WHERE extname = 'pgcrypto'`)) as | |
| unknown[] | undefined; | |
| if (!installed?.length) { | |
| try { | |
| await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS pgcrypto`); | |
| } catch (err) { | |
| throw new Error( | |
| `PostgreSQL ${versionNum} (< 13) needs the pgcrypto extension for gen_random_uuid(), but it is ` + | |
| `not installed and this database role cannot create it. Have a superuser run ` + | |
| `"CREATE EXTENSION pgcrypto;" once, then restart.`, | |
| { cause: err }, | |
| ); | |
| } | |
| } | |
| } | |
| // By this point gen_random_uuid() resolves (core on 13+, pgcrypto ensured on <= 12), so the DEFAULT | |
| // expression β which Postgres validates at ALTER time β is safe to set. | |
| for (const table of this.tables) { | |
| const exists = await queryRunner.hasTable(table); | |
| if (!exists) continue; | |
| await queryRunner.query(`ALTER TABLE "${table}" ALTER COLUMN "id" SET DEFAULT gen_random_uuid()::varchar`); | |
| } | |
| } | |
| public async down(queryRunner: QueryRunner): Promise<void> { | |
| if (queryRunner.dataSource.options.type !== 'postgres') return; | |
| for (const table of this.tables) { | |
| const exists = await queryRunner.hasTable(table); | |
| if (!exists) continue; | |
| await queryRunner.query(`ALTER TABLE "${table}" ALTER COLUMN "id" DROP DEFAULT`); | |
| } | |
| } | |
| } | |