157 lines
4.9 KiB
TypeScript
157 lines
4.9 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 { 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);
|
|
}
|
|
|
|
main()
|
|
.catch((error: unknown) => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
})
|
|
.finally(() => {
|
|
void prisma.$disconnect();
|
|
});
|