fix(api): survive existing-schema boots without DB shell

Nginx 502 Host-not-found meant the API never stayed listening. Stamp all
migrations when tracking is empty but a late table exists, apply DDL
statement-by-statement skipping already-exists, harden remaining CREATE
TYPE/TABLE files, and never let seed crash the process.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
mahmood
2026-10-27 17:35:25 +03:30
co-authored by Cursor
parent b05b6515f2
commit 15adbb7fe1
11 changed files with 202 additions and 169 deletions
@@ -1,5 +1,9 @@
-- CreateEnum
CREATE TYPE "ProductImageRole" AS ENUM ('FEATURED', 'FEATURED_MOBILE', 'GALLERY', 'GALLERY_MOBILE');
-- CreateEnum (idempotent — live DBs may already have this from prisma db push)
DO $$ BEGIN
CREATE TYPE "ProductImageRole" AS ENUM ('FEATURED', 'FEATURED_MOBILE', 'GALLERY', 'GALLERY_MOBILE');
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
-- AlterTable
ALTER TABLE "ProductImage" ADD COLUMN "role" "ProductImageRole" NOT NULL DEFAULT 'GALLERY';
ALTER TABLE "ProductImage" ADD COLUMN IF NOT EXISTS "role" "ProductImageRole" NOT NULL DEFAULT 'GALLERY';
@@ -10,7 +10,7 @@
-- token whose row is revoked. Rows are only written (and only enforced) while
-- session tracking is enabled, so a deployment that leaves it off keeps exactly
-- the previous behaviour and the previous query count.
CREATE TABLE "AdminSession" (
CREATE TABLE IF NOT EXISTS "AdminSession" (
"id" TEXT NOT NULL,
"jti" TEXT NOT NULL,
"adminUserId" TEXT NOT NULL,
@@ -27,12 +27,16 @@ CREATE TABLE "AdminSession" (
CONSTRAINT "AdminSession_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "AdminSession_jti_key" ON "AdminSession"("jti");
CREATE INDEX "AdminSession_adminUserId_idx" ON "AdminSession"("adminUserId");
CREATE INDEX "AdminSession_iamSessionId_idx" ON "AdminSession"("iamSessionId");
CREATE INDEX "AdminSession_iamSub_idx" ON "AdminSession"("iamSub");
CREATE INDEX "AdminSession_expiresAt_idx" ON "AdminSession"("expiresAt");
CREATE UNIQUE INDEX IF NOT EXISTS "AdminSession_jti_key" ON "AdminSession"("jti");
CREATE INDEX IF NOT EXISTS "AdminSession_adminUserId_idx" ON "AdminSession"("adminUserId");
CREATE INDEX IF NOT EXISTS "AdminSession_iamSessionId_idx" ON "AdminSession"("iamSessionId");
CREATE INDEX IF NOT EXISTS "AdminSession_iamSub_idx" ON "AdminSession"("iamSub");
CREATE INDEX IF NOT EXISTS "AdminSession_expiresAt_idx" ON "AdminSession"("expiresAt");
ALTER TABLE "AdminSession"
DO $$ BEGIN
ALTER TABLE "AdminSession"
ADD CONSTRAINT "AdminSession_adminUserId_fkey"
FOREIGN KEY ("adminUserId") REFERENCES "AdminUser"("id") ON DELETE CASCADE ON UPDATE CASCADE;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
@@ -7,7 +7,7 @@
--
-- No foreign key on purpose: an app token's subject is a Customer for a customer
-- and an AdminUser for a sales expert (D21), which one FK cannot express.
CREATE TABLE "AppSession" (
CREATE TABLE IF NOT EXISTS "AppSession" (
"id" TEXT NOT NULL,
"jti" TEXT NOT NULL,
"subjectId" TEXT NOT NULL,
@@ -26,8 +26,8 @@ CREATE TABLE "AppSession" (
CONSTRAINT "AppSession_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "AppSession_jti_key" ON "AppSession"("jti");
CREATE INDEX "AppSession_subjectId_idx" ON "AppSession"("subjectId");
CREATE INDEX "AppSession_iamSessionId_idx" ON "AppSession"("iamSessionId");
CREATE INDEX "AppSession_iamSub_idx" ON "AppSession"("iamSub");
CREATE INDEX "AppSession_expiresAt_idx" ON "AppSession"("expiresAt");
CREATE UNIQUE INDEX IF NOT EXISTS "AppSession_jti_key" ON "AppSession"("jti");
CREATE INDEX IF NOT EXISTS "AppSession_subjectId_idx" ON "AppSession"("subjectId");
CREATE INDEX IF NOT EXISTS "AppSession_iamSessionId_idx" ON "AppSession"("iamSessionId");
CREATE INDEX IF NOT EXISTS "AppSession_iamSub_idx" ON "AppSession"("iamSub");
CREATE INDEX IF NOT EXISTS "AppSession_expiresAt_idx" ON "AppSession"("expiresAt");
@@ -5,28 +5,30 @@
-- jobs run from, the issue list pulls report into, and the cached balance and
-- stock columns read back from the accounting software. Nothing existing
-- changes meaning: goldDebtGrams/fiatDebtRial stay the internal ledger caches.
-- Statements are idempotent so a DB that already has these objects (db push /
-- prior migrate) still records this file without crashing the API boot.
-- CreateEnum
CREATE TYPE "AccountingEntityType" AS ENUM ('CUSTOMER', 'PRODUCT');
DO $$ BEGIN
CREATE TYPE "AccountingEntityType" AS ENUM ('CUSTOMER', 'PRODUCT');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- CreateEnum
CREATE TYPE "AccountingOutboxKind" AS ENUM ('CUSTOMER_PUSH', 'PRODUCT_PUSH', 'ORDER_DOCUMENT', 'ORDER_DOCUMENT_DELETE');
DO $$ BEGIN
CREATE TYPE "AccountingOutboxKind" AS ENUM ('CUSTOMER_PUSH', 'PRODUCT_PUSH', 'ORDER_DOCUMENT', 'ORDER_DOCUMENT_DELETE');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- CreateEnum
CREATE TYPE "AccountingOutboxStatus" AS ENUM ('PENDING', 'SENDING', 'SENT', 'FAILED', 'NEEDS_REVIEW', 'RESOLVED');
DO $$ BEGIN
CREATE TYPE "AccountingOutboxStatus" AS ENUM ('PENDING', 'SENDING', 'SENT', 'FAILED', 'NEEDS_REVIEW', 'RESOLVED');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- AlterTable
ALTER TABLE "Customer" ADD COLUMN "accountingBalanceAt" TIMESTAMP(3),
ADD COLUMN "accountingGoldBalance" DECIMAL(14,4),
ADD COLUMN "accountingRialBalance" DECIMAL(20,2);
ALTER TABLE "Customer" ADD COLUMN IF NOT EXISTS "accountingBalanceAt" TIMESTAMP(3);
ALTER TABLE "Customer" ADD COLUMN IF NOT EXISTS "accountingGoldBalance" DECIMAL(14,4);
ALTER TABLE "Customer" ADD COLUMN IF NOT EXISTS "accountingRialBalance" DECIMAL(20,2);
-- AlterTable
ALTER TABLE "Product" ADD COLUMN "accountingStockAt" TIMESTAMP(3),
ADD COLUMN "accountingStockCount" DECIMAL(14,4),
ADD COLUMN "accountingStockWeight" DECIMAL(14,4);
ALTER TABLE "Product" ADD COLUMN IF NOT EXISTS "accountingStockAt" TIMESTAMP(3);
ALTER TABLE "Product" ADD COLUMN IF NOT EXISTS "accountingStockCount" DECIMAL(14,4);
ALTER TABLE "Product" ADD COLUMN IF NOT EXISTS "accountingStockWeight" DECIMAL(14,4);
-- CreateTable
CREATE TABLE "AccountingLink" (
CREATE TABLE IF NOT EXISTS "AccountingLink" (
"id" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"entityType" "AccountingEntityType" NOT NULL,
@@ -41,8 +43,7 @@ CREATE TABLE "AccountingLink" (
CONSTRAINT "AccountingLink_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AccountingOutbox" (
CREATE TABLE IF NOT EXISTS "AccountingOutbox" (
"id" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"kind" "AccountingOutboxKind" NOT NULL,
@@ -63,8 +64,7 @@ CREATE TABLE "AccountingOutbox" (
CONSTRAINT "AccountingOutbox_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AccountingIssue" (
CREATE TABLE IF NOT EXISTS "AccountingIssue" (
"id" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"kind" TEXT NOT NULL,
@@ -79,27 +79,13 @@ CREATE TABLE "AccountingIssue" (
CONSTRAINT "AccountingIssue_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "AccountingLink_entityType_entityId_idx" ON "AccountingLink"("entityType", "entityId");
-- CreateIndex
CREATE UNIQUE INDEX "AccountingLink_provider_entityType_entityId_key" ON "AccountingLink"("provider", "entityType", "entityId");
-- CreateIndex
CREATE UNIQUE INDEX "AccountingLink_provider_entityType_externalCode_key" ON "AccountingLink"("provider", "entityType", "externalCode");
-- CreateIndex
CREATE INDEX "AccountingOutbox_status_nextAttemptAt_idx" ON "AccountingOutbox"("status", "nextAttemptAt");
-- CreateIndex
CREATE INDEX "AccountingOutbox_kind_entityId_idx" ON "AccountingOutbox"("kind", "entityId");
-- CreateIndex
CREATE INDEX "AccountingIssue_resolvedAt_idx" ON "AccountingIssue"("resolvedAt");
-- CreateIndex
CREATE UNIQUE INDEX "AccountingIssue_provider_kind_externalCode_key" ON "AccountingIssue"("provider", "kind", "externalCode");
CREATE INDEX IF NOT EXISTS "AccountingLink_entityType_entityId_idx" ON "AccountingLink"("entityType", "entityId");
CREATE UNIQUE INDEX IF NOT EXISTS "AccountingLink_provider_entityType_entityId_key" ON "AccountingLink"("provider", "entityType", "entityId");
CREATE UNIQUE INDEX IF NOT EXISTS "AccountingLink_provider_entityType_externalCode_key" ON "AccountingLink"("provider", "entityType", "externalCode");
CREATE INDEX IF NOT EXISTS "AccountingOutbox_status_nextAttemptAt_idx" ON "AccountingOutbox"("status", "nextAttemptAt");
CREATE INDEX IF NOT EXISTS "AccountingOutbox_kind_entityId_idx" ON "AccountingOutbox"("kind", "entityId");
CREATE INDEX IF NOT EXISTS "AccountingIssue_resolvedAt_idx" ON "AccountingIssue"("resolvedAt");
CREATE UNIQUE INDEX IF NOT EXISTS "AccountingIssue_provider_kind_externalCode_key" ON "AccountingIssue"("provider", "kind", "externalCode");
-- The new admin screen's permission. The seeded super-admin user type (85,
-- prisma/seed.ts) gets every rid in ALL_RIDS on a fresh seed; an existing