@hexys/prismix
Version:
Create multiple Prisma schema files with shared model relations.
416 lines (410 loc) • 16.8 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
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(
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __async = (__this, __arguments, generator) => {
return new Promise((resolve, reject) => {
var fulfilled = (value) => {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
};
var rejected = (value) => {
try {
step(generator.throw(value));
} catch (e) {
reject(e);
}
};
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
step((generator = generator.apply(__this, __arguments)).next());
});
};
// lib/index.ts
var import_command = require("@oclif/command");
// lib/prismix.ts
var import_fs = __toESM(require("fs"));
var import_util = require("util");
var import_path = __toESM(require("path"));
var import_internals = require("@prisma/internals");
// lib/utils.ts
function valueIs(value, types) {
return types.map((type) => type.name.toLowerCase() == typeof value).includes(true);
}
// lib/deserializer.ts
var renderAttribute = (field) => {
const { kind, type } = field;
return {
default: (value) => {
if (value == null || value == void 0)
return "";
if (kind === "scalar" && type !== "BigInt" && typeof value == "string")
value = `"${value}"`;
if (valueIs(value, [Number, String, Boolean]) || kind === "enum")
return `@default(${value})`;
if (typeof value === "object") {
if (value.name === "dbgenerated")
return `@default(${value.name}("${value.args}"))`;
return `@default(${value.name}(${value.args}))`;
}
throw new Error(`Prismix: Unsupported field attribute ${value}`);
},
isId: (value) => value ? "@id" : "",
isUnique: (value) => value ? "@unique" : "",
isUpdatedAt: (value) => value ? "@updatedAt" : "",
columnName: (value) => value ? `@map("${value}")` : "",
dbType: (value) => value != null ? value : ""
};
};
function renderAttributes(field) {
const {
relationFromFields,
relationToFields,
relationName,
kind,
relationOnDelete,
relationOnUpdate
} = field;
if (kind == "scalar" || kind == "enum") {
return `${Object.keys(field).map(
(property) => renderAttribute(field)[property] && renderAttribute(field)[property](field[property])
).filter((x) => !!x).join(" ")}`;
}
if (relationFromFields && kind === "object") {
return relationFromFields.length > 0 ? `@relation(name: "${relationName}", fields: [${relationFromFields}], references: [${relationToFields}]${relationOnDelete ? `, onDelete: ${relationOnDelete}` : ""}${relationOnUpdate ? `, onUpdate: ${relationOnUpdate}` : ""})` : `@relation(name: "${relationName}")`;
}
return "";
}
function renderDocumentation(documentation, tab) {
if (!documentation)
return "";
const documentationLines = documentation.split("\n");
return documentationLines.length == 1 ? `/// ${documentationLines[0]}
${tab ? " " : ""}` : documentationLines.map((text, idx) => idx == 0 ? `/// ${text}` : ` /// ${text}`).join("\n") + (tab ? "\n " : "\n");
}
function renderModelFields(fields) {
return fields.map((field) => {
const { name, kind, type, documentation, isRequired, isList } = field;
if (kind == "scalar")
return `${renderDocumentation(documentation, true)}${name} ${type}${isList ? "[]" : isRequired ? "" : "?"} ${renderAttributes(field)}`;
if (kind == "object" || kind == "enum")
return `${renderDocumentation(documentation, true)}${name} ${type}${isList ? "[]" : isRequired ? "" : "?"} ${renderAttributes(field)}`;
throw new Error(`Prismix: Unsupported field kind "${kind}"`);
});
}
function renderIdFieldsOrPrimaryKey(idFields) {
if (!idFields)
return "";
return idFields.length > 0 ? `@@id([${idFields.join(", ")}])` : "";
}
function renderUniqueIndexes(uniqueIndexes) {
return uniqueIndexes.length > 0 ? uniqueIndexes.map(
({ name, fields }) => `@@unique([${fields.join(", ")}]${name ? `, name: "${name}"` : ""})`
) : [];
}
function renderDbName(dbName) {
return dbName ? `@@map("${dbName}")` : "";
}
function renderUrl(envValue) {
const value = envValue.fromEnvVar ? `env("${envValue.fromEnvVar}")` : `"${envValue.value}"`;
return `url = ${value}`;
}
function renderProvider(provider) {
return `provider = "${provider}"`;
}
function renderOutput(path3) {
return path3 ? `output = "${path3}"` : "";
}
function renderPreviewFeatures(previewFeatures) {
return previewFeatures.length ? `previewFeatures = ${JSON.stringify(previewFeatures)}` : "";
}
function renderBlock(type, name, things, documentation) {
return `${renderDocumentation(documentation)}${type} ${name} {
${things.filter((thing) => thing.length > 1).map((thing) => ` ${thing}`).join("\n")}
}`;
}
function deserializeModel(model) {
const { name, fields, dbName, idFields, primaryKey, doubleAtIndexes, uniqueIndexes, documentation } = model;
return renderBlock(
"model",
name,
[
...renderModelFields(fields),
...renderUniqueIndexes(uniqueIndexes),
...doubleAtIndexes != null ? doubleAtIndexes : [],
renderDbName(dbName),
renderIdFieldsOrPrimaryKey(idFields || (primaryKey == null ? void 0 : primaryKey.fields))
],
documentation
);
}
function deserializeDatasource(datasource) {
const { activeProvider: provider, name, url } = datasource;
return renderBlock("datasource", name, [renderProvider(provider), renderUrl(url)]);
}
function deserializeGenerator(generator) {
const { binaryTargets, name, output, provider, previewFeatures } = generator;
return renderBlock("generator", name, [
renderProvider(provider.value),
renderOutput((output == null ? void 0 : output.value) || null),
renderPreviewFeatures(previewFeatures)
]);
}
function deserializeEnum({ name, values, dbName, documentation }) {
const outputValues = values.map(({ name: name2, dbName: dbName2 }) => {
let result = name2;
if (name2 !== dbName2 && dbName2)
result += `@map("${dbName2}")`;
return result;
});
return renderBlock("enum", name, [...outputValues, renderDbName(dbName || null)], documentation);
}
function deserializeModels(models) {
return __async(this, null, function* () {
return models.map((model) => deserializeModel(model)).join("\n");
});
}
function deserializeDatasources(datasources) {
return __async(this, null, function* () {
return datasources.map((datasource) => deserializeDatasource(datasource)).join("\n");
});
}
function deserializeGenerators(generators) {
return __async(this, null, function* () {
return generators.map((generator) => deserializeGenerator(generator)).join("\n");
});
}
function deserializeEnums(enums) {
return __async(this, null, function* () {
return Array.from(new Set(enums.map((each) => deserializeEnum(each)))).join("\n");
});
}
// lib/prismix.ts
var import_glob = __toESM(require("glob"));
var readFile = (0, import_util.promisify)(import_fs.default.readFile);
var writeFile = (0, import_util.promisify)(import_fs.default.writeFile);
function getSchema(schemaPath) {
return __async(this, null, function* () {
try {
const schema = yield readFile(import_path.default.join(process.cwd(), schemaPath), {
encoding: "utf-8"
});
const dmmf = yield (0, import_internals.getDMMF)({ datamodel: schema });
const customAttributes = getCustomAttributes(schema);
const models = dmmf.datamodel.models.map((model) => {
var _a;
return __spreadProps(__spreadValues({}, model), {
doubleAtIndexes: (_a = customAttributes[model.name]) == null ? void 0 : _a.doubleAtIndexes,
fields: model.fields.map(
(field) => {
var _a2, _b;
const attributes = (_b = (_a2 = customAttributes[model.name]) == null ? void 0 : _a2.fields[field.name]) != null ? _b : {};
return __spreadProps(__spreadValues({}, field), {
columnName: attributes.columnName,
dbType: attributes.dbType,
relationOnUpdate: attributes.relationOnUpdate
});
}
)
});
});
const config = yield (0, import_internals.getConfig)({ datamodel: schema });
return {
models,
enums: dmmf.datamodel.enums,
datasources: config.datasources,
generators: config.generators
};
} catch (e) {
console.error(
`Prismix failed to parse schema located at "${schemaPath}". Did you attempt to reference to a model without creating an alias? Remember you must define a "blank" alias model with only the "@id" field in your extended schemas otherwise we can't parse your schema.`,
e
);
}
});
}
function mixModels(inputModels) {
var _a, _b, _c, _d, _e;
const models = {};
for (const newModel of inputModels) {
const existingModel = models[newModel.name];
if (existingModel) {
const existingFieldNames = existingModel.fields.map((f) => f.name);
for (const newField of newModel.fields) {
if (existingFieldNames.includes(newField.name)) {
const existingFieldIndex = existingFieldNames.indexOf(newField.name);
const existingField = existingModel.fields[existingFieldIndex];
if (!newField.columnName && existingField.columnName) {
newField.columnName = existingField.columnName;
}
if (!newField.hasDefaultValue && existingField.hasDefaultValue) {
newField.hasDefaultValue = true;
newField.default = existingField.default;
}
existingModel.fields[existingFieldIndex] = newField;
} else {
existingModel.fields.push(newField);
}
}
if (!existingModel.dbName && newModel.dbName) {
existingModel.dbName = newModel.dbName;
}
if ((_a = newModel.doubleAtIndexes) == null ? void 0 : _a.length) {
existingModel.doubleAtIndexes = [
...(_b = existingModel.doubleAtIndexes) != null ? _b : [],
...newModel.doubleAtIndexes
];
}
if ((_c = newModel.uniqueIndexes) == null ? void 0 : _c.length) {
existingModel.uniqueIndexes = [
...(_d = existingModel.uniqueIndexes) != null ? _d : [],
...newModel.uniqueIndexes
];
existingModel.uniqueFields = [
...(_e = existingModel.uniqueFields) != null ? _e : [],
...newModel.uniqueFields
];
}
} else {
models[newModel.name] = newModel;
}
}
return Object.values(models);
}
function getCustomAttributes(datamodel) {
const modelChunks = datamodel.split("\n}");
return modelChunks.reduce(
(modelDefinitions, modelChunk) => {
var _a;
let pieces = modelChunk.split("\n").filter((chunk) => chunk.trim().length);
const modelName = (_a = pieces.find((name) => name.match(/model (.*) {/))) == null ? void 0 : _a.split(" ")[1];
if (!modelName)
return modelDefinitions;
const mapRegex = new RegExp(new RegExp('[^@]@map\\("(?<name>.*)"\\)'));
const dbRegex = new RegExp(new RegExp("(?<type>@db\\.(.*))"));
const relationOnUpdateRegex = new RegExp(
new RegExp("onUpdate: (?<op>Cascade|NoAction|Restrict|SetDefault|SetNull)")
);
const doubleAtIndexRegex = new RegExp(new RegExp("(?<index>@@index\\(.*\\))"));
const doubleAtIndexes = pieces.reduce((ac, field) => {
var _a2, _b;
const item = (_b = (_a2 = field.match(doubleAtIndexRegex)) == null ? void 0 : _a2.groups) == null ? void 0 : _b.index;
return item ? [...ac, item] : ac;
}, []).filter((f) => f);
const fieldsWithCustomAttributes = pieces.map((field) => {
var _a2, _b, _c, _d, _e, _f;
const columnName = (_b = (_a2 = field.match(mapRegex)) == null ? void 0 : _a2.groups) == null ? void 0 : _b.name;
const dbType = (_d = (_c = field.match(dbRegex)) == null ? void 0 : _c.groups) == null ? void 0 : _d.type;
const relationOnUpdate = (_f = (_e = field.match(relationOnUpdateRegex)) == null ? void 0 : _e.groups) == null ? void 0 : _f.op;
return [field.trim().split(" ")[0], { columnName, dbType, relationOnUpdate }];
}).filter((f) => {
var _a2, _b, _c;
return ((_a2 = f[1]) == null ? void 0 : _a2.columnName) || ((_b = f[1]) == null ? void 0 : _b.dbType) || ((_c = f[1]) == null ? void 0 : _c.relationOnUpdate);
});
return __spreadProps(__spreadValues({}, modelDefinitions), {
[modelName]: { fields: Object.fromEntries(fieldsWithCustomAttributes), doubleAtIndexes }
});
},
{}
);
}
function prismix(options) {
return __async(this, null, function* () {
for (const mixer of options.mixers) {
const schemasToMix = [];
for (const input of mixer.input) {
for (const file of import_glob.default.sync(input)) {
const parsedSchema = yield getSchema(file);
if (parsedSchema)
schemasToMix.push(parsedSchema);
}
}
let models = [];
for (const schema of schemasToMix)
models = [...models, ...schema.models];
models = mixModels(models);
let enums = [];
schemasToMix.forEach((schema) => !!schema.enums && (enums = [...enums, ...schema.enums]));
let datasources = [];
schemasToMix.forEach(
(schema) => schema.datasources.length > 0 && schema.datasources.filter((d) => d.url.value).length > 0 && (datasources = schema.datasources)
);
let generators = [];
schemasToMix.forEach(
(schema) => schema.generators.length > 0 && (generators = schema.generators)
);
let outputSchema = [
"// *** GENERATED BY PRISMIX :: DO NOT EDIT ***",
yield deserializeDatasources(datasources),
yield deserializeGenerators(generators),
yield deserializeModels(models),
yield deserializeEnums(enums)
].filter((e) => e).join("\n");
yield writeFile(import_path.default.join(process.cwd(), mixer.output), outputSchema);
}
});
}
// lib/index.ts
var import_util2 = require("util");
var import_jsonfile = __toESM(require("jsonfile"));
var import_path2 = __toESM(require("path"));
var import_dotenv = __toESM(require("dotenv"));
import_dotenv.default.config();
var readJsonFile = (0, import_util2.promisify)(import_jsonfile.default.readFile);
var args = process.argv.slice(2);
var Prismix = class extends import_command.Command {
run() {
return __async(this, null, function* () {
this.log(`Prismix: mixing your schemas... \u{1F379}`);
const options = yield readJsonFile(
import_path2.default.join(process.cwd(), args[0] || "prismix.config.json")
);
for (const mixer of options.mixers) {
if (!mixer.output)
mixer.output = "prisma/schema.prisma";
}
yield prismix(options);
});
}
};
Prismix.description = "Allows you to have multiple Prisma schema files with shared model relations.";
Prismix.flags = {
version: import_command.flags.version({ char: "v" }),
help: import_command.flags.help({ char: "h" })
};
module.exports = Prismix;
//# sourceMappingURL=index.js.map