From 162211b5fb0def8fc272760f41c445ef252e50ec Mon Sep 17 00:00:00 2001 From: Awosdot Date: Sun, 18 Oct 2026 21:07:53 +0100 Subject: [PATCH] fix(prisma): reconcile schema drift, indexes, and migration timestamps (#1617) --- backend/DLQ_IMPLEMENTATION_SUMMARY.md | 6 +++--- backend/docs/DEAD_LETTER_QUEUE.md | 2 +- .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 13 ++++++++++++ ...econcile_email_queue_index_and_api_key.sql | 8 ++++++++ backend/prisma/schema.prisma | 20 ++----------------- backend/scripts/check-migrations.js | 12 ++++++++++- 11 files changed, 38 insertions(+), 23 deletions(-) rename backend/prisma/migrations/{20260428164351_add_event_replay => 20260428164351_add_admin_audit_log}/migration.sql (100%) rename backend/prisma/migrations/{20260529000000_add_webhook_persistence => 20260529000100_add_webhook_persistence}/migration.sql (100%) rename backend/prisma/migrations/{20260725000000_add_scoped_admin_tokens => 20260725000100_add_scoped_admin_tokens}/migration.sql (100%) rename backend/prisma/migrations/{20260729000000_add_job_dead_letter => 20260729000100_add_job_dead_letter}/migration.sql (100%) rename backend/prisma/migrations/{20260729000000_add_wallet_alias_persistence => 20260729000200_add_wallet_alias_persistence}/migration.sql (100%) create mode 100644 backend/prisma/migrations/20261018000000_reconcile_email_queue_index_and_api_key/migration.sql create mode 100644 backend/prisma/rollbacks/20261018000000_reconcile_email_queue_index_and_api_key.sql diff --git a/backend/DLQ_IMPLEMENTATION_SUMMARY.md b/backend/DLQ_IMPLEMENTATION_SUMMARY.md index 1cbe6f33e..8d0617a43 100644 --- a/backend/DLQ_IMPLEMENTATION_SUMMARY.md +++ b/backend/DLQ_IMPLEMENTATION_SUMMARY.md @@ -21,7 +21,7 @@ This document summarizes the comprehensive implementation of production-hardened ### ✅ Task #2: Implement Database Persistence for DLQ Records **Files Modified**: - `backend/prisma/schema.prisma` - Added `JobDeadLetter` model -- `backend/prisma/migrations/20260729000000_add_job_dead_letter/migration.sql` - Created migration +- `backend/prisma/migrations/20260729000100_add_job_dead_letter/migration.sql` - Created migration - `backend/src/jobGovernance.ts` - Integrated Prisma persistence - `backend/src/index.ts` - Added initialization call @@ -173,7 +173,7 @@ This document summarizes the comprehensive implementation of production-hardened - Added `JobDeadLetter` model (line 304) - Includes all required fields and indexes -4. **backend/prisma/migrations/20260729000000_add_job_dead_letter/migration.sql** +4. **backend/prisma/migrations/20260729000100_add_job_dead_letter/migration.sql** - Creates `JobDeadLetter` table - Creates 5 indexes for efficient querying @@ -207,7 +207,7 @@ This document summarizes the comprehensive implementation of production-hardened 2. **Verify schema**: ```bash - npx prisma db execute --stdin < prisma/migrations/20260729000000_add_job_dead_letter/migration.sql + npx prisma db execute --stdin < prisma/migrations/20260729000100_add_job_dead_letter/migration.sql ``` 3. **Build backend**: diff --git a/backend/docs/DEAD_LETTER_QUEUE.md b/backend/docs/DEAD_LETTER_QUEUE.md index 95209d75c..c6631b9a2 100644 --- a/backend/docs/DEAD_LETTER_QUEUE.md +++ b/backend/docs/DEAD_LETTER_QUEUE.md @@ -546,7 +546,7 @@ DELETE FROM "JobDeadLetter" ## 8. Production Deployment Checklist -- [ ] Database migration applied (`20260729000000_add_job_dead_letter`) +- [ ] Database migration applied (`20260729000100_add_job_dead_letter`) - [ ] `JobDeadLetter` table exists with all indexes - [ ] `initializeJobGovernance()` called at app startup - [ ] API key roles include `JOBS_WRITE` permission for operators diff --git a/backend/prisma/migrations/20260428164351_add_event_replay/migration.sql b/backend/prisma/migrations/20260428164351_add_admin_audit_log/migration.sql similarity index 100% rename from backend/prisma/migrations/20260428164351_add_event_replay/migration.sql rename to backend/prisma/migrations/20260428164351_add_admin_audit_log/migration.sql diff --git a/backend/prisma/migrations/20260529000000_add_webhook_persistence/migration.sql b/backend/prisma/migrations/20260529000100_add_webhook_persistence/migration.sql similarity index 100% rename from backend/prisma/migrations/20260529000000_add_webhook_persistence/migration.sql rename to backend/prisma/migrations/20260529000100_add_webhook_persistence/migration.sql diff --git a/backend/prisma/migrations/20260725000000_add_scoped_admin_tokens/migration.sql b/backend/prisma/migrations/20260725000100_add_scoped_admin_tokens/migration.sql similarity index 100% rename from backend/prisma/migrations/20260725000000_add_scoped_admin_tokens/migration.sql rename to backend/prisma/migrations/20260725000100_add_scoped_admin_tokens/migration.sql diff --git a/backend/prisma/migrations/20260729000000_add_job_dead_letter/migration.sql b/backend/prisma/migrations/20260729000100_add_job_dead_letter/migration.sql similarity index 100% rename from backend/prisma/migrations/20260729000000_add_job_dead_letter/migration.sql rename to backend/prisma/migrations/20260729000100_add_job_dead_letter/migration.sql diff --git a/backend/prisma/migrations/20260729000000_add_wallet_alias_persistence/migration.sql b/backend/prisma/migrations/20260729000200_add_wallet_alias_persistence/migration.sql similarity index 100% rename from backend/prisma/migrations/20260729000000_add_wallet_alias_persistence/migration.sql rename to backend/prisma/migrations/20260729000200_add_wallet_alias_persistence/migration.sql diff --git a/backend/prisma/migrations/20261018000000_reconcile_email_queue_index_and_api_key/migration.sql b/backend/prisma/migrations/20261018000000_reconcile_email_queue_index_and_api_key/migration.sql new file mode 100644 index 000000000..87b48e5a6 --- /dev/null +++ b/backend/prisma/migrations/20261018000000_reconcile_email_queue_index_and_api_key/migration.sql @@ -0,0 +1,13 @@ +-- Issue #1617: reconcile schema.prisma with the migration history. +-- migration-safety: allow-nonconcurrent-indexes +-- migration-safety: allow-index-drop +-- SQLite does not support CONCURRENTLY. + +-- The email worker polls on (status, nextRetryAt); the composite index also +-- serves status-only lookups, so the single-column status index is redundant. +CREATE INDEX "EmailQueue_status_nextRetryAt_idx" ON "EmailQueue"("status", "nextRetryAt"); +DROP INDEX "EmailQueue_status_idx"; + +-- apiKeyAuth records last use on every authenticated request; the column was +-- only declared in a duplicate (removed) ApiKey model, never in a migration. +ALTER TABLE "ApiKey" ADD COLUMN "lastUsedAt" DATETIME; diff --git a/backend/prisma/rollbacks/20261018000000_reconcile_email_queue_index_and_api_key.sql b/backend/prisma/rollbacks/20261018000000_reconcile_email_queue_index_and_api_key.sql new file mode 100644 index 000000000..139ac9a16 --- /dev/null +++ b/backend/prisma/rollbacks/20261018000000_reconcile_email_queue_index_and_api_key.sql @@ -0,0 +1,8 @@ +-- Rollback for 20261018000000_reconcile_email_queue_index_and_api_key (#1617). +-- Apply with: npm run prisma:rollback -- --name 20261018000000_reconcile_email_queue_index_and_api_key + +-- migration-safety: canary-safe + +CREATE INDEX IF NOT EXISTS "EmailQueue_status_idx" ON "EmailQueue"("status"); +DROP INDEX IF EXISTS "EmailQueue_status_nextRetryAt_idx"; +ALTER TABLE "ApiKey" DROP COLUMN "lastUsedAt"; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 151c82c85..bd26e3205 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -143,23 +143,6 @@ model AdminAuditLog { @@index([actor]) } -model ApiKey { - id String @id @default(uuid()) - tenantId String - hashedKey String @unique - scopes String // JSON-serialised string[] - expiresAt DateTime? - lastUsedAt DateTime? - revoked Boolean @default(false) - revokedAt DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([tenantId]) - @@index([hashedKey]) - @@index([expiresAt]) -} - model ApiKeyAuditEvent { id String @id @default(uuid()) actor String @@ -343,7 +326,7 @@ model EmailQueue { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - @@index([status]) + @@index([status, nextRetryAt]) @@index([createdAt]) } @@ -547,6 +530,7 @@ model ApiKey { scopes String @default("[]") createdAt DateTime @default(now()) expiresAt DateTime? + lastUsedAt DateTime? isActive Boolean @default(true) deletedAt DateTime? deletedBy String? diff --git a/backend/scripts/check-migrations.js b/backend/scripts/check-migrations.js index 79c0d3a0f..601131938 100644 --- a/backend/scripts/check-migrations.js +++ b/backend/scripts/check-migrations.js @@ -31,9 +31,14 @@ function checkApiKeyMigration(content, file) { return results; } + // Only a migration that creates the table owns the full column set. A + // baseline `CREATE TABLE IF NOT EXISTS` is a no-op on existing databases and + // an ALTER-only migration adds individual columns. + const isBaselineOnly = !/create\s+table\s+(?!if\s+not\s+exists)["`]?apikey["`]?/i.test(content); + // The migration must declare the per-tenant credential columns. const missing = API_KEY_REQUIRED_COLUMNS.filter((col) => !lowered.includes(col)); - if (missing.length > 0) { + if (missing.length > 0 && !isBaselineOnly) { results.push({ file, severity: 'error', @@ -103,6 +108,9 @@ function checkFile(file) { const allowsWebhookSecretDrop = lowered.includes('migration-safety: allow-webhook-secret-drop') && /alter\s+table\s+["`]?webhookendpoint["`]?\s+drop\s+column\s+["`]?secret["`]?/i.test(content); + const allowsIndexDrop = + lowered.includes('migration-safety: allow-index-drop') && + !/\bdrop\s+(table|column)\b/i.test(content); const isCanarySafeOptIn = lowered.includes( 'migration-safety: canary-safe' ); @@ -118,6 +126,8 @@ function checkFile(file) { for (const rule of highRiskPatterns) { if (allowsWebhookSecretDrop && rule.label === 'blocking alter/drop change') continue; + if (allowsWebhookSecretDrop && rule.label === 'irreversible drop operation') continue; + if (allowsIndexDrop && rule.label === 'irreversible drop operation') continue; if (rule.pattern.test(content)) { results.push({ file, severity: 'error', message: rule.label }); }