feat(seed): rewrite seed for multi-cardinality refactor

Split seed into orchestrator (seed.ts) + per-port fixture builder
(seed-data.ts). Creates three ports (Port Nimara, Marina Azzurra,
Harbor Royale) and seeds each with a realistic multi-cardinality
dataset: 12 berths (5 available / 5 reserved / 2 sold), 8 clients
with contacts and primary addresses, 3 companies (2 active / 1
dissolved) with billing addresses, memberships exercising dual-
company ownership and ended state, 12 yachts (7 client-owned /
5 company-owned) plus matching open ownership-history rows, 3
completed ownership transfers per port (client <-> company), 15
interests spanning all pipeline stages, and 8 reservations (5
active on distinct berths / 2 ended / 1 cancelled). Seed wraps
per-port work in withTransaction and is idempotent: re-running
detects existing company rows and skips.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-04-24 13:26:37 +02:00
parent 7abbdd4913
commit 727e323288
2 changed files with 1212 additions and 29 deletions

1105
src/lib/db/seed-data.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,29 @@
/** /**
* Seed script for Port Nimara CRM. * Seed script for Port Nimara CRM.
* *
* Seeds: * Top-level orchestrator:
* - 1 Port: Port Nimara * 1. Create 3 ports (idempotent):
* - 5 System roles with full permission maps * - Port Nimara
* - 1 Super admin user profile (matt@portnimara.com) * - Marina Azzurra
* - Harbor Royale
* 2. Create 5 system roles with full permission maps
* 3. Create the super admin user profile placeholder (matt@portnimara.com)
* 4. For each port, call `seedPortData(portId, portSlug)` from seed-data.ts
* to produce the realistic multi-cardinality fixture
* (berths, clients, companies, yachts, memberships, interests,
* reservations, ownership-transfer history).
* 5. Print a summary.
* *
* Run with: npm run db:seed * Run with: pnpm db:seed
*/ */
import 'dotenv/config'; import 'dotenv/config';
import { eq } from 'drizzle-orm';
import { db } from './index'; import { db } from './index';
import { ports } from './schema/ports'; import { ports } from './schema/ports';
import { roles, userProfiles } from './schema/users'; import { roles, userProfiles } from './schema/users';
import type { RolePermissions } from './schema/users'; import type { RolePermissions } from './schema/users';
import { seedPortData, type SeedSummary } from './seed-data';
// ─── Permission Maps ───────────────────────────────────────────────────────── // ─── Permission Maps ─────────────────────────────────────────────────────────
@@ -347,34 +357,77 @@ const VIEWER_PERMISSIONS: RolePermissions = {
}, },
}; };
// ─── Port Definitions ────────────────────────────────────────────────────────
const PORT_DEFINITIONS: Array<{
name: string;
slug: string;
primaryColor: string;
defaultCurrency: string;
timezone: string;
}> = [
{
name: 'Port Nimara',
slug: 'port-nimara',
primaryColor: '#0F4C81',
defaultCurrency: 'USD',
timezone: 'America/Anguilla',
},
{
name: 'Marina Azzurra',
slug: 'marina-azzurra',
primaryColor: '#2E86AB',
defaultCurrency: 'EUR',
timezone: 'Europe/Rome',
},
{
name: 'Harbor Royale',
slug: 'harbor-royale',
primaryColor: '#8B1E3F',
defaultCurrency: 'GBP',
timezone: 'Europe/London',
},
];
// ─── Seed Function ──────────────────────────────────────────────────────────── // ─── Seed Function ────────────────────────────────────────────────────────────
async function seed() { async function seed() {
console.log('Seeding Port Nimara CRM...'); console.log('Seeding Port Nimara CRM...');
// ── 1. Port ──────────────────────────────────────────────────────────────── // ── 1. Ports ────────────────────────────────────────────────────────────────
console.log('Creating Port Nimara...'); console.log('Creating ports...');
const [port] = await db const portIds: Array<{ id: string; name: string; slug: string }> = [];
for (const def of PORT_DEFINITIONS) {
const [inserted] = await db
.insert(ports) .insert(ports)
.values({ .values({
id: crypto.randomUUID(), id: crypto.randomUUID(),
name: 'Port Nimara', name: def.name,
slug: 'port-nimara', slug: def.slug,
logoUrl: null, logoUrl: null,
primaryColor: '#0F4C81', primaryColor: def.primaryColor,
defaultCurrency: 'USD', defaultCurrency: def.defaultCurrency,
timezone: 'America/Anguilla', timezone: def.timezone,
settings: {}, settings: {},
isActive: true, isActive: true,
}) })
.onConflictDoNothing() .onConflictDoNothing()
.returning(); .returning();
const portId = port?.id; if (inserted) {
if (!portId) { console.log(` Port created: ${def.name} (${inserted.id})`);
console.log('Port already exists, skipping...'); portIds.push({ id: inserted.id, name: def.name, slug: def.slug });
} else { } else {
console.log(`Port created: ${portId}`); // Port already existed — look it up so we can still seed fixtures for it.
const [existing] = await db.select().from(ports).where(eq(ports.slug, def.slug)).limit(1);
if (existing) {
console.log(` Port exists: ${def.name} (${existing.id})`);
portIds.push({ id: existing.id, name: def.name, slug: def.slug });
} else {
console.warn(` Port insert conflict but lookup returned no row: ${def.slug}`);
}
}
} }
// ── 2. System Roles ───────────────────────────────────────────────────────── // ── 2. System Roles ─────────────────────────────────────────────────────────
@@ -426,7 +479,7 @@ async function seed() {
for (const role of systemRoles) { for (const role of systemRoles) {
await db.insert(roles).values(role).onConflictDoNothing(); await db.insert(roles).values(role).onConflictDoNothing();
console.log(`Role created: ${role.name}`); console.log(` Role: ${role.name}`);
} }
// ── 3. Super Admin User Profile ───────────────────────────────────────────── // ── 3. Super Admin User Profile ─────────────────────────────────────────────
@@ -453,7 +506,32 @@ async function seed() {
}) })
.onConflictDoNothing(); .onConflictDoNothing();
console.log(`Super admin profile created for user_id: ${superAdminUserId}`); console.log(` Super admin profile for user_id: ${superAdminUserId}`);
// ── 4. Per-port fixtures ────────────────────────────────────────────────────
console.log('');
console.log('Seeding per-port fixtures...');
const summaries: Array<{ name: string; summary: SeedSummary | null }> = [];
for (const p of portIds) {
console.log(` [${p.slug}] seeding fixture data...`);
const summary = await seedPortData(p.id, p.slug);
summaries.push({ name: p.name, summary });
}
// ── 5. Summary ─────────────────────────────────────────────────────────────
console.log('');
console.log('─── Summary ───────────────────────────────────────────────');
for (const s of summaries) {
if (s.summary === null) {
console.log(` ✓ Port "${s.name}" — already seeded (skipped)`);
} else {
const x = s.summary;
console.log(
` ✓ Port "${s.name}" — ${x.berths} berths, ${x.clients} clients, ${x.companies} companies, ${x.yachts} yachts, ${x.interests} interests, ${x.reservations} reservations`,
);
}
}
console.log(''); console.log('');
console.log('Seed complete!'); console.log('Seed complete!');
console.log(''); console.log('');