Files
web_sport/apps/api/prisma/seed.ts
T
2026-08-13 23:20:22 +07:00

171 lines
5.5 KiB
TypeScript

/**
* 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 { seedDevAccounts } from './seed/accounts';
import { seedCatalog } from './seed/catalog';
const prisma = new PrismaClient();
/** Which permissions each system role starts with. Editable later at runtime. */
const ROLE_DEFINITIONS: Record<string, { name: string; permissions: readonly Permission[] }> = {
[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<Map<string, string>> {
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<string, string>): Promise<void> {
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<void> {
await prisma.inventoryLocation.upsert({
where: { code: 'MAIN' },
update: {},
create: { code: 'MAIN', name: 'Main Warehouse', isDefault: true },
});
}
async function main(): Promise<void> {
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);
const accounts = await seedDevAccounts(prisma);
const created = accounts.filter((account) => account.created);
if (created.length > 0) {
console.log('\nDevelopment sign-in accounts (shown once):\n');
for (const account of created) {
console.log(` ${account.email.padEnd(24)} ${account.password} [${account.role}]`);
}
console.log('\n Development only. Use `pnpm db:create-admin` for real environments.\n');
} else {
console.log('Development accounts already exist; passwords left unchanged.');
}
}
main()
.catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
})
.finally(() => {
void prisma.$disconnect();
});