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:
s.esmaeili
2026-10-25 23:55:07 +03:30
co-authored by Claude Sonnet 5.5
parent 06c8a5f434
commit e2ec30f241
10 changed files with 5403 additions and 3445 deletions
@@ -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 $$;