build: rebuild API release bundle for the admin/expert table split
app.js, seed.js, the Prisma client in release/vendor and the new migration 20261026120000_split_admin_table, built with `pnpm build:release` (API only; the admin panel and user apps are unchanged by this work). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
06c8a5f434
commit
e2ec30f241
@@ -0,0 +1,167 @@
|
||||
-- Split panel staff out of "AdminUser" into their own table, "Admin".
|
||||
--
|
||||
-- WHY
|
||||
-- ---
|
||||
-- "AdminUser" held two unrelated kinds of person: panel staff and sales
|
||||
-- experts (role SALES_EXPERT). Because the mobile is unique per table, one
|
||||
-- number could not be both an admin and an expert. Staff belong to the ADMIN
|
||||
-- PANEL only; experts belong to the apps. They are now separate tables, each
|
||||
-- unique on its own.
|
||||
--
|
||||
-- SAFETY — READ BEFORE RUNNING ON PRODUCTION
|
||||
-- ------------------------------------------
|
||||
-- This migration NEVER modifies, deletes or locks "AdminUser" (the sales-expert
|
||||
-- table; ~12,000 rows in production). It only
|
||||
-- 1. creates "Admin" and its indexes/foreign keys,
|
||||
-- 2. COPIES the non-expert rows from "AdminUser" into it, keeping their ids,
|
||||
-- 3. re-points the "who did this" foreign keys from "AdminUser" to "Admin".
|
||||
--
|
||||
-- * Ids are preserved, so every issued panel token (its `sub` is the id) and
|
||||
-- every existing ledger / status / SMS / upload row keeps resolving.
|
||||
-- * The old non-expert rows are LEFT in "AdminUser", dormant. The code no
|
||||
-- longer reads them as admins. Remove them later, in a separate release, only
|
||||
-- once the split has been verified.
|
||||
-- * Re-runnable: every statement is guarded (IF NOT EXISTS / ON CONFLICT / drop
|
||||
-- then add), so a half-applied run can simply be run again.
|
||||
-- * No data is moved on the big tables (LedgerEntry, OrderStatusEvent, …):
|
||||
-- the existing columns keep their names and values; only the constraint's
|
||||
-- target changes.
|
||||
--
|
||||
-- The re-pointed foreign keys are added NOT VALID (checked for new/changed rows
|
||||
-- immediately, existing rows not scanned under a heavy lock) and then validated
|
||||
-- at the end. A row that references an id that is not an admin — only possible
|
||||
-- if a sales expert once appears as an actor — leaves that one constraint
|
||||
-- NOT VALID and prints a NOTICE instead of failing the deploy. See
|
||||
-- docs/ADMIN-TABLE-SPLIT.md for the check query.
|
||||
--
|
||||
-- ROLLBACK: deploy the previous code. Nothing was deleted, so the old code
|
||||
-- still finds every admin in "AdminUser". Admins created AFTER the split exist
|
||||
-- only in "Admin" and would need re-creating in the old system.
|
||||
|
||||
-- ── 1. The table ───────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "Admin" (
|
||||
"id" TEXT NOT NULL,
|
||||
"fullName" TEXT NOT NULL,
|
||||
"firstName" TEXT,
|
||||
"lastName" TEXT,
|
||||
"mobile" TEXT NOT NULL,
|
||||
"passwordHash" TEXT NOT NULL,
|
||||
"role" "AdminRole" NOT NULL,
|
||||
"userTypeId" INTEGER,
|
||||
"branchId" TEXT,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Admin_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "Admin_mobile_key" ON "Admin" ("mobile");
|
||||
CREATE INDEX IF NOT EXISTS "Admin_role_idx" ON "Admin" ("role");
|
||||
CREATE INDEX IF NOT EXISTS "Admin_userTypeId_idx" ON "Admin" ("userTypeId");
|
||||
CREATE INDEX IF NOT EXISTS "Admin_firstName_idx" ON "Admin" ("firstName");
|
||||
CREATE INDEX IF NOT EXISTS "Admin_lastName_idx" ON "Admin" ("lastName");
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Admin" ADD CONSTRAINT "Admin_userTypeId_fkey"
|
||||
FOREIGN KEY ("userTypeId") REFERENCES "user_type"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Admin" ADD CONSTRAINT "Admin_branchId_fkey"
|
||||
FOREIGN KEY ("branchId") REFERENCES "Branch"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
|
||||
-- ── 2. Copy the staff (everyone who is not a sales expert), ids preserved ──
|
||||
--
|
||||
-- Done BEFORE the name-sync trigger exists, so the stored names are copied
|
||||
-- exactly as they are rather than rebuilt.
|
||||
|
||||
INSERT INTO "Admin"
|
||||
("id", "fullName", "firstName", "lastName", "mobile", "passwordHash",
|
||||
"role", "userTypeId", "branchId", "isActive", "createdAt")
|
||||
SELECT
|
||||
"id", "fullName", "firstName", "lastName", "mobile", "passwordHash",
|
||||
"role", "userTypeId", "branchId", "isActive", "createdAt"
|
||||
FROM "AdminUser"
|
||||
WHERE "role" <> 'SALES_EXPERT'
|
||||
ON CONFLICT ("id") DO NOTHING;
|
||||
|
||||
-- Same two-way fullName <-> firstName/lastName sync as AdminUser, sharing its
|
||||
-- function (created in 20260922180000_sales_expert_profile_and_admin_name_parts).
|
||||
DROP TRIGGER IF EXISTS admin_sync_full_name_trg ON "Admin";
|
||||
CREATE TRIGGER admin_sync_full_name_trg
|
||||
BEFORE INSERT OR UPDATE ON "Admin"
|
||||
FOR EACH ROW EXECUTE FUNCTION admin_user_sync_full_name();
|
||||
|
||||
-- ── 3. Re-point the foreign keys that mean "an admin did this" ─────────────
|
||||
--
|
||||
-- Same constraint names, same column names, same ON DELETE / ON UPDATE actions;
|
||||
-- only the referenced table changes. "Order"."salesAgentId",
|
||||
-- "Customer"."referredByAgentId" and "SalesExpertProfile"."adminUserId" are
|
||||
-- about SALES EXPERTS and keep pointing at "AdminUser".
|
||||
|
||||
ALTER TABLE "Workshop" DROP CONSTRAINT IF EXISTS "Workshop_supervisorId_fkey";
|
||||
ALTER TABLE "Workshop" ADD CONSTRAINT "Workshop_supervisorId_fkey"
|
||||
FOREIGN KEY ("supervisorId") REFERENCES "Admin"("id") ON DELETE SET NULL ON UPDATE CASCADE NOT VALID;
|
||||
|
||||
ALTER TABLE "OrderStatusEvent" DROP CONSTRAINT IF EXISTS "OrderStatusEvent_actorAdminUserId_fkey";
|
||||
ALTER TABLE "OrderStatusEvent" ADD CONSTRAINT "OrderStatusEvent_actorAdminUserId_fkey"
|
||||
FOREIGN KEY ("actorAdminUserId") REFERENCES "Admin"("id") ON DELETE SET NULL ON UPDATE CASCADE NOT VALID;
|
||||
|
||||
ALTER TABLE "PreOrderStatusEvent" DROP CONSTRAINT IF EXISTS "PreOrderStatusEvent_actorAdminUserId_fkey";
|
||||
ALTER TABLE "PreOrderStatusEvent" ADD CONSTRAINT "PreOrderStatusEvent_actorAdminUserId_fkey"
|
||||
FOREIGN KEY ("actorAdminUserId") REFERENCES "Admin"("id") ON DELETE SET NULL ON UPDATE CASCADE NOT VALID;
|
||||
|
||||
ALTER TABLE "LedgerEntry" DROP CONSTRAINT IF EXISTS "LedgerEntry_postedByAdminUserId_fkey";
|
||||
ALTER TABLE "LedgerEntry" ADD CONSTRAINT "LedgerEntry_postedByAdminUserId_fkey"
|
||||
FOREIGN KEY ("postedByAdminUserId") REFERENCES "Admin"("id") ON DELETE SET NULL ON UPDATE CASCADE NOT VALID;
|
||||
|
||||
ALTER TABLE "BranchAllocation" DROP CONSTRAINT IF EXISTS "BranchAllocation_allocatorAdminUserId_fkey";
|
||||
ALTER TABLE "BranchAllocation" ADD CONSTRAINT "BranchAllocation_allocatorAdminUserId_fkey"
|
||||
FOREIGN KEY ("allocatorAdminUserId") REFERENCES "Admin"("id") ON DELETE SET NULL ON UPDATE CASCADE NOT VALID;
|
||||
|
||||
ALTER TABLE "SmsMessage" DROP CONSTRAINT IF EXISTS "SmsMessage_sentByAdminUserId_fkey";
|
||||
ALTER TABLE "SmsMessage" ADD CONSTRAINT "SmsMessage_sentByAdminUserId_fkey"
|
||||
FOREIGN KEY ("sentByAdminUserId") REFERENCES "Admin"("id") ON DELETE SET NULL ON UPDATE CASCADE NOT VALID;
|
||||
|
||||
ALTER TABLE "Notification" DROP CONSTRAINT IF EXISTS "Notification_sentByAdminUserId_fkey";
|
||||
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_sentByAdminUserId_fkey"
|
||||
FOREIGN KEY ("sentByAdminUserId") REFERENCES "Admin"("id") ON DELETE SET NULL ON UPDATE CASCADE NOT VALID;
|
||||
|
||||
ALTER TABLE "UploadedFile" DROP CONSTRAINT IF EXISTS "UploadedFile_uploadedByAdminUserId_fkey";
|
||||
ALTER TABLE "UploadedFile" ADD CONSTRAINT "UploadedFile_uploadedByAdminUserId_fkey"
|
||||
FOREIGN KEY ("uploadedByAdminUserId") REFERENCES "Admin"("id") ON DELETE SET NULL ON UPDATE CASCADE NOT VALID;
|
||||
|
||||
-- Panel tokens are issued to staff only. CASCADE as before: deleting an admin
|
||||
-- ends their sessions.
|
||||
ALTER TABLE "AdminSession" DROP CONSTRAINT IF EXISTS "AdminSession_adminUserId_fkey";
|
||||
ALTER TABLE "AdminSession" ADD CONSTRAINT "AdminSession_adminUserId_fkey"
|
||||
FOREIGN KEY ("adminUserId") REFERENCES "Admin"("id") ON DELETE CASCADE ON UPDATE CASCADE NOT VALID;
|
||||
|
||||
-- ── 4. Validate; never fail the deploy over an old orphan ──────────────────
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
c record;
|
||||
BEGIN
|
||||
FOR c IN
|
||||
SELECT * FROM (VALUES
|
||||
('Workshop', 'Workshop_supervisorId_fkey'),
|
||||
('OrderStatusEvent', 'OrderStatusEvent_actorAdminUserId_fkey'),
|
||||
('PreOrderStatusEvent', 'PreOrderStatusEvent_actorAdminUserId_fkey'),
|
||||
('LedgerEntry', 'LedgerEntry_postedByAdminUserId_fkey'),
|
||||
('BranchAllocation', 'BranchAllocation_allocatorAdminUserId_fkey'),
|
||||
('SmsMessage', 'SmsMessage_sentByAdminUserId_fkey'),
|
||||
('Notification', 'Notification_sentByAdminUserId_fkey'),
|
||||
('UploadedFile', 'UploadedFile_uploadedByAdminUserId_fkey'),
|
||||
('AdminSession', 'AdminSession_adminUserId_fkey')
|
||||
) AS t(tbl, con)
|
||||
LOOP
|
||||
BEGIN
|
||||
EXECUTE format('ALTER TABLE %I VALIDATE CONSTRAINT %I', c.tbl, c.con);
|
||||
EXCEPTION WHEN foreign_key_violation THEN
|
||||
RAISE NOTICE '% on % left NOT VALID: some existing rows reference an id that is not in "Admin" (see docs/ADMIN-TABLE-SPLIT.md)', c.con, c.tbl;
|
||||
END;
|
||||
END LOOP;
|
||||
END $$;
|
||||
Reference in New Issue
Block a user