@authzkit/prisma-tenant-guard-generator
Version:
Generate tenant guard metadata from Prisma schemas
228 lines (226 loc) • 8.3 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
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 __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
formatMeta: () => formatMeta,
generateTenantMeta: () => generateTenantMeta,
writeMetaFile: () => writeMetaFile,
writeMetaJson: () => writeMetaJson
});
module.exports = __toCommonJS(index_exports);
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}`;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
formatMeta,
generateTenantMeta,
writeMetaFile,
writeMetaJson
});