mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
176 lines
6.1 KiB
JavaScript
176 lines
6.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Scoped, idempotent seeder for one module's IAM application + permissions +
|
|
* roles + role_permissions.
|
|
*
|
|
* WHY THIS EXISTS rather than `pnpm run seed:hr`:
|
|
* The vendor seeder (`@tria-plc/iamapi-common`'s seed.cli.js) re-seeds IAM's
|
|
* ENTIRE baseline, and aborts the whole transaction when a baseline row it
|
|
* wants to write already exists under a different id — which is the case on
|
|
* every database restored from a dump taken at a different package version:
|
|
*
|
|
* Role seed conflicts with existing data, refusing to write:
|
|
* "people_operation" is id 9f9c0c2e-… in the database but the seed assigns
|
|
* it id 7bbf4e76-… — this would orphan every FK pointing at the old id
|
|
*
|
|
* That refusal is correct — it protects rows another product owns. But it also
|
|
* means HR's and Finance's own rows can never be written on those databases.
|
|
* This script writes ONLY the module's own rows, by the stable ids the module's
|
|
* registry declares, and touches nothing it does not own.
|
|
*
|
|
* Usage (from the repo root):
|
|
* DB_NAME=smart_office_e2e node scripts/seed-module-permissions.cjs hr
|
|
* DB_NAME=smart_office_e2e node scripts/seed-module-permissions.cjs finance
|
|
*
|
|
* Env: DB_HOST DB_PORT DB_USER DB_PASSWORD DB_NAME (same convention as the apps).
|
|
* Re-running is a no-op — every insert is ON CONFLICT DO NOTHING.
|
|
*/
|
|
const path = require("path");
|
|
|
|
const MODULES = {
|
|
hr: {
|
|
dist: "apps/edr-hr-api/dist/seed/hr-permissions.registry.js",
|
|
application: "HR_APPLICATION",
|
|
permissions: "HR_PERMISSIONS",
|
|
roles: "HR_ROLES",
|
|
rolePermissions: "HR_ROLE_PERMISSIONS",
|
|
// `pg` resolves from the app that owns the registry.
|
|
pgFrom: "apps/edr-hr-api",
|
|
buildFilter: "@edr/hr-api",
|
|
},
|
|
finance: {
|
|
dist: "apps/finance-api/dist/seed/finance-permissions.registry.js",
|
|
application: "FINANCE_APPLICATION",
|
|
permissions: "FINANCE_PERMISSIONS",
|
|
roles: "FINANCE_ROLES",
|
|
rolePermissions: "FINANCE_ROLE_PERMISSIONS",
|
|
pgFrom: "apps/finance-api",
|
|
buildFilter: "@edr/finance-api",
|
|
},
|
|
};
|
|
|
|
async function main() {
|
|
const moduleName = process.argv[2];
|
|
const spec = MODULES[moduleName];
|
|
if (!spec) {
|
|
console.error(
|
|
`Usage: DB_NAME=<db> node scripts/seed-module-permissions.cjs <${Object.keys(MODULES).join("|")}>`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const root = path.resolve(__dirname, "..");
|
|
const registryPath = path.join(root, spec.dist);
|
|
let registry;
|
|
try {
|
|
registry = require(registryPath);
|
|
} catch (err) {
|
|
console.error(
|
|
`Cannot load ${spec.dist}. Build the app first:\n` +
|
|
` pnpm turbo build --filter=${spec.buildFilter}\n${err.message}`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const { Client } = require(
|
|
require.resolve("pg", { paths: [path.join(root, spec.pgFrom)] }),
|
|
);
|
|
|
|
const application = registry[spec.application];
|
|
const permissions = registry[spec.permissions];
|
|
const roles = registry[spec.roles];
|
|
const rolePermissions = registry[spec.rolePermissions];
|
|
|
|
const dbName = process.env.DB_NAME;
|
|
if (!dbName) {
|
|
console.error("DB_NAME is required — refusing to guess which database to write to.");
|
|
process.exit(1);
|
|
}
|
|
|
|
const client = new Client({
|
|
host: process.env.DB_HOST || "localhost",
|
|
port: Number(process.env.DB_PORT || 5432),
|
|
user: process.env.DB_USER || "postgres",
|
|
password: process.env.DB_PASSWORD,
|
|
database: dbName,
|
|
});
|
|
await client.connect();
|
|
console.log(`[${moduleName}] → ${dbName}`);
|
|
|
|
try {
|
|
await client.query("BEGIN");
|
|
|
|
await client.query(
|
|
`INSERT INTO iam.application (id, key, name) VALUES ($1, $2, $3)
|
|
ON CONFLICT (id) DO NOTHING`,
|
|
[application.id, application.key, JSON.stringify(application.name)],
|
|
);
|
|
|
|
// Guard: a key collision against a row owned by something else means the
|
|
// registry's stable id and the database disagree. Writing would either fail
|
|
// on the unique index or silently do nothing while the module keeps using
|
|
// its own id — both worse than stopping. This has fired for real: a
|
|
// hand-made `e2e_hr_employee` role in the e2e database had duplicated five
|
|
// HR keys under ids the registry did not know.
|
|
const keys = permissions.map((p) => p.key);
|
|
const { rows: clashes } = await client.query(
|
|
`SELECT id, key FROM iam.permissions WHERE key = ANY($1::text[]) AND id <> ALL($2::uuid[])`,
|
|
[keys, permissions.map((p) => p.id)],
|
|
);
|
|
if (clashes.length) {
|
|
throw new Error(
|
|
`Permission key(s) already exist under a different id — refusing to write:\n` +
|
|
clashes.map((c) => ` ${c.key} is ${c.id}`).join("\n"),
|
|
);
|
|
}
|
|
|
|
for (const p of permissions) {
|
|
await client.query(
|
|
`INSERT INTO iam.permissions (id, key, name, application_id) VALUES ($1, $2, $3, $4)
|
|
ON CONFLICT (id) DO NOTHING`,
|
|
[p.id, p.key, JSON.stringify(p.name), application.id],
|
|
);
|
|
}
|
|
|
|
for (const r of roles) {
|
|
await client.query(
|
|
`INSERT INTO iam.roles (id, key, name) VALUES ($1, $2, $3)
|
|
ON CONFLICT (id) DO NOTHING`,
|
|
[r.id, r.key, JSON.stringify(r.name)],
|
|
);
|
|
}
|
|
|
|
const permIdByKey = new Map(permissions.map((p) => [p.key, p.id]));
|
|
const roleIdByKey = new Map(roles.map((r) => [r.key, r.id]));
|
|
let grants = 0;
|
|
for (const grant of rolePermissions) {
|
|
const roleId = roleIdByKey.get(grant.roleKey);
|
|
if (!roleId) throw new Error(`Unknown roleKey ${grant.roleKey}`);
|
|
for (const permKey of grant.permissionKeys) {
|
|
const permId = permIdByKey.get(permKey);
|
|
if (!permId) throw new Error(`Unknown permission key ${permKey}`);
|
|
await client.query(
|
|
`INSERT INTO iam.role_permissions (role_id, permission_id) VALUES ($1, $2)
|
|
ON CONFLICT DO NOTHING`,
|
|
[roleId, permId],
|
|
);
|
|
grants++;
|
|
}
|
|
}
|
|
|
|
await client.query("COMMIT");
|
|
console.log(
|
|
` ✓ application "${application.key}" · ${permissions.length} permissions · ` +
|
|
`${roles.length} roles · ${grants} grants`,
|
|
);
|
|
} catch (err) {
|
|
await client.query("ROLLBACK");
|
|
console.error(` ✗ rolled back: ${err.message}`);
|
|
process.exitCode = 1;
|
|
} finally {
|
|
await client.end();
|
|
}
|
|
}
|
|
|
|
main();
|