Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e85d909f74 | |||
| c4385617c6 | |||
| 8cd5a2ef4d | |||
| 2566fb8f20 | |||
| 4868f18dfd | |||
| 37e42b3104 | |||
| d617c69571 | |||
| 76d9850464 | |||
| 96dbb8c41d | |||
| 636fa713e1 | |||
| 6120b96c7c | |||
| eb92f99c4a | |||
| 587fd4ec95 | |||
| 8cf72d926d | |||
| 8721f0b63c | |||
| 027e012a58 | |||
| b3db206588 | |||
| 6538406db2 | |||
| e2eacbc9fe | |||
| e639cc82d1 | |||
| f2931d7be2 | |||
| d4a4ddce37 | |||
| bd384bdf5c | |||
| 411c42b2c4 | |||
| bf97849324 | |||
| 7181d41b24 | |||
| 4e9c4c5e08 | |||
| 16c959434b | |||
| 23484dc90a | |||
| 6a81a52a50 | |||
| 5a4b9a98bd | |||
| f7f88156e1 | |||
| 8af5a49d14 |
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"gitea": {
|
|
||||||
"type": "http",
|
|
||||||
"url": "https://git-mcp.farh.net/mcp",
|
|
||||||
"headers": {
|
|
||||||
"Authorization": "Bearer ${GITEA_TOKEN}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"wait-for-db": "node ./scripts/wait-for-db.mjs",
|
"wait-for-db": "node ./scripts/wait-for-db.mjs",
|
||||||
"migrate": "node ./scripts/wait-for-db.mjs && drizzle-kit migrate",
|
"migrate": "node ./scripts/wait-for-db.mjs && drizzle-kit migrate",
|
||||||
"seed": "node ./scripts/wait-for-db.mjs && tsx src/seed.ts",
|
"seed": "node ./scripts/wait-for-db.mjs && tsx src/seed.ts",
|
||||||
"reset": "node ./scripts/wait-for-db.mjs && tsx src/reset.ts",
|
"reset": "node ./scripts/wait-for-db.mjs && tsx src/reset.ts && drizzle-kit migrate && tsx src/seed.ts",
|
||||||
"studio": "drizzle-kit studio",
|
"studio": "drizzle-kit studio",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
|
|||||||
+39
-114
@@ -1,52 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* reset.ts — Drop all application tables, re-run migrations, and re-seed.
|
* reset.ts — Drop all application tables and re-run migrations + seed.
|
||||||
*
|
*
|
||||||
* Intended for local development only. Never run against production.
|
* Intended for local development only. Never run against production.
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* DATABASE_URL=postgres://... npx tsx packages/db/src/reset.ts
|
* DATABASE_URL=postgres://... npx tsx packages/db/src/reset.ts
|
||||||
*
|
|
||||||
* GRO-2139: the entire drop→migrate→seed chain runs inside a single
|
|
||||||
* Postgres advisory lock (SEED_ADVISORY_LOCK_KEY) so a concurrent
|
|
||||||
* `seed.ts` (e.g. the dev `seed-test-data-*` Job being recreated at
|
|
||||||
* the top of the hour) cannot interleave between `reset.ts` (DROP)
|
|
||||||
* and `seed.ts` (TRUNCATE+insert) and collide on `invoices_pkey`.
|
|
||||||
*
|
|
||||||
* Why this matters: `seed.ts` derives every primary key from a single
|
|
||||||
* shared Mulberry32 PRNG seeded with 42 (see `createPrng(42)` and
|
|
||||||
* `uuid()` in seed.ts). Two concurrent same-profile seeders therefore
|
|
||||||
* emit *identical* ids for the same logical row, and any moment
|
|
||||||
* between a concurrent `seed.ts` TRUNCATE and INSERT is exactly the
|
|
||||||
* window in which the second seeder's INSERT can hit a pkey already
|
|
||||||
* taken by the first. Pre-GRO-2123 this raced unconditionally;
|
|
||||||
* GRO-2123 added the advisory lock around `runSeedBody` but left
|
|
||||||
* `reset.ts` and `drizzle-kit migrate` outside the lock. This script
|
|
||||||
* now wraps the *whole* chain in the same lock: `withSeedAdvisoryLock`
|
|
||||||
* pins the lock to one reserved session and the DROP → migrate → seed
|
|
||||||
* work runs on the rest of the pool, so the lock guarantees mutual
|
|
||||||
* exclusion against any concurrent seeder for the entire chain.
|
|
||||||
*
|
|
||||||
* See: groombook/infra `apps/base/reset-cronjob.yaml` (CronJob) and
|
|
||||||
* `apps/base/seed-job.yaml` (one-shot Job) — both invoke the same
|
|
||||||
* `seed.ts` code path on the same database in `groombook-dev`.
|
|
||||||
*/
|
*/
|
||||||
import postgres from "postgres";
|
|
||||||
import { drizzle } from "drizzle-orm/postgres-js";
|
|
||||||
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import { dirname, resolve } from "node:path";
|
|
||||||
import * as schema from "./schema.js";
|
|
||||||
import {
|
|
||||||
SEED_ADVISORY_LOCK_KEY,
|
|
||||||
withSeedAdvisoryLock,
|
|
||||||
getProfile,
|
|
||||||
runSeedBody,
|
|
||||||
profiles,
|
|
||||||
} from "./seed.js";
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
import postgres from "postgres";
|
||||||
const __dirname = dirname(__filename);
|
|
||||||
const MIGRATIONS_FOLDER = resolve(__dirname, "../migrations");
|
|
||||||
|
|
||||||
async function reset() {
|
async function reset() {
|
||||||
const url = process.env.DATABASE_URL;
|
const url = process.env.DATABASE_URL;
|
||||||
@@ -55,88 +16,52 @@ async function reset() {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (process.env.NODE_ENV === "production" && process.env.ALLOW_RESET !== "true") {
|
||||||
process.env.NODE_ENV === "production" &&
|
console.error("[FATAL] db:reset must not be run in production without ALLOW_RESET=true.");
|
||||||
process.env.ALLOW_RESET !== "true"
|
|
||||||
) {
|
|
||||||
console.error(
|
|
||||||
"[FATAL] db:reset must not be run in production without ALLOW_RESET=true.",
|
|
||||||
);
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pool sizing is load-bearing here. `withSeedAdvisoryLock` does
|
const client = postgres(url, { max: 1 });
|
||||||
// `pool.reserve()` to pin the advisory lock to one dedicated session
|
|
||||||
// (a session-level lock released on a *different* pooled connection is
|
|
||||||
// a no-op), and the DROP / migrate / seed work then runs on the
|
|
||||||
// *remaining* pooled connections. The lock provides mutual exclusion
|
|
||||||
// across processes regardless of how many connections the work uses —
|
|
||||||
// it does NOT require the work to share the lock's session.
|
|
||||||
//
|
|
||||||
// Therefore `max` must be ≥ 2: 1 reserved for the lock + ≥1 free for
|
|
||||||
// the work. `max: 1` would let `reserve()` consume the only connection
|
|
||||||
// and every query inside the callback would block forever waiting for
|
|
||||||
// a connection that never frees (connection-starvation deadlock). We
|
|
||||||
// use `max: 6` to match `seed()`'s headroom (1 reserved + 5 work).
|
|
||||||
const client = postgres(url, { max: 6 });
|
|
||||||
const db = drizzle(client, { schema });
|
|
||||||
|
|
||||||
try {
|
console.log("Dropping all application tables...\n");
|
||||||
await withSeedAdvisoryLock(client, async () => {
|
|
||||||
console.log("Dropping all application tables...\n");
|
|
||||||
|
|
||||||
// Drop dependencies (tables) first
|
// Drop in dependency order (children before parents)
|
||||||
await client`
|
await client`
|
||||||
DO $$ DECLARE
|
DO $$ DECLARE
|
||||||
r RECORD;
|
r RECORD;
|
||||||
BEGIN
|
BEGIN
|
||||||
FOR r IN (
|
FOR r IN (
|
||||||
SELECT tablename FROM pg_tables
|
SELECT tablename FROM pg_tables
|
||||||
WHERE schemaname = 'public'
|
WHERE schemaname = 'public'
|
||||||
) LOOP
|
) LOOP
|
||||||
EXECUTE 'DROP TABLE IF EXISTS public.' || quote_ident(r.tablename) || ' CASCADE';
|
EXECUTE 'DROP TABLE IF EXISTS public.' || quote_ident(r.tablename) || ' CASCADE';
|
||||||
END LOOP;
|
END LOOP;
|
||||||
END $$;
|
END $$;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Drop custom enums
|
// Drop custom enums
|
||||||
await client`
|
await client`
|
||||||
DO $$ DECLARE
|
DO $$ DECLARE
|
||||||
r RECORD;
|
r RECORD;
|
||||||
BEGIN
|
BEGIN
|
||||||
FOR r IN (
|
FOR r IN (
|
||||||
SELECT typname FROM pg_type
|
SELECT typname FROM pg_type
|
||||||
WHERE typtype = 'e' AND typnamespace = (
|
WHERE typtype = 'e' AND typnamespace = (
|
||||||
SELECT oid FROM pg_namespace WHERE nspname = 'public'
|
SELECT oid FROM pg_namespace WHERE nspname = 'public'
|
||||||
)
|
)
|
||||||
) LOOP
|
) LOOP
|
||||||
EXECUTE 'DROP TYPE IF EXISTS ' || quote_ident(r.typname) || ' CASCADE';
|
EXECUTE 'DROP TYPE IF EXISTS ' || quote_ident(r.typname) || ' CASCADE';
|
||||||
END LOOP;
|
END LOOP;
|
||||||
END $$;
|
END $$;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Drop the drizzle migrations tracking table
|
// Drop the drizzle migrations tracking table
|
||||||
await client`DROP TABLE IF EXISTS drizzle.__drizzle_migrations CASCADE`;
|
await client`DROP TABLE IF EXISTS drizzle.__drizzle_migrations CASCADE`;
|
||||||
await client`DROP SCHEMA IF EXISTS drizzle CASCADE`;
|
await client`DROP SCHEMA IF EXISTS drizzle CASCADE`;
|
||||||
|
|
||||||
console.log("✓ All tables and enums dropped\n");
|
console.log("✓ All tables and enums dropped\n");
|
||||||
|
|
||||||
console.log("Running migrations...");
|
await client.end();
|
||||||
await migrate(db, { migrationsFolder: MIGRATIONS_FOLDER });
|
|
||||||
console.log("✓ Migrations applied\n");
|
|
||||||
|
|
||||||
console.log("Seeding database...");
|
|
||||||
const profile = getProfile();
|
|
||||||
const cfg = profiles[profile];
|
|
||||||
await runSeedBody(client, db, profile, cfg);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`\n✓ Reset complete (advisory lock key=0x${SEED_ADVISORY_LOCK_KEY.toString(16)})`,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
await client.end();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reset().catch((err) => {
|
reset().catch((err) => {
|
||||||
|
|||||||
+176
-8
@@ -24,9 +24,9 @@ import type { MedicalAlert } from "@groombook/types";
|
|||||||
|
|
||||||
// ── Seed profile configuration ─────────────────────────────────────────────
|
// ── Seed profile configuration ─────────────────────────────────────────────
|
||||||
|
|
||||||
export type SeedProfile = "dev" | "uat" | "demo";
|
type SeedProfile = "dev" | "uat" | "demo";
|
||||||
|
|
||||||
export interface ProfileConfig {
|
interface ProfileConfig {
|
||||||
staffCount: { manager: number; receptionist: number; groomer: number; bather: number };
|
staffCount: { manager: number; receptionist: number; groomer: number; bather: number };
|
||||||
clientCount: number;
|
clientCount: number;
|
||||||
appointmentsBackDays: number;
|
appointmentsBackDays: number;
|
||||||
@@ -35,7 +35,7 @@ export interface ProfileConfig {
|
|||||||
includeUatClients: boolean;
|
includeUatClients: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const profiles: Record<SeedProfile, ProfileConfig> = {
|
const profiles: Record<SeedProfile, ProfileConfig> = {
|
||||||
dev: {
|
dev: {
|
||||||
staffCount: { manager: 1, receptionist: 1, groomer: 2, bather: 0 },
|
staffCount: { manager: 1, receptionist: 1, groomer: 2, bather: 0 },
|
||||||
clientCount: 100,
|
clientCount: 100,
|
||||||
@@ -70,8 +70,6 @@ function getProfile(): SeedProfile {
|
|||||||
return "uat";
|
return "uat";
|
||||||
}
|
}
|
||||||
|
|
||||||
export { getProfile };
|
|
||||||
|
|
||||||
// ── Deterministic PRNG (Mulberry32) ──────────────────────────────────────────
|
// ── Deterministic PRNG (Mulberry32) ──────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -832,6 +830,168 @@ async function seedUatGroomerLinkage(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── GRO-2311 / GRO-2313: portal customer StatusBadge coverage ────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GRO-2311 / GRO-2313: give the UAT portal customer (`uat-customer@groombook.dev`)
|
||||||
|
* a deterministic spread of appointments so the customer-portal StatusBadge
|
||||||
|
* palette can be LIVE-observed (not just code-verified against the bundle).
|
||||||
|
*
|
||||||
|
* Scope is the subset of badge states reachable from the `appointment_status`
|
||||||
|
* enum (`scheduled, confirmed, in_progress, completed, cancelled, no_show`) —
|
||||||
|
* the portal's <StatusBadge> renders `appointment.status` verbatim. `pending`
|
||||||
|
* and `waitlisted` are NOT valid appointment statuses and cannot be seeded; the
|
||||||
|
* styled `no_show`→`no-show` badge fix and any pending/waitlisted derivation are
|
||||||
|
* tracked separately in GRO-2319 (web). CTO-approved Option A on GRO-2313.
|
||||||
|
*
|
||||||
|
* - confirmed → future startTime → renders as an Upcoming card (Confirmed badge)
|
||||||
|
* - scheduled → future startTime → renders as an Upcoming card (Scheduled badge)
|
||||||
|
* - cancelled → past startTime → Past tab (isUpcoming excludes cancelled)
|
||||||
|
* - no_show → past startTime → Past tab (raw `no_show` label until GRO-2319)
|
||||||
|
*
|
||||||
|
* The existing GRO-2100 `completed` appointment (a0000001-…-0001) is left
|
||||||
|
* untouched (AC #4), so Completed is also covered.
|
||||||
|
*
|
||||||
|
* Idempotent: each appointment uses a fixed UUID and is upserted with
|
||||||
|
* onConflictDoNothing, so the hourly reset-demo-data CronJob (which TRUNCATEs
|
||||||
|
* then re-seeds) and non-truncating dev re-seeds never dup-key
|
||||||
|
* (see GRO-2033 for the dup-key class).
|
||||||
|
*/
|
||||||
|
async function seedUatCustomerPortalAppointments(
|
||||||
|
db: ReturnType<typeof drizzle>,
|
||||||
|
customerClientId: string | null,
|
||||||
|
): Promise<void> {
|
||||||
|
const LINKED_PET_ID = "c0000001-0000-0000-0000-000000000002"; // UAT Pup Alpha
|
||||||
|
|
||||||
|
// Skip silently outside the UAT persona profile (e.g. a dev/test seed that
|
||||||
|
// never created the UAT Customer client).
|
||||||
|
if (!customerClientId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The customer's pet must exist (pets are NOT truncated on reset, so this is
|
||||||
|
// stable). Defensive: bail cleanly if the persona pet is absent.
|
||||||
|
const [linkedPet] = await db
|
||||||
|
.select({ id: schema.pets.id })
|
||||||
|
.from(schema.pets)
|
||||||
|
.where(eq(schema.pets.id, LINKED_PET_ID))
|
||||||
|
.limit(1);
|
||||||
|
if (!linkedPet) {
|
||||||
|
console.warn(`⚠ GRO-2311: UAT Pup Alpha (${LINKED_PET_ID}) not found — skipping portal appointment seed`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stable "Bath & Brush" service; fall back to any active service.
|
||||||
|
const BATH_AND_BRUSH_ID = "b0000001-0000-0000-0000-000000000001";
|
||||||
|
const [bathService] = await db
|
||||||
|
.select({ id: schema.services.id })
|
||||||
|
.from(schema.services)
|
||||||
|
.where(eq(schema.services.id, BATH_AND_BRUSH_ID))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
let serviceId: string;
|
||||||
|
if (bathService) {
|
||||||
|
serviceId = bathService.id;
|
||||||
|
} else {
|
||||||
|
const [fallback] = await db
|
||||||
|
.select({ id: schema.services.id })
|
||||||
|
.from(schema.services)
|
||||||
|
.where(eq(schema.services.active, true))
|
||||||
|
.limit(1);
|
||||||
|
if (!fallback) {
|
||||||
|
console.warn(`⚠ GRO-2311: no active services found — skipping portal appointment seed`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
serviceId = fallback.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach the UAT groomer when present (nicer "with <groomer>" card); else null
|
||||||
|
// ("First Available"). Either way these are the customer's own appointments —
|
||||||
|
// no new groomer↔pet linkage invariant is created (uses the already-linked
|
||||||
|
// Pup Alpha), so GRO-1987 TC-UAT-3 (403 on the UNLINKED Pup Beta) is unaffected.
|
||||||
|
const [uatGroomerStaff] = await db
|
||||||
|
.select({ id: schema.staff.id })
|
||||||
|
.from(schema.staff)
|
||||||
|
.where(eq(schema.staff.email, "uat-groomer@groombook.dev"))
|
||||||
|
.limit(1);
|
||||||
|
const staffId = uatGroomerStaff?.id ?? null;
|
||||||
|
|
||||||
|
// Anchor all times to local wall-clock so future/past holds regardless of the
|
||||||
|
// hourly reset cadence.
|
||||||
|
const at = (deltaDays: number, hour: number): Date => {
|
||||||
|
const d = new Date();
|
||||||
|
d.setDate(d.getDate() + deltaDays);
|
||||||
|
d.setHours(hour, 0, 0, 0);
|
||||||
|
return d;
|
||||||
|
};
|
||||||
|
const DURATION_MS = 45 * 60 * 1000;
|
||||||
|
|
||||||
|
const rows = [
|
||||||
|
{
|
||||||
|
id: "a0000001-0000-0000-0000-000000000002",
|
||||||
|
status: "confirmed" as const,
|
||||||
|
start: at(3, 10),
|
||||||
|
confirmationStatus: "confirmed",
|
||||||
|
confirmedAt: new Date(),
|
||||||
|
cancelledAt: null as Date | null,
|
||||||
|
notes: "GRO-2311: upcoming confirmed appointment for portal StatusBadge coverage.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "a0000001-0000-0000-0000-000000000003",
|
||||||
|
status: "scheduled" as const,
|
||||||
|
start: at(5, 14),
|
||||||
|
confirmationStatus: "pending",
|
||||||
|
confirmedAt: null as Date | null,
|
||||||
|
cancelledAt: null as Date | null,
|
||||||
|
notes: "GRO-2311: upcoming scheduled appointment for portal StatusBadge coverage.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "a0000001-0000-0000-0000-000000000004",
|
||||||
|
status: "cancelled" as const,
|
||||||
|
start: at(-3, 11),
|
||||||
|
confirmationStatus: "cancelled",
|
||||||
|
confirmedAt: null as Date | null,
|
||||||
|
cancelledAt: new Date(),
|
||||||
|
notes: "GRO-2311: cancelled appointment (Past tab) for portal StatusBadge coverage.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "a0000001-0000-0000-0000-000000000005",
|
||||||
|
status: "no_show" as const,
|
||||||
|
start: at(-10, 9),
|
||||||
|
confirmationStatus: "confirmed",
|
||||||
|
confirmedAt: null as Date | null,
|
||||||
|
cancelledAt: null as Date | null,
|
||||||
|
notes: "GRO-2311: no_show appointment (Past tab) for portal StatusBadge coverage.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await db
|
||||||
|
.insert(schema.appointments)
|
||||||
|
.values(
|
||||||
|
rows.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
clientId: customerClientId,
|
||||||
|
petId: LINKED_PET_ID,
|
||||||
|
serviceId,
|
||||||
|
staffId,
|
||||||
|
batherStaffId: null,
|
||||||
|
status: r.status,
|
||||||
|
startTime: r.start,
|
||||||
|
endTime: new Date(r.start.getTime() + DURATION_MS),
|
||||||
|
notes: r.notes,
|
||||||
|
priceCents: null,
|
||||||
|
confirmationStatus: r.confirmationStatus,
|
||||||
|
confirmedAt: r.confirmedAt,
|
||||||
|
cancelledAt: r.cancelledAt,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.onConflictDoNothing({ target: schema.appointments.id });
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`✓ GRO-2311: seeded ${rows.length} portal StatusBadge appointments (confirmed/scheduled/cancelled/no_show) for UAT customer`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── GRO-2225: deterministic route-optimization cohort ────────────────────────
|
// ── GRO-2225: deterministic route-optimization cohort ────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1113,6 +1273,10 @@ async function seedKnownUsers() {
|
|||||||
// to attach to the appointment; on a fresh reset there are none yet at
|
// to attach to the appointment; on a fresh reset there are none yet at
|
||||||
// the time seedUatStaffAccounts() returns).
|
// the time seedUatStaffAccounts() returns).
|
||||||
await seedUatGroomerLinkage(db, uatCustomerClientId);
|
await seedUatGroomerLinkage(db, uatCustomerClientId);
|
||||||
|
// GRO-2311 / GRO-2313: portal customer StatusBadge palette coverage (reachable
|
||||||
|
// appointment statuses only). Runs after the groomer linkage so the customer
|
||||||
|
// client + Pup Alpha already exist.
|
||||||
|
await seedUatCustomerPortalAppointments(db, uatCustomerClientId);
|
||||||
|
|
||||||
// ── Client: Demo Client ──
|
// ── Client: Demo Client ──
|
||||||
const [existingClient] = await db
|
const [existingClient] = await db
|
||||||
@@ -1196,7 +1360,7 @@ async function seedKnownUsers() {
|
|||||||
// from runbooks without ambiguity and binds to the single-argument
|
// from runbooks without ambiguity and binds to the single-argument
|
||||||
// `pg_advisory_lock(int)` form, which postgres-js serializes as a plain
|
// `pg_advisory_lock(int)` form, which postgres-js serializes as a plain
|
||||||
// number (no bigint type plumbing required).
|
// number (no bigint type plumbing required).
|
||||||
export const SEED_ADVISORY_LOCK_KEY = 0x47524f4f; // "GROO" in ASCII — arbitrary, stable
|
const SEED_ADVISORY_LOCK_KEY = 0x47524f4f; // "GROO" in ASCII — arbitrary, stable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reserve a dedicated connection from `pool`, take the seed advisory lock
|
* Reserve a dedicated connection from `pool`, take the seed advisory lock
|
||||||
@@ -1209,7 +1373,7 @@ export const SEED_ADVISORY_LOCK_KEY = 0x47524f4f; // "GROO" in ASCII — arbitra
|
|||||||
* for the lock and release it from the same reserved connection. The
|
* for the lock and release it from the same reserved connection. The
|
||||||
* seed work itself still runs on the pooled connections.
|
* seed work itself still runs on the pooled connections.
|
||||||
*/
|
*/
|
||||||
export async function withSeedAdvisoryLock<T>(
|
async function withSeedAdvisoryLock<T>(
|
||||||
pool: ReturnType<typeof postgres>,
|
pool: ReturnType<typeof postgres>,
|
||||||
fn: () => Promise<T>,
|
fn: () => Promise<T>,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
@@ -1267,7 +1431,7 @@ async function seed() {
|
|||||||
await client.end();
|
await client.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runSeedBody(
|
async function runSeedBody(
|
||||||
client: ReturnType<typeof postgres>,
|
client: ReturnType<typeof postgres>,
|
||||||
db: ReturnType<typeof drizzle>,
|
db: ReturnType<typeof drizzle>,
|
||||||
profile: SeedProfile,
|
profile: SeedProfile,
|
||||||
@@ -1375,6 +1539,10 @@ export async function runSeedBody(
|
|||||||
// to attach to the appointment; on a fresh reset there are none yet at
|
// to attach to the appointment; on a fresh reset there are none yet at
|
||||||
// the time seedUatStaffAccounts() returns).
|
// the time seedUatStaffAccounts() returns).
|
||||||
await seedUatGroomerLinkage(db, uatCustomerClientId);
|
await seedUatGroomerLinkage(db, uatCustomerClientId);
|
||||||
|
// GRO-2311 / GRO-2313: portal customer StatusBadge palette coverage (reachable
|
||||||
|
// appointment statuses only). Runs after the groomer linkage so the customer
|
||||||
|
// client + Pup Alpha already exist.
|
||||||
|
await seedUatCustomerPortalAppointments(db, uatCustomerClientId);
|
||||||
|
|
||||||
// GRO-2225: deterministic pre-geocoded route cohort + fixed-date appointments
|
// GRO-2225: deterministic pre-geocoded route cohort + fixed-date appointments
|
||||||
// for the UAT groomer. Must run AFTER services are seeded (it looks up a
|
// for the UAT groomer. Must run AFTER services are seeded (it looks up a
|
||||||
|
|||||||
Reference in New Issue
Block a user