@authzkit/prisma-tenant-guard-generator
Version:
Generate tenant guard metadata from Prisma schemas
437 lines (433 loc) • 15.2 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/cli.ts
var cli_exports = {};
__export(cli_exports, {
run: () => run
});
module.exports = __toCommonJS(cli_exports);
var import_node_fs = require("fs");
var import_node_path2 = require("path");
var import_node_url = require("url");
var import_kleur = __toESM(require("kleur"), 1);
// src/index.ts
var import_promises = require("fs/promises");
var import_node_path = require("path");
var import_internals = require("@prisma/internals");
function normalizeModel(model) {
const serialized = JSON.stringify(model);
const parsed = JSON.parse(serialized);
return parsed;
}
async function generateTenantMeta(options) {
const schemaPath = (0, import_node_path.resolve)(options.schemaPath);
const datamodel = await (0, import_promises.readFile)(schemaPath, "utf8");
const dmmf = await (0, import_internals.getDMMF)({ datamodel });
const tenantFieldCandidates = (options.tenantField ?? "tenantId").split(",").map((field) => field.trim()).filter(Boolean);
if (tenantFieldCandidates.length === 0) {
tenantFieldCandidates.push("tenantId");
}
const includeSet = options.include ? new Set(options.include) : void 0;
const excludeSet = options.exclude ? new Set(options.exclude) : void 0;
const candidates = [];
for (const model of dmmf.datamodel.models) {
if (includeSet && !includeSet.has(model.name)) {
continue;
}
if (excludeSet && excludeSet.has(model.name)) {
continue;
}
const tenantField = model.fields.find(
(field) => field.kind === "scalar" && tenantFieldCandidates.includes(field.name)
);
if (!tenantField) {
continue;
}
const normalizedModel = normalizeModel(model);
const compositeSelector = detectCompositeSelector(normalizedModel, tenantField.name);
const relations = [];
for (const field of model.fields) {
if (field.kind !== "object") {
continue;
}
if (!field.type || typeof field.type !== "string") {
continue;
}
relations.push({ fieldName: field.name, targetModel: field.type });
}
const candidate = {
name: model.name,
tenantField: tenantField.name,
relations
};
if (compositeSelector) {
candidate.compositeSelector = compositeSelector;
}
candidates.push(candidate);
}
const candidateByName = new Map(
candidates.map((candidate) => [candidate.name, candidate])
);
const meta = {};
for (const candidate of candidates) {
const nestedTargets = {};
for (const relation of candidate.relations) {
if (!candidateByName.has(relation.targetModel)) {
continue;
}
if (relation.targetModel === candidate.name) {
nestedTargets[relation.fieldName] = relation.targetModel;
continue;
}
nestedTargets[relation.fieldName] = relation.targetModel;
}
meta[candidate.name] = {
tenantField: candidate.tenantField,
...candidate.compositeSelector ? { compositeSelector: candidate.compositeSelector } : {},
...Object.keys(nestedTargets).length > 0 ? { nestedTargets } : {}
};
}
const artifacts = {};
if (options.outputPath && options.emitTs !== false) {
artifacts.ts = await writeMetaFile(meta, options.outputPath);
}
if (options.emitJson) {
const inferredJsonPath = options.outputPath ? replaceExtension(options.outputPath, ".json") : void 0;
const jsonPath = (0, import_node_path.resolve)(
options.jsonOutputPath ?? inferredJsonPath ?? "tenant-guard.meta.json"
);
artifacts.json = await writeMetaJson(meta, jsonPath);
}
const result = { meta, artifacts };
const writtenTo = artifacts.ts ?? artifacts.json;
if (writtenTo) {
result.writtenTo = writtenTo;
}
return result;
}
function detectCompositeSelector(model, tenantField) {
for (const index of model.uniqueIndexes ?? []) {
if (index.fields.includes(tenantField) && index.fields.length > 1) {
return index.name ?? index.fields.join("_");
}
}
for (const uniqueFields of model.uniqueFields ?? []) {
if (uniqueFields.includes(tenantField) && uniqueFields.length > 1) {
return uniqueFields.join("_");
}
}
const primaryFields = model.primaryKey?.fields ?? [];
if (primaryFields.includes(tenantField) && primaryFields.length > 1) {
return (model.primaryKey?.name ?? primaryFields.join("_")) || void 0;
}
return void 0;
}
async function writeMetaFile(meta, outputPath) {
const filePath = (0, import_node_path.resolve)(outputPath);
const outputDir = (0, import_node_path.dirname)(filePath);
await (0, import_promises.mkdir)(outputDir, { recursive: true });
const contents = formatMeta(meta);
await (0, import_promises.writeFile)(filePath, contents, "utf8");
return filePath;
}
async function writeMetaJson(meta, outputPath) {
const filePath = (0, import_node_path.resolve)(outputPath);
const outputDir = (0, import_node_path.dirname)(filePath);
await (0, import_promises.mkdir)(outputDir, { recursive: true });
await (0, import_promises.writeFile)(filePath, JSON.stringify(meta, null, 2) + "\n", "utf8");
return filePath;
}
function formatMeta(meta) {
const lines = [];
lines.push("import type { TenantMeta } from '@authzkit/prisma-tenant-guard';");
lines.push("");
lines.push(
"export const tenantMeta = " + serializeMeta(meta) + " satisfies TenantMeta;"
);
lines.push("");
lines.push("export default tenantMeta;");
lines.push("");
return lines.join("\n");
}
function serializeMeta(meta) {
const entries = Object.entries(meta).sort(([a], [b]) => a.localeCompare(b));
if (entries.length === 0) {
return "{}";
}
const indent = (level) => " ".repeat(level);
const lines = ["{"];
for (const [model, config] of entries) {
const modelLines = [];
if (config.tenantField) {
modelLines.push(`${indent(2)}tenantField: '${config.tenantField}',`);
}
if (config.compositeSelector) {
modelLines.push(`${indent(2)}compositeSelector: '${config.compositeSelector}',`);
}
if (config.nestedTargets && Object.keys(config.nestedTargets).length > 0) {
modelLines.push(`${indent(2)}nestedTargets: {`);
const nestedEntries = Object.entries(config.nestedTargets).sort(
([a], [b]) => a.localeCompare(b)
);
for (const [field, target] of nestedEntries) {
if (typeof target === "string") {
modelLines.push(`${indent(3)}${field}: '${target}',`);
continue;
}
const targetLines = [`${indent(3)}${field}: {`];
const entries2 = Object.entries(target).sort(([a], [b]) => a.localeCompare(b));
for (const [key, value] of entries2) {
targetLines.push(`${indent(4)}${key}: '${value}',`);
}
targetLines.push(`${indent(3)}}`);
modelLines.push(...targetLines);
}
modelLines.push(`${indent(2)}},`);
}
if (modelLines.length === 0) {
lines.push(`${indent(1)}${model}: {},`);
continue;
}
lines.push(`${indent(1)}${model}: {`);
lines.push(...modelLines);
lines.push(`${indent(1)}},`);
}
lines.push("}");
return lines.join("\n");
}
function replaceExtension(path, nextExtension) {
const resolved = (0, import_node_path.resolve)(path);
const lastDot = resolved.lastIndexOf(".");
if (lastDot === -1) {
return `${resolved}${nextExtension}`;
}
return `${resolved.slice(0, lastDot)}${nextExtension}`;
}
// src/cli.ts
var import_meta = {};
async function run(argv = process.argv.slice(2)) {
const parsed = parseArgs(argv);
if (parsed.flags.help) {
printHelp();
return { status: "ok" };
}
const cwd = process.cwd();
const schemaPath = (0, import_node_path2.resolve)(cwd, parsed.flags.schema ?? defaultSchemaPath(cwd));
const schemaDir = (0, import_node_path2.dirname)(schemaPath);
const defaultOutput = (0, import_node_path2.resolve)(schemaDir, "..", ".prisma", "tenant-guard", "meta.ts");
const outputPath = (0, import_node_path2.resolve)(cwd, parsed.flags.out ?? defaultOutput);
const include = parsed.flags.include ?? [];
const exclude = parsed.flags.exclude ?? [];
try {
const generationOptions = { schemaPath };
if (parsed.flags.tenantField) {
generationOptions.tenantField = parsed.flags.tenantField;
}
if (include.length > 0) {
generationOptions.include = include;
}
if (exclude.length > 0) {
generationOptions.exclude = exclude;
}
const emitTs = !parsed.flags.print && !parsed.flags.jsonOnly;
if (emitTs) {
generationOptions.outputPath = outputPath;
} else if (parsed.flags.jsonOnly) {
generationOptions.emitTs = false;
}
const shouldEmitJson = !parsed.flags.print && !parsed.flags.tsOnly;
if (shouldEmitJson) {
generationOptions.emitJson = true;
if (parsed.flags.jsonOnly) {
generationOptions.jsonOutputPath = (0, import_node_path2.resolve)(
cwd,
parsed.flags.out ?? defaultOutput.replace(/\.ts$/, ".json")
);
}
}
const result = await generateTenantMeta(generationOptions);
if (parsed.flags.print) {
process.stdout.write(formatMeta(result.meta));
return { status: "ok" };
}
const models = Object.keys(result.meta);
const artifactParts = [];
if (result.artifacts?.ts) {
artifactParts.push(`${import_kleur.default.cyan(result.artifacts.ts)} (.ts)`);
}
if (result.artifacts?.json) {
artifactParts.push(`${import_kleur.default.magenta(result.artifacts.json)} (.json)`);
}
const artifactsSummary = artifactParts.length > 0 ? artifactParts.join(", ") : "stdout";
console.log(
import_kleur.default.green("\u2714"),
`Generated tenant guard meta for ${import_kleur.default.bold(String(models.length))} models -> ${artifactsSummary}`
);
return { status: "ok", models: result.meta };
} catch (error) {
const details = error instanceof Error ? error.message : String(error);
console.error(import_kleur.default.red("\u2716"), "Failed to generate tenant guard meta");
console.error(import_kleur.default.gray(details));
return { status: "error", error };
}
}
function defaultSchemaPath(cwd) {
return (0, import_node_path2.resolve)(cwd, "prisma/schema.prisma");
}
function parseArgs(argv) {
const flags = {};
const pending = {};
for (const token of argv) {
if (token.startsWith("--")) {
const [rawKey, rawValue] = token.includes("=") ? token.slice(2).split("=", 2) : [token.slice(2), void 0];
const key = rawKey.trim();
if (key === "help") {
flags.help = true;
continue;
}
if (key === "print") {
flags.print = true;
continue;
}
if (key === "ts-only") {
flags.tsOnly = true;
continue;
}
if (key === "json-only") {
flags.jsonOnly = true;
continue;
}
if (rawValue !== void 0) {
setFlag(flags, key, rawValue);
continue;
}
pending[key] = true;
continue;
}
if (token.startsWith("-")) {
const key = token.slice(1);
if (key === "h") {
flags.help = true;
continue;
}
pending[key] = true;
continue;
}
const pendingKey = Object.keys(pending)[0];
if (pendingKey) {
setFlag(flags, pendingKey, token);
delete pending[pendingKey];
}
}
return { flags };
}
function setFlag(flags, key, value) {
switch (key) {
case "schema":
case "s":
flags.schema = value;
break;
case "out":
case "output":
case "o":
flags.out = value;
break;
case "tenant-field":
case "tenantField":
case "t":
flags.tenantField = value;
break;
case "include":
case "includes":
case "i":
flags.include = [
...flags.include ?? [],
...value.split(",").map((item) => item.trim()).filter(Boolean)
];
break;
case "exclude":
case "excludes":
case "e":
flags.exclude = [
...flags.exclude ?? [],
...value.split(",").map((item) => item.trim()).filter(Boolean)
];
break;
default:
break;
}
}
function printHelp() {
const banner = import_kleur.default.bold("AuthzKit Prisma Tenant Guard Generator");
const usage = import_kleur.default.cyan("authzkit-tenant-guard-gen [options]");
const lines = [
banner,
"",
`Usage: ${usage}`,
"",
"Options:",
" --schema <path> Path to schema.prisma (default: prisma/schema.prisma)",
" --out <path> Output file (default: ../.prisma/tenant-guard/meta.ts)",
" --tenant-field <name> Tenant field to detect (default: tenantId)",
" --include <Model[,..]> Only emit metadata for specific models",
" --exclude <Model[,..]> Skip metadata for specific models",
" --print Print generated meta to stdout instead of writing to disk",
" --ts-only Emit only the TypeScript artifact",
" --json-only Emit only the JSON artifact",
" --help Show this help message",
""
];
console.log(lines.join("\n"));
}
if (isMainModule()) {
void run().then((result) => {
if (result.status === "error") {
process.exitCode = 1;
}
});
}
function isMainModule() {
const entry = process.argv[1];
if (!entry) {
return false;
}
try {
const resolvedEntry = (0, import_node_fs.realpathSync)(entry);
const modulePath = (0, import_node_fs.realpathSync)((0, import_node_url.fileURLToPath)(import_meta.url));
return resolvedEntry === modulePath;
} catch {
const resolvedModule = (0, import_node_url.fileURLToPath)(import_meta.url);
return entry === resolvedModule;
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
run
});