UNPKG

prismaql

Version:

A powerful tool for managing and editing Prisma schema files using a SQL-like DSL.

374 lines (365 loc) 16.7 kB
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/modules/prehandlers/mutation-handlers/add-relation.ts var add_relation_exports = {}; __export(add_relation_exports, { addRelation: () => addRelation }); module.exports = __toCommonJS(add_relation_exports); // src/modules/field-relation-collector.ts var import_internals = __toESM(require("@prisma/internals"), 1); var import_pluralize = __toESM(require("pluralize"), 1); // node_modules/change-case/dist/index.cjs var SPLIT_LOWER_UPPER_RE = new RegExp("([\\p{Ll}\\d])(\\p{Lu})", "gu"); var SPLIT_UPPER_UPPER_RE = new RegExp("(\\p{Lu})([\\p{Lu}][\\p{Ll}])", "gu"); var SPLIT_SEPARATE_NUMBER_RE = new RegExp("(\\d)\\p{Ll}|(\\p{L})\\d", "u"); var DEFAULT_STRIP_REGEXP = /[^\p{L}\d]+/giu; var SPLIT_REPLACE_VALUE = "$1\0$2"; var DEFAULT_PREFIX_SUFFIX_CHARACTERS = ""; function split(value) { let result = value.trim(); result = result.replace(SPLIT_LOWER_UPPER_RE, SPLIT_REPLACE_VALUE).replace(SPLIT_UPPER_UPPER_RE, SPLIT_REPLACE_VALUE); result = result.replace(DEFAULT_STRIP_REGEXP, "\0"); let start = 0; let end = result.length; while (result.charAt(start) === "\0") start++; if (start === end) return []; while (result.charAt(end - 1) === "\0") end--; return result.slice(start, end).split(/\0/g); } function splitSeparateNumbers(value) { const words = split(value); for (let i = 0; i < words.length; i++) { const word = words[i]; const match = SPLIT_SEPARATE_NUMBER_RE.exec(word); if (match) { const offset = match.index + (match[1] ?? match[2]).length; words.splice(i, 1, word.slice(0, offset), word.slice(offset)); } } return words; } function camelCase(input, options) { const [prefix, words, suffix] = splitPrefixSuffix(input, options); const lower = lowerFactory(options?.locale); const upper = upperFactory(options?.locale); const transform = options?.mergeAmbiguousCharacters ? capitalCaseTransformFactory(lower, upper) : pascalCaseTransformFactory(lower, upper); return prefix + words.map((word, index) => { if (index === 0) return lower(word); return transform(word, index); }).join(options?.delimiter ?? "") + suffix; } function pascalCase(input, options) { const [prefix, words, suffix] = splitPrefixSuffix(input, options); const lower = lowerFactory(options?.locale); const upper = upperFactory(options?.locale); const transform = options?.mergeAmbiguousCharacters ? capitalCaseTransformFactory(lower, upper) : pascalCaseTransformFactory(lower, upper); return prefix + words.map(transform).join(options?.delimiter ?? "") + suffix; } function lowerFactory(locale) { return locale === false ? (input) => input.toLowerCase() : (input) => input.toLocaleLowerCase(locale); } function upperFactory(locale) { return locale === false ? (input) => input.toUpperCase() : (input) => input.toLocaleUpperCase(locale); } function capitalCaseTransformFactory(lower, upper) { return (word) => `${upper(word[0])}${lower(word.slice(1))}`; } function pascalCaseTransformFactory(lower, upper) { return (word, index) => { const char0 = word[0]; const initial = index > 0 && char0 >= "0" && char0 <= "9" ? "_" + char0 : upper(char0); return initial + lower(word.slice(1)); }; } function splitPrefixSuffix(input, options = {}) { const splitFn = options.split ?? (options.separateNumbers ? splitSeparateNumbers : split); const prefixCharacters = options.prefixCharacters ?? DEFAULT_PREFIX_SUFFIX_CHARACTERS; const suffixCharacters = options.suffixCharacters ?? DEFAULT_PREFIX_SUFFIX_CHARACTERS; let prefixIndex = 0; let suffixIndex = input.length; while (prefixIndex < input.length) { const char = input.charAt(prefixIndex); if (!prefixCharacters.includes(char)) break; prefixIndex++; } while (suffixIndex > prefixIndex) { const index = suffixIndex - 1; const char = input.charAt(index); if (!suffixCharacters.includes(char)) break; suffixIndex = index; } return [ input.slice(0, prefixIndex), splitFn(input.slice(prefixIndex, suffixIndex)), input.slice(suffixIndex) ]; } // src/modules/field-relation-collector.ts var { getDMMF } = import_internals.default; var getManyToManyTableName = (modelA, modelB, relationName) => { if (relationName) return relationName; const [first, second] = [modelA, modelB].sort(); return `_${pascalCase(first)}To${pascalCase(second)}`; }; var getManyToManyModelName = (modelA, modelB, relationName) => { if (relationName) return relationName; const [first, second] = [modelA, modelB].sort(); return `${pascalCase(first)}To${pascalCase(second)}`; }; // src/modules/handler-registries/handler-registry.ts var handlerResponse = (dsl) => { return { error: (error) => { return { dsl, error }; }, result: (result) => { return { dsl, result }; } }; }; // src/modules/prehandlers/mutation-handlers/add-relation.ts var import_pluralize2 = __toESM(require("pluralize"), 1); // src/modules/utils/schema-helper.ts var PrismaQlSchemaHelper = class { parsedSchema; constructor(parsedSchema) { this.parsedSchema = parsedSchema; } getModels(names) { const models = this.parsedSchema.list.filter((item) => item.type === "model"); if (names?.length) { return models.filter((model) => names.includes(model.name)); } return models; } getModelByName(name) { return this.getModels().find((model) => model.name === name); } getFieldByName(modelName, fieldName) { const model = this.getModelByName(modelName); if (!model) return void 0; return model.properties.find((prop) => prop.type === "field" && prop.name === fieldName); } getFields(modelName) { const model = this.getModelByName(modelName); if (!model) return []; return model.properties.filter((prop) => prop.type === "field"); } getIdFieldTypeModel(modelName) { const model = this.getModelByName(modelName); if (!model) return void 0; const idField = model.properties.find((prop) => prop.type === "field" && prop?.attributes?.some((attr) => attr.name === "id")); return idField?.fieldType; } getEnums() { return this.parsedSchema.list.filter((item) => item.type === "enum"); } getEnumByName(name) { return this.getEnums().find((enumItem) => enumItem.name === name); } getEnumRelations(enumName) { const models = this.getModels(); return models.filter((model) => { return model.properties.some((prop) => { return prop.type === "field" && prop.fieldType === enumName; }); }).map((model) => { const field = model.properties.find((prop) => { return prop.type === "field" && prop.fieldType === enumName; }); return { model, field }; }); } getRelations() { return this.getModels().flatMap((model) => model.properties).filter((prop) => prop.type === "field" && prop.fieldType === "relation"); } getGenerators() { return this.parsedSchema.list.filter((item) => item.type === "generator"); } getModelRelations(modelName) { const model = this.getModelByName(modelName); if (!model) return []; return model.properties.filter( (prop) => prop.type === "field" && prop.fieldType === "relation" ); } }; var useHelper = (schema) => { return new PrismaQlSchemaHelper("type" in schema ? schema : schema.ast); }; // src/modules/prehandlers/mutation-handlers/add-relation.ts var addRelation = (prismaState, data) => { const { args, options } = data; const response = handlerResponse(data); const type = options?.type; const models = args?.models; if (!models || models.length !== 2) { return response.error("Two models are required for relation. Example: ADD RELATION ->[ModelA] AND ->[ModelB] (type=1:1)"); } if (!type) { return response.error("Relation type is required. Valid types are: '1:1', '1:M', 'M:N'. Example: ADD RELATION ModelA AND ModelB (type=1:1)"); } const [modelA, modelB] = models; const { builder } = prismaState; const modelARef = builder.findByType("model", { name: modelA }); const modelBRef = builder.findByType("model", { name: modelB }); if (!modelARef) { return response.error(`Model ${modelA} not found, please add it first`); } if (!modelBRef) { return response.error(`Model ${modelB} not found, please add it first`); } const optional = options?.required === false || options?.required === void 0; const pivotTable = options?.pivotTable && options?.pivotTable === true ? getManyToManyModelName(modelA, modelB) : options?.pivotTable || null; const sourceRelationName = options?.relationName || getManyToManyTableName(modelA, modelB); const relationName = `"${sourceRelationName}"`; const pivotModelName = pivotTable ? pascalCase(pivotTable) : null; const isSelfRelation = modelA === modelB; const fk = (modelName) => { return camelCase(`${modelName}Id`); }; const selfPrefix = (modelName, asFk = false) => { return isSelfRelation ? camelCase(modelName + "By" + (asFk ? "Id" : "")) : camelCase(modelName + (asFk ? "Id" : "")); }; if (options?.fkHolder && options?.fkHolder !== modelA && options?.fkHolder !== modelB) { return response.error(`Model ${options.fkHolder} not found, please add it first`); } const makeOptional = (str) => { return str + "?"; }; const isOptional = (str) => { return str + (optional ? "?" : ""); }; const helper = useHelper(prismaState); if (type == "1:1") { if (!pivotTable) { const aModelName = options?.fkHolder || modelBRef.name; const bModelName = aModelName === modelA ? modelB : modelA; const idFieldModelB = helper.getIdFieldTypeModel(bModelName) || "String"; const fkKey = fk(bModelName); const refModelKey = selfPrefix(bModelName); builder.model(aModelName).field(fkKey, isOptional(idFieldModelB)).attribute("unique").field(refModelKey, isOptional(bModelName)).attribute("relation", [relationName, `fields: [${fkKey}]`, `references: [id]`]); builder.model(bModelName).field(camelCase(aModelName), makeOptional(aModelName)).attribute("relation", [relationName]); return response.result(`One-to-One relation added between ${modelA} and ${modelB}`); } else { const fkA = selfPrefix(modelA, true); const fkB = fk(modelB); const pivotOnly = options?.pivotOnly; const idFieldModelA = helper.getIdFieldTypeModel(modelA) || "String"; const idFieldModelB = helper.getIdFieldTypeModel(modelB) || "String"; if (!pivotOnly) { builder.model(pivotModelName).field("createdAt", "DateTime").attribute("default", ["now()"]).field(fkA, idFieldModelA).attribute("unique").field(fkB, idFieldModelB).attribute("unique").blockAttribute("id", [fkA, fkB]).field(modelA.toLowerCase(), modelA).attribute("relation", [ relationName, `fields: [${fkA}]`, `references: [id]` ]); builder.model(modelA).field(camelCase(modelB), `${pivotModelName}?`).attribute("relation", [relationName]); } else { builder.model(pivotModelName).field(fkA, idFieldModelA).attribute("unique").field(fkB, idFieldModelB).attribute("unique").field("createdAt", "DateTime").attribute("default", ["now()"]).blockAttribute("id", [fkA, fkB]); } return response.result(`One-to-One relation (with pivot table) added between ${modelA} and ${modelB}`); } } else if (type == "1:M") { if (!pivotTable) { const aModelName = options?.fkHolder || modelA; const bModelName = aModelName === modelA ? modelB : modelA; const idFieldModelB = helper.getIdFieldTypeModel(bModelName) || "String"; const fkKey = fk(bModelName); const refModelKey = selfPrefix(bModelName); builder.model(aModelName).field(fkKey, isOptional(idFieldModelB)).field(refModelKey, isOptional(bModelName)).attribute("relation", [relationName, `fields: [${fkKey}]`, `references: [id]`]); builder.model(bModelName).field(import_pluralize2.default.plural(camelCase(aModelName)), aModelName + "[]").attribute("relation", [relationName]); return response.result(`One-to-Many relation added between ${modelA} and ${modelB}`); } else { const fkA = selfPrefix(modelA, true); const fkB = selfPrefix(modelB, true); const idFieldModelA = helper.getIdFieldTypeModel(modelA) || "String"; const idFieldModelB = helper.getIdFieldTypeModel(modelB) || "String"; builder.model(pivotModelName).field("createdAt", "DateTime").attribute("default", ["now()"]).field(fkA, idFieldModelA).attribute("unique").field(fkB, idFieldModelB).attribute("unique").blockAttribute("id", [fkA, fkB]).field(modelA.toLowerCase(), modelA).attribute("relation", [ relationName, `fields: [${fkA}]`, `references: [id]` ]); builder.model(modelA).field(import_pluralize2.default.plural(camelCase(modelB)), `${pivotModelName}[]`).attribute("relation", [relationName]); return response.result(`One-to-Many relation (with pivot table) added between ${modelA} and ${modelB}`); } } if (type == "M:N") { if (!pivotTable) { const toKey = import_pluralize2.default.plural(camelCase(modelB)); const fromKey = import_pluralize2.default.plural(selfPrefix(modelA)); builder.model(modelA).field(toKey, `${modelB}[]`).attribute("relation", [relationName]); builder.model(modelB).field(fromKey, `${modelA}[]`).attribute("relation", [relationName]); return response.result(`Many-to-Many relation (without pivot table) added between ${modelA} and ${modelB}`); } else { const customSelfPrefix = (str) => { return isSelfRelation ? str + "By" : str; }; const toPluralKey = import_pluralize2.default.plural(camelCase(modelB)); const fromPluralKey = import_pluralize2.default.plural(selfPrefix(modelA)); const aModelBuilder = builder.model(modelA).field(toPluralKey, `${pivotModelName}[]`).attribute("relation", [relationName]); const bModelBuilder = isSelfRelation ? aModelBuilder : builder.model(modelB); bModelBuilder.field(fromPluralKey, `${pivotModelName}[]`).attribute("relation", [`"${customSelfPrefix(sourceRelationName)}"`]); const fkA = selfPrefix(modelA, true); const fkB = fk(modelB); const toKey = camelCase(modelB); const fromKey = selfPrefix(modelA); const idFieldModelA = helper.getIdFieldTypeModel(modelA) || "String"; const idFieldModelB = helper.getIdFieldTypeModel(modelB) || "String"; builder.model(pivotModelName).field(fkA, idFieldModelA).field(fkB, idFieldModelB).field("createdAt", "DateTime").attribute("default", ["now()"]).field(fromKey, modelA).attribute("relation", [ `"${customSelfPrefix(sourceRelationName)}"`, `fields: [${fkA}]`, `references: [id]`, `onDelete: Cascade` ]).field(toKey, modelB).attribute("relation", [ relationName, `fields: [${fkB}]`, `references: [id]`, `onDelete: Cascade` ]).blockAttribute("id", [fkA, fkB]); } return response.result(`Many-to-Many relation (with pivot table) added for ${modelA} and ${modelB}`); } return response.error("Not implemented"); }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { addRelation });