Files
edr-platform/scripts/seed-module-permissions.cjs
2026-08-25 00:11:39 +03:00

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();