Basic Architecture of Sport Web
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* 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 { ALL_PERMISSIONS, PERMISSIONS, SYSTEM_ROLES, type Permission } from '@sport/types';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
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.`,
|
||||
);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((error: unknown) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
})
|
||||
.finally(() => {
|
||||
void prisma.$disconnect();
|
||||
});
|
||||
Reference in New Issue
Block a user