/** * Idempotent seed. Safe to run on every environment, including production. * * It reconciles the *code-owned* parts of the schema — the permission catalog * and the system roles — with the database. It deliberately does NOT create * users: account provisioning belongs to the auth milestone, and baking a * default admin password into a repository is how stores get compromised. */ import { PrismaClient } from '@prisma/client'; import { ALL_PERMISSIONS, PERMISSIONS, SYSTEM_ROLES, type Permission } from '@sport/types'; import { seedCatalog } from './seed/catalog'; const prisma = new PrismaClient(); /** Which permissions each system role starts with. Editable later at runtime. */ const ROLE_DEFINITIONS: Record = { [SYSTEM_ROLES.SUPER_ADMIN]: { name: 'Super Admin', permissions: ALL_PERMISSIONS, }, [SYSTEM_ROLES.ADMIN]: { name: 'Admin', permissions: ALL_PERMISSIONS.filter( (permission) => permission !== PERMISSIONS.ROLE_MANAGE && permission !== PERMISSIONS.USER_MANAGE, ), }, [SYSTEM_ROLES.CATALOG_MANAGER]: { name: 'Catalog Manager', permissions: [ PERMISSIONS.PRODUCT_READ, PERMISSIONS.PRODUCT_CREATE, PERMISSIONS.PRODUCT_UPDATE, PERMISSIONS.PRODUCT_PUBLISH, PERMISSIONS.CATEGORY_READ, PERMISSIONS.CATEGORY_MANAGE, PERMISSIONS.COLLECTION_READ, PERMISSIONS.COLLECTION_MANAGE, PERMISSIONS.BRAND_READ, PERMISSIONS.BRAND_MANAGE, PERMISSIONS.INVENTORY_READ, PERMISSIONS.INVENTORY_UPDATE, PERMISSIONS.MEDIA_READ, PERMISSIONS.MEDIA_UPLOAD, ], }, [SYSTEM_ROLES.ORDER_MANAGER]: { name: 'Order Manager', permissions: [ PERMISSIONS.ORDER_READ, PERMISSIONS.ORDER_UPDATE, PERMISSIONS.ORDER_CANCEL, PERMISSIONS.PAYMENT_READ, PERMISSIONS.CUSTOMER_READ, PERMISSIONS.INVENTORY_READ, PERMISSIONS.PRODUCT_READ, ], }, [SYSTEM_ROLES.SUPPORT_AGENT]: { name: 'Support Agent', permissions: [ PERMISSIONS.ORDER_READ, PERMISSIONS.CUSTOMER_READ, PERMISSIONS.PRODUCT_READ, PERMISSIONS.REVIEW_MODERATE, ], }, [SYSTEM_ROLES.CUSTOMER]: { name: 'Customer', permissions: [], }, }; async function seedPermissions(): Promise> { for (const key of ALL_PERMISSIONS) { const [resource = key, action = 'unknown'] = key.split('.'); await prisma.permission.upsert({ where: { key }, update: { resource, action }, create: { key, resource, action }, }); } // Anything in the table but no longer in the catalog is dead configuration. const removed = await prisma.permission.deleteMany({ where: { key: { notIn: [...ALL_PERMISSIONS] } }, }); if (removed.count > 0) { console.log(`Removed ${removed.count} stale permission(s).`); } const rows = await prisma.permission.findMany({ select: { id: true, key: true } }); return new Map(rows.map((row) => [row.key, row.id])); } async function seedRoles(permissionIds: Map): Promise { for (const [key, definition] of Object.entries(ROLE_DEFINITIONS)) { const role = await prisma.role.upsert({ where: { key }, update: { name: definition.name, isSystem: true }, create: { key, name: definition.name, isSystem: true }, }); // Replace the grant set wholesale — the code definition wins for system roles. await prisma.rolePermission.deleteMany({ where: { roleId: role.id } }); const grants = definition.permissions .map((permission) => permissionIds.get(permission)) .filter((id): id is string => Boolean(id)) .map((permissionId) => ({ roleId: role.id, permissionId })); if (grants.length > 0) { await prisma.rolePermission.createMany({ data: grants, skipDuplicates: true }); } } } async function seedInventoryLocation(): Promise { await prisma.inventoryLocation.upsert({ where: { code: 'MAIN' }, update: {}, create: { code: 'MAIN', name: 'Main Warehouse', isDefault: true }, }); } async function main(): Promise { const permissionIds = await seedPermissions(); await seedRoles(permissionIds); await seedInventoryLocation(); console.log( `Seed complete: ${permissionIds.size} permissions, ${Object.keys(ROLE_DEFINITIONS).length} roles.`, ); // Demo catalog: development and staging only. Guarded twice — by NODE_ENV and // by an explicit opt-out — because sample products appearing in a production // storefront is the kind of mistake that reaches customers. const isProduction = process.env['NODE_ENV'] === 'production'; const demoDisabled = process.env['SEED_DEMO'] === 'false'; if (isProduction || demoDisabled) { console.log('Skipping demo catalog seed.'); return; } await seedCatalog(prisma); } main() .catch((error: unknown) => { console.error(error); process.exitCode = 1; }) .finally(() => { void prisma.$disconnect(); });