zod-prisma
Advanced tools
'use strict'; | ||
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } | ||
var path = _interopDefault(require('path')); | ||
var generatorHelper = require('@prisma/generator-helper'); | ||
var typescript = require('typescript'); | ||
var zod = require('zod'); | ||
var path = require('path'); | ||
var tsMorph = require('ts-morph'); | ||
var typescript = require('typescript'); | ||
var z = _interopDefault(require('zod')); | ||
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } | ||
var path__default = /*#__PURE__*/_interopDefaultLegacy(path); | ||
var version = "0.5.0"; | ||
var configBoolean = /*#__PURE__*/zod.z["enum"](['true', 'false']).transform(function (arg) { | ||
return JSON.parse(arg); | ||
}); | ||
var configSchema = /*#__PURE__*/zod.z.object({ | ||
relationModel: /*#__PURE__*/configBoolean["default"]('true').or( /*#__PURE__*/zod.z.literal('default')), | ||
modelSuffix: /*#__PURE__*/zod.z.string()["default"]('Model'), | ||
modelCase: /*#__PURE__*/zod.z["enum"](['PascalCase', 'camelCase'])["default"]('PascalCase'), | ||
useDecimalJs: /*#__PURE__*/configBoolean["default"]('false'), | ||
imports: /*#__PURE__*/zod.z.string().optional(), | ||
prismaJsonNullability: /*#__PURE__*/configBoolean["default"]('true') | ||
}); | ||
var writeArray = function writeArray(writer, array, newLine) { | ||
if (newLine === void 0) { | ||
newLine = true; | ||
} | ||
return array.forEach(function (line) { | ||
return writer.write(line).conditionalNewLine(newLine); | ||
}); | ||
}; | ||
var useModelNames = function useModelNames(_ref) { | ||
var modelCase = _ref.modelCase, | ||
modelSuffix = _ref.modelSuffix, | ||
relationModel = _ref.relationModel; | ||
var formatModelName = function formatModelName(name, prefix) { | ||
if (prefix === void 0) { | ||
prefix = ''; | ||
} | ||
if (modelCase === 'camelCase') { | ||
name = name.slice(0, 1).toLowerCase() + name.slice(1); | ||
} | ||
return "" + prefix + name + modelSuffix; | ||
}; | ||
return { | ||
modelName: function modelName(name) { | ||
return formatModelName(name, relationModel === 'default' ? '_' : ''); | ||
}, | ||
relatedModelName: function relatedModelName(name) { | ||
return formatModelName(relationModel === 'default' ? name.toString() : "Related" + name.toString()); | ||
} | ||
}; | ||
}; | ||
var needsRelatedModel = function needsRelatedModel(model, config) { | ||
return model.fields.some(function (field) { | ||
return field.kind === 'object'; | ||
}) && config.relationModel !== false; | ||
}; | ||
var getJSDocs = function getJSDocs(docString) { | ||
@@ -30,10 +87,24 @@ var lines = []; | ||
}; | ||
var computeModifiers = function computeModifiers(docString) { | ||
var getZodDocElements = function getZodDocElements(docString) { | ||
return docString.split('\n').filter(function (line) { | ||
return line.trimLeft().startsWith('@zod'); | ||
}).map(function (line) { | ||
return line.trim().split('@zod.').slice(-1)[0]; | ||
return line.trimStart().startsWith('@zod'); | ||
}).flatMap(function (line) { | ||
return Array.from(line.matchAll(/\.([^().]+\(.*?\))/g), function (m) { | ||
return m.slice(1); | ||
}).flat(); | ||
}); | ||
}; | ||
var computeCustomSchema = function computeCustomSchema(docString) { | ||
var _getZodDocElements$fi; | ||
return (_getZodDocElements$fi = getZodDocElements(docString).find(function (modifier) { | ||
return modifier.startsWith('custom('); | ||
})) == null ? void 0 : _getZodDocElements$fi.slice(7).slice(0, -1); | ||
}; | ||
var computeModifiers = function computeModifiers(docString) { | ||
return getZodDocElements(docString).filter(function (each) { | ||
return !each.startsWith('custom('); | ||
}); | ||
}; | ||
var getZodConstructor = function getZodConstructor(field, getRelatedModelName) { | ||
@@ -77,3 +148,3 @@ if (getRelatedModelName === void 0) { | ||
case 'Json': | ||
zodType = 'z.any()'; | ||
zodType = 'jsonSchema'; | ||
break; | ||
@@ -84,2 +155,3 @@ | ||
break; | ||
// TODO: Proper type for bytes fields | ||
@@ -96,186 +168,221 @@ case 'Bytes': | ||
if (!field.isRequired) extraModifiers.push('nullable()'); | ||
if (field.isList) extraModifiers.push('array()'); | ||
if (field.documentation) { | ||
var _computeCustomSchema; | ||
zodType = (_computeCustomSchema = computeCustomSchema(field.documentation)) != null ? _computeCustomSchema : zodType; | ||
extraModifiers.push.apply(extraModifiers, computeModifiers(field.documentation)); | ||
} | ||
if (!field.isRequired && field.type !== 'Json') extraModifiers.push('nullish()'); | ||
return "" + zodType + extraModifiers.join('.'); | ||
}; | ||
var writeArray = function writeArray(writer, array, newLine) { | ||
if (newLine === void 0) { | ||
newLine = true; | ||
var writeImportsForModel = function writeImportsForModel(model, sourceFile, config, _ref) { | ||
var schemaPath = _ref.schemaPath, | ||
outputPath = _ref.outputPath, | ||
clientPath = _ref.clientPath; | ||
var _useModelNames = useModelNames(config), | ||
relatedModelName = _useModelNames.relatedModelName; | ||
var importList = [{ | ||
kind: tsMorph.StructureKind.ImportDeclaration, | ||
namespaceImport: 'z', | ||
moduleSpecifier: 'zod' | ||
}]; | ||
if (config.imports) { | ||
importList.push({ | ||
kind: tsMorph.StructureKind.ImportDeclaration, | ||
namespaceImport: 'imports', | ||
moduleSpecifier: path__default["default"].relative(outputPath, path__default["default"].resolve(path__default["default"].dirname(schemaPath), config.imports)) | ||
}); | ||
} | ||
return array.forEach(function (line) { | ||
return writer.write(line).conditionalNewLine(newLine); | ||
if (config.useDecimalJs && model.fields.some(function (f) { | ||
return f.type === 'Decimal'; | ||
})) { | ||
importList.push({ | ||
kind: tsMorph.StructureKind.ImportDeclaration, | ||
namedImports: ['Decimal'], | ||
moduleSpecifier: 'decimal.js' | ||
}); | ||
} | ||
var enumFields = model.fields.filter(function (f) { | ||
return f.kind === 'enum'; | ||
}); | ||
var relationFields = model.fields.filter(function (f) { | ||
return f.kind === 'object'; | ||
}); | ||
var relativePath = path__default["default"].relative(outputPath, clientPath); | ||
if (enumFields.length > 0) { | ||
importList.push({ | ||
kind: tsMorph.StructureKind.ImportDeclaration, | ||
isTypeOnly: enumFields.length === 0, | ||
moduleSpecifier: relativePath, | ||
namedImports: enumFields.map(function (f) { | ||
return f.type; | ||
}) | ||
}); | ||
} | ||
if (config.relationModel !== false && relationFields.length > 0) { | ||
var filteredFields = relationFields.filter(function (f) { | ||
return f.type !== model.name; | ||
}); | ||
if (filteredFields.length > 0) { | ||
importList.push({ | ||
kind: tsMorph.StructureKind.ImportDeclaration, | ||
moduleSpecifier: './index', | ||
namedImports: Array.from(new Set(filteredFields.flatMap(function (f) { | ||
return ["Complete" + f.type, relatedModelName(f.type)]; | ||
}))) | ||
}); | ||
} | ||
} | ||
sourceFile.addImportDeclarations(importList); | ||
}; | ||
var writeTypeSpecificSchemas = function writeTypeSpecificSchemas(model, sourceFile, config, _prismaOptions) { | ||
if (model.fields.some(function (f) { | ||
return f.type === 'Json'; | ||
})) { | ||
sourceFile.addStatements(function (writer) { | ||
writer.newLine(); | ||
writeArray(writer, ['// Helper schema for JSON fields', "type Literal = boolean | number | string" + (config.prismaJsonNullability ? '' : '| null'), 'type Json = Literal | { [key: string]: Json } | Json[]', "const literalSchema = z.union([z.string(), z.number(), z.boolean()" + (config.prismaJsonNullability ? '' : ', z.null()') + "])", 'const jsonSchema: z.ZodSchema<Json> = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]))']); | ||
}); | ||
} | ||
var configSchema = /*#__PURE__*/z.object({ | ||
relationModel: /*#__PURE__*/z["enum"](['default', 'true', 'false'])["default"]('true').transform(function (val) { | ||
switch (val) { | ||
case 'default': | ||
return val; | ||
if (config.useDecimalJs && model.fields.some(function (f) { | ||
return f.type === 'Decimal'; | ||
})) { | ||
sourceFile.addStatements(function (writer) { | ||
writer.newLine(); | ||
writeArray(writer, ['// Helper schema for Decimal fields', 'z', '.instanceof(Decimal)', '.or(z.string())', '.or(z.number())', '.refine((value) => {', ' try {', ' return new Decimal(value);', ' } catch (error) {', ' return false;', ' }', '})', '.transform((value) => new Decimal(value));']); | ||
}); | ||
} // TODO: Add Decimal | ||
case 'true': | ||
return true; | ||
}; | ||
var generateSchemaForModel = function generateSchemaForModel(model, sourceFile, config, _prismaOptions) { | ||
var _useModelNames2 = useModelNames(config), | ||
modelName = _useModelNames2.modelName; | ||
case 'false': | ||
return false; | ||
} | ||
}), | ||
modelSuffix: /*#__PURE__*/z.string()["default"]('Model'), | ||
modelCase: /*#__PURE__*/z["enum"](['PascalCase', 'camelCase'])["default"]('PascalCase') | ||
}); | ||
sourceFile.addVariableStatement({ | ||
declarationKind: tsMorph.VariableDeclarationKind.Const, | ||
isExported: true, | ||
leadingTrivia: function leadingTrivia(writer) { | ||
return writer.blankLineIfLastNot(); | ||
}, | ||
declarations: [{ | ||
name: modelName(model.name), | ||
initializer: function initializer(writer) { | ||
writer.write('z.object(').inlineBlock(function () { | ||
model.fields.filter(function (f) { | ||
return f.kind !== 'object'; | ||
}).forEach(function (field) { | ||
writeArray(writer, getJSDocs(field.documentation)); | ||
writer.write(field.name + ": " + getZodConstructor(field)).write(',').newLine(); | ||
}); | ||
}).write(')'); | ||
} | ||
}] | ||
}); | ||
}; | ||
var generateRelatedSchemaForModel = function generateRelatedSchemaForModel(model, sourceFile, config, _prismaOptions) { | ||
var _useModelNames3 = useModelNames(config), | ||
modelName = _useModelNames3.modelName, | ||
relatedModelName = _useModelNames3.relatedModelName; | ||
var relationFields = model.fields.filter(function (f) { | ||
return f.kind === 'object'; | ||
}); | ||
sourceFile.addInterface({ | ||
name: "Complete" + model.name, | ||
isExported: true, | ||
"extends": ["z.infer<typeof " + modelName(model.name) + ">"], | ||
properties: relationFields.map(function (f) { | ||
return { | ||
name: f.name, | ||
type: "Complete" + f.type + (f.isList ? '[]' : '') + (!f.isRequired ? ' | null' : '') | ||
}; | ||
}) | ||
}); | ||
sourceFile.addStatements(function (writer) { | ||
return writeArray(writer, ['', '/**', " * " + relatedModelName(model.name) + " contains all relations on your model in addition to the scalars", ' *', ' * NOTE: Lazy required in case of potential circular dependencies within schema', ' */']); | ||
}); | ||
sourceFile.addVariableStatement({ | ||
declarationKind: tsMorph.VariableDeclarationKind.Const, | ||
isExported: true, | ||
declarations: [{ | ||
name: relatedModelName(model.name), | ||
type: "z.ZodSchema<Complete" + model.name + ">", | ||
initializer: function initializer(writer) { | ||
writer.write("z.lazy(() => " + modelName(model.name) + ".extend(").inlineBlock(function () { | ||
relationFields.forEach(function (field) { | ||
writeArray(writer, getJSDocs(field.documentation)); | ||
writer.write(field.name + ": " + getZodConstructor(field, relatedModelName)).write(',').newLine(); | ||
}); | ||
}).write('))'); | ||
} | ||
}] | ||
}); | ||
}; | ||
var populateModelFile = function populateModelFile(model, sourceFile, config, prismaOptions) { | ||
writeImportsForModel(model, sourceFile, config, prismaOptions); | ||
writeTypeSpecificSchemas(model, sourceFile, config); | ||
generateSchemaForModel(model, sourceFile, config); | ||
if (needsRelatedModel(model, config)) generateRelatedSchemaForModel(model, sourceFile, config); | ||
}; | ||
var generateBarrelFile = function generateBarrelFile(models, indexFile) { | ||
models.forEach(function (model) { | ||
return indexFile.addExportDeclaration({ | ||
moduleSpecifier: "./" + model.name.toLowerCase() | ||
}); | ||
}); | ||
}; | ||
// @ts-ignore Importing package.json for automated synchronization of version numbers | ||
generatorHelper.generatorHandler({ | ||
onManifest: function onManifest() { | ||
return { | ||
version: version, | ||
prettyName: 'Zod Schemas', | ||
defaultOutput: 'zod', | ||
version: '0.2.1' | ||
defaultOutput: 'zod' | ||
}; | ||
}, | ||
onGenerate: function onGenerate(options) { | ||
var project = new tsMorph.Project({ | ||
skipAddingFilesFromTsConfig: true | ||
}); | ||
var project = new tsMorph.Project(); | ||
var models = options.dmmf.datamodel.models; | ||
var schemaPath = options.schemaPath; | ||
var outputPath = options.generator.output.value; | ||
var models = options.dmmf.datamodel.models; | ||
var prismaClient = options.otherGenerators.find(function (each) { | ||
var clientPath = options.otherGenerators.find(function (each) { | ||
return each.provider.value === 'prisma-client-js'; | ||
}); | ||
var parsedConfig = configSchema.safeParse(options.generator.config); | ||
if (!parsedConfig.success) throw new Error('Incorrect config provided. Please check the values you provided and try again.'); | ||
var _parsedConfig$data = parsedConfig.data, | ||
relationModel = _parsedConfig$data.relationModel, | ||
modelSuffix = _parsedConfig$data.modelSuffix, | ||
modelCase = _parsedConfig$data.modelCase; | ||
var formatModelName = function formatModelName(name, prefix) { | ||
if (prefix === void 0) { | ||
prefix = ''; | ||
} | ||
if (modelCase === 'camelCase') { | ||
name = name.slice(0, 1).toLowerCase() + name.slice(1); | ||
} | ||
return "" + prefix + name + modelSuffix; | ||
}).output.value; | ||
var results = configSchema.safeParse(options.generator.config); | ||
if (!results.success) throw new Error('Incorrect config provided. Please check the values you provided and try again.'); | ||
var config = results.data; | ||
var prismaOptions = { | ||
clientPath: clientPath, | ||
outputPath: outputPath, | ||
schemaPath: schemaPath | ||
}; | ||
var indexSource = project.createSourceFile(outputPath + "/index.ts", {}, { | ||
var indexFile = project.createSourceFile(outputPath + "/index.ts", {}, { | ||
overwrite: true | ||
}); | ||
generateBarrelFile(models, indexFile); | ||
indexFile.formatText({ | ||
indentSize: 2, | ||
convertTabsToSpaces: true, | ||
semicolons: typescript.SemicolonPreference.Remove | ||
}); | ||
models.forEach(function (model) { | ||
var _prismaClient$output, _relativePath; | ||
indexSource.addExportDeclaration({ | ||
moduleSpecifier: "./" + model.name.toLowerCase() | ||
}); | ||
var modelName = function modelName(name) { | ||
return formatModelName(name, relationModel === 'default' ? '_' : ''); | ||
}; | ||
var relatedModelName = function relatedModelName(name) { | ||
return formatModelName(relationModel === 'default' ? name.toString() : "Related" + name.toString()); | ||
}; | ||
var sourceFile = project.createSourceFile(outputPath + "/" + model.name.toLowerCase() + ".ts", { | ||
statements: [{ | ||
kind: tsMorph.StructureKind.ImportDeclaration, | ||
namespaceImport: 'z', | ||
moduleSpecifier: 'zod' | ||
}] | ||
}, { | ||
var sourceFile = project.createSourceFile(outputPath + "/" + model.name.toLowerCase() + ".ts", {}, { | ||
overwrite: true | ||
}); | ||
var enumFields = model.fields.filter(function (f) { | ||
return f.kind === 'enum'; | ||
}); | ||
var relativePath = prismaClient != null && (_prismaClient$output = prismaClient.output) != null && _prismaClient$output.value ? path.relative(outputPath, prismaClient.output.value) : null; | ||
if (relativePath && !(relativePath.startsWith('./') || relativePath.startsWith('../'))) relativePath = "./" + relativePath; | ||
sourceFile.addImportDeclaration({ | ||
kind: tsMorph.StructureKind.ImportDeclaration, | ||
moduleSpecifier: (_relativePath = relativePath) != null ? _relativePath : '@prisma/client', | ||
namedImports: [model.name].concat(enumFields.map(function (f) { | ||
return f.type; | ||
})) | ||
}); | ||
sourceFile.addStatements(function (writer) { | ||
return writeArray(writer, getJSDocs(model.documentation)); | ||
}); | ||
sourceFile.addVariableStatement({ | ||
declarationKind: tsMorph.VariableDeclarationKind.Const, | ||
isExported: true, | ||
declarations: [{ | ||
name: modelName(model.name), | ||
initializer: function initializer(writer) { | ||
writer.write('z.object(').inlineBlock(function () { | ||
model.fields.filter(function (f) { | ||
return f.kind !== 'object'; | ||
}).forEach(function (field) { | ||
writeArray(writer, getJSDocs(field.documentation)); | ||
writer.write(field.name + ": " + getZodConstructor(field)).write(',').newLine(); | ||
}); | ||
}).write(')'); | ||
} | ||
}] | ||
}); | ||
var relationFields = model.fields.filter(function (f) { | ||
return f.kind === 'object'; | ||
}); | ||
if (relationModel !== false && relationFields.length > 0) { | ||
var filteredFields = relationFields.filter(function (f) { | ||
return f.type !== model.name; | ||
}); | ||
if (filteredFields.length > 0) { | ||
sourceFile.addImportDeclaration({ | ||
kind: tsMorph.StructureKind.ImportDeclaration, | ||
moduleSpecifier: './index', | ||
namedImports: Array.from(new Set(filteredFields.flatMap(function (f) { | ||
return ["Complete" + f.type, relatedModelName(f.type)]; | ||
}))) | ||
}); | ||
} | ||
sourceFile.addInterface({ | ||
name: "Complete" + model.name, | ||
isExported: true, | ||
"extends": function _extends(writer) { | ||
return writer.write(model.name); | ||
}, | ||
properties: relationFields.map(function (f) { | ||
return { | ||
name: f.name, | ||
type: "Complete" + f.type + (f.isList ? '[]' : '') + (!f.isRequired ? ' | null' : '') | ||
}; | ||
}) | ||
}); | ||
sourceFile.addStatements(function (writer) { | ||
return writeArray(writer, ['', '/**', " * " + relatedModelName(model.name) + " contains all relations on your model in addition to the scalars", ' *', ' * NOTE: Lazy required in case of potential circular dependencies within schema', ' */']); | ||
}); | ||
sourceFile.addVariableStatement({ | ||
declarationKind: tsMorph.VariableDeclarationKind.Const, | ||
isExported: true, | ||
declarations: [{ | ||
name: relatedModelName(model.name), | ||
type: "z.ZodSchema<Complete" + model.name + ">", | ||
initializer: function initializer(writer) { | ||
writer.write("z.lazy(() => " + modelName(model.name) + ".extend(").inlineBlock(function () { | ||
relationFields.forEach(function (field) { | ||
writeArray(writer, getJSDocs(field.documentation)); | ||
writer.write(field.name + ": " + getZodConstructor(field, relatedModelName)).write(',').newLine(); | ||
}); | ||
}).write('))'); | ||
} | ||
}] | ||
}); | ||
} | ||
populateModelFile(model, sourceFile, config, prismaOptions); | ||
sourceFile.formatText({ | ||
@@ -282,0 +389,0 @@ indentSize: 2, |
@@ -1,2 +0,2 @@ | ||
"use strict";function e(e){return e&&"object"==typeof e&&"default"in e?e.default:e}var t=e(require("path")),n=require("@prisma/generator-helper"),r=require("ts-morph"),i=require("typescript"),a=e(require("zod")),o=function(e){var t=[];if(e){var n=e.split("\n").filter((function(e){return!e.trimLeft().startsWith("@zod")}));n.length&&(t.push("/**"),n.forEach((function(e){return t.push(" * "+e)})),t.push(" */"))}return t},u=function(e,t){void 0===t&&(t=function(e){return e.toString()});var n="z.unknown()",r=[""];if("scalar"===e.kind)switch(e.type){case"String":n="z.string()";break;case"Int":n="z.number()",r.push("int()");break;case"BigInt":n="z.bigint()";break;case"DateTime":n="z.date()";break;case"Float":case"Decimal":n="z.number()";break;case"Json":n="z.any()";break;case"Boolean":n="z.boolean()";break;case"Bytes":n="z.unknown()"}else"enum"===e.kind?n="z.nativeEnum("+e.type+")":"object"===e.kind&&(n=t(e.type));return e.isRequired||r.push("nullable()"),e.isList&&r.push("array()"),e.documentation&&r.push.apply(r,e.documentation.split("\n").filter((function(e){return e.trimLeft().startsWith("@zod")})).map((function(e){return e.trim().split("@zod.").slice(-1)[0]}))),""+n+r.join(".")},c=function(e,t,n){return void 0===n&&(n=!0),t.forEach((function(t){return e.write(t).conditionalNewLine(n)}))},l=a.object({relationModel:a.enum(["default","true","false"]).default("true").transform((function(e){switch(e){case"default":return e;case"true":return!0;case"false":return!1}})),modelSuffix:a.string().default("Model"),modelCase:a.enum(["PascalCase","camelCase"]).default("PascalCase")});n.generatorHandler({onManifest:function(){return{prettyName:"Zod Schemas",defaultOutput:"zod",version:"0.2.1"}},onGenerate:function(e){var n=new r.Project({skipAddingFilesFromTsConfig:!0}),a=e.generator.output.value,s=e.dmmf.datamodel.models,d=e.otherGenerators.find((function(e){return"prisma-client-js"===e.provider.value})),f=l.safeParse(e.generator.config);if(!f.success)throw new Error("Incorrect config provided. Please check the values you provided and try again.");var m=f.data,p=m.relationModel,h=m.modelSuffix,v=m.modelCase,w=function(e,t){return void 0===t&&(t=""),"camelCase"===v&&(e=e.slice(0,1).toLowerCase()+e.slice(1)),""+t+e+h},z=n.createSourceFile(a+"/index.ts",{},{overwrite:!0});return s.forEach((function(e){var l,s;z.addExportDeclaration({moduleSpecifier:"./"+e.name.toLowerCase()});var f=function(e){return w(e,"default"===p?"_":"")},m=function(e){return w("default"===p?e.toString():"Related"+e.toString())},h=n.createSourceFile(a+"/"+e.name.toLowerCase()+".ts",{statements:[{kind:r.StructureKind.ImportDeclaration,namespaceImport:"z",moduleSpecifier:"zod"}]},{overwrite:!0}),v=e.fields.filter((function(e){return"enum"===e.kind})),S=null!=d&&null!=(l=d.output)&&l.value?t.relative(a,d.output.value):null;!S||S.startsWith("./")||S.startsWith("../")||(S="./"+S),h.addImportDeclaration({kind:r.StructureKind.ImportDeclaration,moduleSpecifier:null!=(s=S)?s:"@prisma/client",namedImports:[e.name].concat(v.map((function(e){return e.type})))}),h.addStatements((function(t){return c(t,o(e.documentation))})),h.addVariableStatement({declarationKind:r.VariableDeclarationKind.Const,isExported:!0,declarations:[{name:f(e.name),initializer:function(t){t.write("z.object(").inlineBlock((function(){e.fields.filter((function(e){return"object"!==e.kind})).forEach((function(e){c(t,o(e.documentation)),t.write(e.name+": "+u(e)).write(",").newLine()}))})).write(")")}}]});var b=e.fields.filter((function(e){return"object"===e.kind}));if(!1!==p&&b.length>0){var y=b.filter((function(t){return t.type!==e.name}));y.length>0&&h.addImportDeclaration({kind:r.StructureKind.ImportDeclaration,moduleSpecifier:"./index",namedImports:Array.from(new Set(y.flatMap((function(e){return["Complete"+e.type,m(e.type)]}))))}),h.addInterface({name:"Complete"+e.name,isExported:!0,extends:function(t){return t.write(e.name)},properties:b.map((function(e){return{name:e.name,type:"Complete"+e.type+(e.isList?"[]":"")+(e.isRequired?"":" | null")}}))}),h.addStatements((function(t){return c(t,["","/**"," * "+m(e.name)+" contains all relations on your model in addition to the scalars"," *"," * NOTE: Lazy required in case of potential circular dependencies within schema"," */"])})),h.addVariableStatement({declarationKind:r.VariableDeclarationKind.Const,isExported:!0,declarations:[{name:m(e.name),type:"z.ZodSchema<Complete"+e.name+">",initializer:function(t){t.write("z.lazy(() => "+f(e.name)+".extend(").inlineBlock((function(){b.forEach((function(e){c(t,o(e.documentation)),t.write(e.name+": "+u(e,m)).write(",").newLine()}))})).write("))")}}]})}h.formatText({indentSize:2,convertTabsToSpaces:!0,semicolons:i.SemicolonPreference.Remove})})),n.save()}}); | ||
"use strict";var e=require("@prisma/generator-helper"),t=require("typescript"),n=require("zod"),r=require("path"),i=require("ts-morph");function a(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var o=a(r),u=n.z.enum(["true","false"]).transform((function(e){return JSON.parse(e)})),l=n.z.object({relationModel:u.default("true").or(n.z.literal("default")),modelSuffix:n.z.string().default("Model"),modelCase:n.z.enum(["PascalCase","camelCase"]).default("PascalCase"),useDecimalJs:u.default("false"),imports:n.z.string().optional(),prismaJsonNullability:u.default("true")}),c=function(e,t,n){return void 0===n&&(n=!0),t.forEach((function(t){return e.write(t).conditionalNewLine(n)}))},s=function(e){var t=e.modelCase,n=e.modelSuffix,r=e.relationModel,i=function(e,r){return void 0===r&&(r=""),"camelCase"===t&&(e=e.slice(0,1).toLowerCase()+e.slice(1)),""+r+e+n};return{modelName:function(e){return i(e,"default"===r?"_":"")},relatedModelName:function(e){return i("default"===r?e.toString():"Related"+e.toString())}}},d=function(e){var t=[];if(e){var n=e.split("\n").filter((function(e){return!e.trimLeft().startsWith("@zod")}));n.length&&(t.push("/**"),n.forEach((function(e){return t.push(" * "+e)})),t.push(" */"))}return t},m=function(e){return e.split("\n").filter((function(e){return e.trimStart().startsWith("@zod")})).flatMap((function(e){return Array.from(e.matchAll(/\.([^().]+\(.*?\))/g),(function(e){return e.slice(1)})).flat()}))},f=function(e,t){void 0===t&&(t=function(e){return e.toString()});var n,r,i="z.unknown()",a=[""];if("scalar"===e.kind)switch(e.type){case"String":i="z.string()";break;case"Int":i="z.number()",a.push("int()");break;case"BigInt":i="z.bigint()";break;case"DateTime":i="z.date()";break;case"Float":case"Decimal":i="z.number()";break;case"Json":i="jsonSchema";break;case"Boolean":i="z.boolean()";break;case"Bytes":i="z.unknown()"}else"enum"===e.kind?i="z.nativeEnum("+e.type+")":"object"===e.kind&&(i=t(e.type));return e.isList&&a.push("array()"),e.documentation&&(i=null!=(n=null==(r=m(e.documentation).find((function(e){return e.startsWith("custom(")})))?void 0:r.slice(7).slice(0,-1))?n:i,a.push.apply(a,m(e.documentation).filter((function(e){return!e.startsWith("custom(")})))),e.isRequired||"Json"===e.type||a.push("nullish()"),""+i+a.join(".")};e.generatorHandler({onManifest:function(){return{version:"0.5.0",prettyName:"Zod Schemas",defaultOutput:"zod"}},onGenerate:function(e){var n=new i.Project,r=e.dmmf.datamodel.models,a=e.schemaPath,u=e.generator.output.value,m=e.otherGenerators.find((function(e){return"prisma-client-js"===e.provider.value})).output.value,p=l.safeParse(e.generator.config);if(!p.success)throw new Error("Incorrect config provided. Please check the values you provided and try again.");var h=p.data,z={clientPath:m,outputPath:u,schemaPath:a},S=n.createSourceFile(u+"/index.ts",{},{overwrite:!0});return function(e,t){e.forEach((function(e){return t.addExportDeclaration({moduleSpecifier:"./"+e.name.toLowerCase()})}))}(r,S),S.formatText({indentSize:2,convertTabsToSpaces:!0,semicolons:t.SemicolonPreference.Remove}),r.forEach((function(e){var r=n.createSourceFile(u+"/"+e.name.toLowerCase()+".ts",{},{overwrite:!0});(function(e,t,n,r){!function(e,t,n,r){var a=r.schemaPath,u=r.outputPath,l=r.clientPath,c=s(n).relatedModelName,d=[{kind:i.StructureKind.ImportDeclaration,namespaceImport:"z",moduleSpecifier:"zod"}];n.imports&&d.push({kind:i.StructureKind.ImportDeclaration,namespaceImport:"imports",moduleSpecifier:o.default.relative(u,o.default.resolve(o.default.dirname(a),n.imports))}),n.useDecimalJs&&e.fields.some((function(e){return"Decimal"===e.type}))&&d.push({kind:i.StructureKind.ImportDeclaration,namedImports:["Decimal"],moduleSpecifier:"decimal.js"});var m=e.fields.filter((function(e){return"enum"===e.kind})),f=e.fields.filter((function(e){return"object"===e.kind})),p=o.default.relative(u,l);if(m.length>0&&d.push({kind:i.StructureKind.ImportDeclaration,isTypeOnly:0===m.length,moduleSpecifier:p,namedImports:m.map((function(e){return e.type}))}),!1!==n.relationModel&&f.length>0){var h=f.filter((function(t){return t.type!==e.name}));h.length>0&&d.push({kind:i.StructureKind.ImportDeclaration,moduleSpecifier:"./index",namedImports:Array.from(new Set(h.flatMap((function(e){return["Complete"+e.type,c(e.type)]}))))})}t.addImportDeclarations(d)}(e,t,n,r),function(e,t,n,r){e.fields.some((function(e){return"Json"===e.type}))&&t.addStatements((function(e){e.newLine(),c(e,["// Helper schema for JSON fields","type Literal = boolean | number | string"+(n.prismaJsonNullability?"":"| null"),"type Json = Literal | { [key: string]: Json } | Json[]","const literalSchema = z.union([z.string(), z.number(), z.boolean()"+(n.prismaJsonNullability?"":", z.null()")+"])","const jsonSchema: z.ZodSchema<Json> = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]))"])})),n.useDecimalJs&&e.fields.some((function(e){return"Decimal"===e.type}))&&t.addStatements((function(e){e.newLine(),c(e,["// Helper schema for Decimal fields","z",".instanceof(Decimal)",".or(z.string())",".or(z.number())",".refine((value) => {"," try {"," return new Decimal(value);"," } catch (error) {"," return false;"," }","})",".transform((value) => new Decimal(value));"])}))}(e,t,n),function(e,t,n,r){var a=s(n);t.addVariableStatement({declarationKind:i.VariableDeclarationKind.Const,isExported:!0,leadingTrivia:function(e){return e.blankLineIfLastNot()},declarations:[{name:(0,a.modelName)(e.name),initializer:function(t){t.write("z.object(").inlineBlock((function(){e.fields.filter((function(e){return"object"!==e.kind})).forEach((function(e){c(t,d(e.documentation)),t.write(e.name+": "+f(e)).write(",").newLine()}))})).write(")")}}]})}(e,t,n),function(e,t){return e.fields.some((function(e){return"object"===e.kind}))&&!1!==t.relationModel}(e,n)&&function(e,t,n,r){var a=s(n),o=a.modelName,u=a.relatedModelName,l=e.fields.filter((function(e){return"object"===e.kind}));t.addInterface({name:"Complete"+e.name,isExported:!0,extends:["z.infer<typeof "+o(e.name)+">"],properties:l.map((function(e){return{name:e.name,type:"Complete"+e.type+(e.isList?"[]":"")+(e.isRequired?"":" | null")}}))}),t.addStatements((function(t){return c(t,["","/**"," * "+u(e.name)+" contains all relations on your model in addition to the scalars"," *"," * NOTE: Lazy required in case of potential circular dependencies within schema"," */"])})),t.addVariableStatement({declarationKind:i.VariableDeclarationKind.Const,isExported:!0,declarations:[{name:u(e.name),type:"z.ZodSchema<Complete"+e.name+">",initializer:function(t){t.write("z.lazy(() => "+o(e.name)+".extend(").inlineBlock((function(){l.forEach((function(e){c(t,d(e.documentation)),t.write(e.name+": "+f(e,u)).write(",").newLine()}))})).write("))")}}]})}(e,t,n)})(e,r,h,z),r.formatText({indentSize:2,convertTabsToSpaces:!0,semicolons:t.SemicolonPreference.Remove})})),n.save()}}); | ||
//# sourceMappingURL=zod-prisma.cjs.production.min.js.map |
@@ -1,7 +0,62 @@ | ||
import path from 'path'; | ||
import { generatorHandler } from '@prisma/generator-helper'; | ||
import { Project, StructureKind, VariableDeclarationKind } from 'ts-morph'; | ||
import { SemicolonPreference } from 'typescript'; | ||
import z from 'zod'; | ||
import { z } from 'zod'; | ||
import path from 'path'; | ||
import { StructureKind, VariableDeclarationKind, Project } from 'ts-morph'; | ||
var version = "0.5.0"; | ||
var configBoolean = /*#__PURE__*/z["enum"](['true', 'false']).transform(function (arg) { | ||
return JSON.parse(arg); | ||
}); | ||
var configSchema = /*#__PURE__*/z.object({ | ||
relationModel: /*#__PURE__*/configBoolean["default"]('true').or( /*#__PURE__*/z.literal('default')), | ||
modelSuffix: /*#__PURE__*/z.string()["default"]('Model'), | ||
modelCase: /*#__PURE__*/z["enum"](['PascalCase', 'camelCase'])["default"]('PascalCase'), | ||
useDecimalJs: /*#__PURE__*/configBoolean["default"]('false'), | ||
imports: /*#__PURE__*/z.string().optional(), | ||
prismaJsonNullability: /*#__PURE__*/configBoolean["default"]('true') | ||
}); | ||
var writeArray = function writeArray(writer, array, newLine) { | ||
if (newLine === void 0) { | ||
newLine = true; | ||
} | ||
return array.forEach(function (line) { | ||
return writer.write(line).conditionalNewLine(newLine); | ||
}); | ||
}; | ||
var useModelNames = function useModelNames(_ref) { | ||
var modelCase = _ref.modelCase, | ||
modelSuffix = _ref.modelSuffix, | ||
relationModel = _ref.relationModel; | ||
var formatModelName = function formatModelName(name, prefix) { | ||
if (prefix === void 0) { | ||
prefix = ''; | ||
} | ||
if (modelCase === 'camelCase') { | ||
name = name.slice(0, 1).toLowerCase() + name.slice(1); | ||
} | ||
return "" + prefix + name + modelSuffix; | ||
}; | ||
return { | ||
modelName: function modelName(name) { | ||
return formatModelName(name, relationModel === 'default' ? '_' : ''); | ||
}, | ||
relatedModelName: function relatedModelName(name) { | ||
return formatModelName(relationModel === 'default' ? name.toString() : "Related" + name.toString()); | ||
} | ||
}; | ||
}; | ||
var needsRelatedModel = function needsRelatedModel(model, config) { | ||
return model.fields.some(function (field) { | ||
return field.kind === 'object'; | ||
}) && config.relationModel !== false; | ||
}; | ||
var getJSDocs = function getJSDocs(docString) { | ||
@@ -26,10 +81,24 @@ var lines = []; | ||
}; | ||
var computeModifiers = function computeModifiers(docString) { | ||
var getZodDocElements = function getZodDocElements(docString) { | ||
return docString.split('\n').filter(function (line) { | ||
return line.trimLeft().startsWith('@zod'); | ||
}).map(function (line) { | ||
return line.trim().split('@zod.').slice(-1)[0]; | ||
return line.trimStart().startsWith('@zod'); | ||
}).flatMap(function (line) { | ||
return Array.from(line.matchAll(/\.([^().]+\(.*?\))/g), function (m) { | ||
return m.slice(1); | ||
}).flat(); | ||
}); | ||
}; | ||
var computeCustomSchema = function computeCustomSchema(docString) { | ||
var _getZodDocElements$fi; | ||
return (_getZodDocElements$fi = getZodDocElements(docString).find(function (modifier) { | ||
return modifier.startsWith('custom('); | ||
})) == null ? void 0 : _getZodDocElements$fi.slice(7).slice(0, -1); | ||
}; | ||
var computeModifiers = function computeModifiers(docString) { | ||
return getZodDocElements(docString).filter(function (each) { | ||
return !each.startsWith('custom('); | ||
}); | ||
}; | ||
var getZodConstructor = function getZodConstructor(field, getRelatedModelName) { | ||
@@ -73,3 +142,3 @@ if (getRelatedModelName === void 0) { | ||
case 'Json': | ||
zodType = 'z.any()'; | ||
zodType = 'jsonSchema'; | ||
break; | ||
@@ -80,2 +149,3 @@ | ||
break; | ||
// TODO: Proper type for bytes fields | ||
@@ -92,186 +162,221 @@ case 'Bytes': | ||
if (!field.isRequired) extraModifiers.push('nullable()'); | ||
if (field.isList) extraModifiers.push('array()'); | ||
if (field.documentation) { | ||
var _computeCustomSchema; | ||
zodType = (_computeCustomSchema = computeCustomSchema(field.documentation)) != null ? _computeCustomSchema : zodType; | ||
extraModifiers.push.apply(extraModifiers, computeModifiers(field.documentation)); | ||
} | ||
if (!field.isRequired && field.type !== 'Json') extraModifiers.push('nullish()'); | ||
return "" + zodType + extraModifiers.join('.'); | ||
}; | ||
var writeArray = function writeArray(writer, array, newLine) { | ||
if (newLine === void 0) { | ||
newLine = true; | ||
var writeImportsForModel = function writeImportsForModel(model, sourceFile, config, _ref) { | ||
var schemaPath = _ref.schemaPath, | ||
outputPath = _ref.outputPath, | ||
clientPath = _ref.clientPath; | ||
var _useModelNames = useModelNames(config), | ||
relatedModelName = _useModelNames.relatedModelName; | ||
var importList = [{ | ||
kind: StructureKind.ImportDeclaration, | ||
namespaceImport: 'z', | ||
moduleSpecifier: 'zod' | ||
}]; | ||
if (config.imports) { | ||
importList.push({ | ||
kind: StructureKind.ImportDeclaration, | ||
namespaceImport: 'imports', | ||
moduleSpecifier: path.relative(outputPath, path.resolve(path.dirname(schemaPath), config.imports)) | ||
}); | ||
} | ||
return array.forEach(function (line) { | ||
return writer.write(line).conditionalNewLine(newLine); | ||
if (config.useDecimalJs && model.fields.some(function (f) { | ||
return f.type === 'Decimal'; | ||
})) { | ||
importList.push({ | ||
kind: StructureKind.ImportDeclaration, | ||
namedImports: ['Decimal'], | ||
moduleSpecifier: 'decimal.js' | ||
}); | ||
} | ||
var enumFields = model.fields.filter(function (f) { | ||
return f.kind === 'enum'; | ||
}); | ||
var relationFields = model.fields.filter(function (f) { | ||
return f.kind === 'object'; | ||
}); | ||
var relativePath = path.relative(outputPath, clientPath); | ||
if (enumFields.length > 0) { | ||
importList.push({ | ||
kind: StructureKind.ImportDeclaration, | ||
isTypeOnly: enumFields.length === 0, | ||
moduleSpecifier: relativePath, | ||
namedImports: enumFields.map(function (f) { | ||
return f.type; | ||
}) | ||
}); | ||
} | ||
if (config.relationModel !== false && relationFields.length > 0) { | ||
var filteredFields = relationFields.filter(function (f) { | ||
return f.type !== model.name; | ||
}); | ||
if (filteredFields.length > 0) { | ||
importList.push({ | ||
kind: StructureKind.ImportDeclaration, | ||
moduleSpecifier: './index', | ||
namedImports: Array.from(new Set(filteredFields.flatMap(function (f) { | ||
return ["Complete" + f.type, relatedModelName(f.type)]; | ||
}))) | ||
}); | ||
} | ||
} | ||
sourceFile.addImportDeclarations(importList); | ||
}; | ||
var writeTypeSpecificSchemas = function writeTypeSpecificSchemas(model, sourceFile, config, _prismaOptions) { | ||
if (model.fields.some(function (f) { | ||
return f.type === 'Json'; | ||
})) { | ||
sourceFile.addStatements(function (writer) { | ||
writer.newLine(); | ||
writeArray(writer, ['// Helper schema for JSON fields', "type Literal = boolean | number | string" + (config.prismaJsonNullability ? '' : '| null'), 'type Json = Literal | { [key: string]: Json } | Json[]', "const literalSchema = z.union([z.string(), z.number(), z.boolean()" + (config.prismaJsonNullability ? '' : ', z.null()') + "])", 'const jsonSchema: z.ZodSchema<Json> = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]))']); | ||
}); | ||
} | ||
var configSchema = /*#__PURE__*/z.object({ | ||
relationModel: /*#__PURE__*/z["enum"](['default', 'true', 'false'])["default"]('true').transform(function (val) { | ||
switch (val) { | ||
case 'default': | ||
return val; | ||
if (config.useDecimalJs && model.fields.some(function (f) { | ||
return f.type === 'Decimal'; | ||
})) { | ||
sourceFile.addStatements(function (writer) { | ||
writer.newLine(); | ||
writeArray(writer, ['// Helper schema for Decimal fields', 'z', '.instanceof(Decimal)', '.or(z.string())', '.or(z.number())', '.refine((value) => {', ' try {', ' return new Decimal(value);', ' } catch (error) {', ' return false;', ' }', '})', '.transform((value) => new Decimal(value));']); | ||
}); | ||
} // TODO: Add Decimal | ||
case 'true': | ||
return true; | ||
}; | ||
var generateSchemaForModel = function generateSchemaForModel(model, sourceFile, config, _prismaOptions) { | ||
var _useModelNames2 = useModelNames(config), | ||
modelName = _useModelNames2.modelName; | ||
case 'false': | ||
return false; | ||
} | ||
}), | ||
modelSuffix: /*#__PURE__*/z.string()["default"]('Model'), | ||
modelCase: /*#__PURE__*/z["enum"](['PascalCase', 'camelCase'])["default"]('PascalCase') | ||
}); | ||
sourceFile.addVariableStatement({ | ||
declarationKind: VariableDeclarationKind.Const, | ||
isExported: true, | ||
leadingTrivia: function leadingTrivia(writer) { | ||
return writer.blankLineIfLastNot(); | ||
}, | ||
declarations: [{ | ||
name: modelName(model.name), | ||
initializer: function initializer(writer) { | ||
writer.write('z.object(').inlineBlock(function () { | ||
model.fields.filter(function (f) { | ||
return f.kind !== 'object'; | ||
}).forEach(function (field) { | ||
writeArray(writer, getJSDocs(field.documentation)); | ||
writer.write(field.name + ": " + getZodConstructor(field)).write(',').newLine(); | ||
}); | ||
}).write(')'); | ||
} | ||
}] | ||
}); | ||
}; | ||
var generateRelatedSchemaForModel = function generateRelatedSchemaForModel(model, sourceFile, config, _prismaOptions) { | ||
var _useModelNames3 = useModelNames(config), | ||
modelName = _useModelNames3.modelName, | ||
relatedModelName = _useModelNames3.relatedModelName; | ||
var relationFields = model.fields.filter(function (f) { | ||
return f.kind === 'object'; | ||
}); | ||
sourceFile.addInterface({ | ||
name: "Complete" + model.name, | ||
isExported: true, | ||
"extends": ["z.infer<typeof " + modelName(model.name) + ">"], | ||
properties: relationFields.map(function (f) { | ||
return { | ||
name: f.name, | ||
type: "Complete" + f.type + (f.isList ? '[]' : '') + (!f.isRequired ? ' | null' : '') | ||
}; | ||
}) | ||
}); | ||
sourceFile.addStatements(function (writer) { | ||
return writeArray(writer, ['', '/**', " * " + relatedModelName(model.name) + " contains all relations on your model in addition to the scalars", ' *', ' * NOTE: Lazy required in case of potential circular dependencies within schema', ' */']); | ||
}); | ||
sourceFile.addVariableStatement({ | ||
declarationKind: VariableDeclarationKind.Const, | ||
isExported: true, | ||
declarations: [{ | ||
name: relatedModelName(model.name), | ||
type: "z.ZodSchema<Complete" + model.name + ">", | ||
initializer: function initializer(writer) { | ||
writer.write("z.lazy(() => " + modelName(model.name) + ".extend(").inlineBlock(function () { | ||
relationFields.forEach(function (field) { | ||
writeArray(writer, getJSDocs(field.documentation)); | ||
writer.write(field.name + ": " + getZodConstructor(field, relatedModelName)).write(',').newLine(); | ||
}); | ||
}).write('))'); | ||
} | ||
}] | ||
}); | ||
}; | ||
var populateModelFile = function populateModelFile(model, sourceFile, config, prismaOptions) { | ||
writeImportsForModel(model, sourceFile, config, prismaOptions); | ||
writeTypeSpecificSchemas(model, sourceFile, config); | ||
generateSchemaForModel(model, sourceFile, config); | ||
if (needsRelatedModel(model, config)) generateRelatedSchemaForModel(model, sourceFile, config); | ||
}; | ||
var generateBarrelFile = function generateBarrelFile(models, indexFile) { | ||
models.forEach(function (model) { | ||
return indexFile.addExportDeclaration({ | ||
moduleSpecifier: "./" + model.name.toLowerCase() | ||
}); | ||
}); | ||
}; | ||
// @ts-ignore Importing package.json for automated synchronization of version numbers | ||
generatorHandler({ | ||
onManifest: function onManifest() { | ||
return { | ||
version: version, | ||
prettyName: 'Zod Schemas', | ||
defaultOutput: 'zod', | ||
version: '0.2.1' | ||
defaultOutput: 'zod' | ||
}; | ||
}, | ||
onGenerate: function onGenerate(options) { | ||
var project = new Project({ | ||
skipAddingFilesFromTsConfig: true | ||
}); | ||
var project = new Project(); | ||
var models = options.dmmf.datamodel.models; | ||
var schemaPath = options.schemaPath; | ||
var outputPath = options.generator.output.value; | ||
var models = options.dmmf.datamodel.models; | ||
var prismaClient = options.otherGenerators.find(function (each) { | ||
var clientPath = options.otherGenerators.find(function (each) { | ||
return each.provider.value === 'prisma-client-js'; | ||
}); | ||
var parsedConfig = configSchema.safeParse(options.generator.config); | ||
if (!parsedConfig.success) throw new Error('Incorrect config provided. Please check the values you provided and try again.'); | ||
var _parsedConfig$data = parsedConfig.data, | ||
relationModel = _parsedConfig$data.relationModel, | ||
modelSuffix = _parsedConfig$data.modelSuffix, | ||
modelCase = _parsedConfig$data.modelCase; | ||
var formatModelName = function formatModelName(name, prefix) { | ||
if (prefix === void 0) { | ||
prefix = ''; | ||
} | ||
if (modelCase === 'camelCase') { | ||
name = name.slice(0, 1).toLowerCase() + name.slice(1); | ||
} | ||
return "" + prefix + name + modelSuffix; | ||
}).output.value; | ||
var results = configSchema.safeParse(options.generator.config); | ||
if (!results.success) throw new Error('Incorrect config provided. Please check the values you provided and try again.'); | ||
var config = results.data; | ||
var prismaOptions = { | ||
clientPath: clientPath, | ||
outputPath: outputPath, | ||
schemaPath: schemaPath | ||
}; | ||
var indexSource = project.createSourceFile(outputPath + "/index.ts", {}, { | ||
var indexFile = project.createSourceFile(outputPath + "/index.ts", {}, { | ||
overwrite: true | ||
}); | ||
generateBarrelFile(models, indexFile); | ||
indexFile.formatText({ | ||
indentSize: 2, | ||
convertTabsToSpaces: true, | ||
semicolons: SemicolonPreference.Remove | ||
}); | ||
models.forEach(function (model) { | ||
var _prismaClient$output, _relativePath; | ||
indexSource.addExportDeclaration({ | ||
moduleSpecifier: "./" + model.name.toLowerCase() | ||
}); | ||
var modelName = function modelName(name) { | ||
return formatModelName(name, relationModel === 'default' ? '_' : ''); | ||
}; | ||
var relatedModelName = function relatedModelName(name) { | ||
return formatModelName(relationModel === 'default' ? name.toString() : "Related" + name.toString()); | ||
}; | ||
var sourceFile = project.createSourceFile(outputPath + "/" + model.name.toLowerCase() + ".ts", { | ||
statements: [{ | ||
kind: StructureKind.ImportDeclaration, | ||
namespaceImport: 'z', | ||
moduleSpecifier: 'zod' | ||
}] | ||
}, { | ||
var sourceFile = project.createSourceFile(outputPath + "/" + model.name.toLowerCase() + ".ts", {}, { | ||
overwrite: true | ||
}); | ||
var enumFields = model.fields.filter(function (f) { | ||
return f.kind === 'enum'; | ||
}); | ||
var relativePath = prismaClient != null && (_prismaClient$output = prismaClient.output) != null && _prismaClient$output.value ? path.relative(outputPath, prismaClient.output.value) : null; | ||
if (relativePath && !(relativePath.startsWith('./') || relativePath.startsWith('../'))) relativePath = "./" + relativePath; | ||
sourceFile.addImportDeclaration({ | ||
kind: StructureKind.ImportDeclaration, | ||
moduleSpecifier: (_relativePath = relativePath) != null ? _relativePath : '@prisma/client', | ||
namedImports: [model.name].concat(enumFields.map(function (f) { | ||
return f.type; | ||
})) | ||
}); | ||
sourceFile.addStatements(function (writer) { | ||
return writeArray(writer, getJSDocs(model.documentation)); | ||
}); | ||
sourceFile.addVariableStatement({ | ||
declarationKind: VariableDeclarationKind.Const, | ||
isExported: true, | ||
declarations: [{ | ||
name: modelName(model.name), | ||
initializer: function initializer(writer) { | ||
writer.write('z.object(').inlineBlock(function () { | ||
model.fields.filter(function (f) { | ||
return f.kind !== 'object'; | ||
}).forEach(function (field) { | ||
writeArray(writer, getJSDocs(field.documentation)); | ||
writer.write(field.name + ": " + getZodConstructor(field)).write(',').newLine(); | ||
}); | ||
}).write(')'); | ||
} | ||
}] | ||
}); | ||
var relationFields = model.fields.filter(function (f) { | ||
return f.kind === 'object'; | ||
}); | ||
if (relationModel !== false && relationFields.length > 0) { | ||
var filteredFields = relationFields.filter(function (f) { | ||
return f.type !== model.name; | ||
}); | ||
if (filteredFields.length > 0) { | ||
sourceFile.addImportDeclaration({ | ||
kind: StructureKind.ImportDeclaration, | ||
moduleSpecifier: './index', | ||
namedImports: Array.from(new Set(filteredFields.flatMap(function (f) { | ||
return ["Complete" + f.type, relatedModelName(f.type)]; | ||
}))) | ||
}); | ||
} | ||
sourceFile.addInterface({ | ||
name: "Complete" + model.name, | ||
isExported: true, | ||
"extends": function _extends(writer) { | ||
return writer.write(model.name); | ||
}, | ||
properties: relationFields.map(function (f) { | ||
return { | ||
name: f.name, | ||
type: "Complete" + f.type + (f.isList ? '[]' : '') + (!f.isRequired ? ' | null' : '') | ||
}; | ||
}) | ||
}); | ||
sourceFile.addStatements(function (writer) { | ||
return writeArray(writer, ['', '/**', " * " + relatedModelName(model.name) + " contains all relations on your model in addition to the scalars", ' *', ' * NOTE: Lazy required in case of potential circular dependencies within schema', ' */']); | ||
}); | ||
sourceFile.addVariableStatement({ | ||
declarationKind: VariableDeclarationKind.Const, | ||
isExported: true, | ||
declarations: [{ | ||
name: relatedModelName(model.name), | ||
type: "z.ZodSchema<Complete" + model.name + ">", | ||
initializer: function initializer(writer) { | ||
writer.write("z.lazy(() => " + modelName(model.name) + ".extend(").inlineBlock(function () { | ||
relationFields.forEach(function (field) { | ||
writeArray(writer, getJSDocs(field.documentation)); | ||
writer.write(field.name + ": " + getZodConstructor(field, relatedModelName)).write(',').newLine(); | ||
}); | ||
}).write('))'); | ||
} | ||
}] | ||
}); | ||
} | ||
populateModelFile(model, sourceFile, config, prismaOptions); | ||
sourceFile.formatText({ | ||
@@ -278,0 +383,0 @@ indentSize: 2, |
{ | ||
"version": "0.3.2", | ||
"name": "zod-prisma", | ||
"version": "0.5.0", | ||
"description": "A Prisma generator that creates Zod schemas for all of your models", | ||
"license": "MIT", | ||
"description": "A Prisma generator that creates Zod schemas for all of your models", | ||
"author": "Carter Grimmeisen", | ||
"homepage": "https://github.com/CarterGrimmeisen/zod-prisma#readme", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/CarterGrimmeisen/zod-prisma.git" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/CarterGrimmeisen/zod-prisma/issues" | ||
}, | ||
"main": "dist/index.js", | ||
"module": "dist/zod-prisma.esm.js", | ||
"typings": "dist/index.d.ts", | ||
@@ -10,2 +21,7 @@ "bin": { | ||
}, | ||
"keywords": [ | ||
"zod", | ||
"prisma", | ||
"generator" | ||
], | ||
"files": [ | ||
@@ -16,43 +32,36 @@ "dist", | ||
], | ||
"engines": { | ||
"node": ">=14" | ||
}, | ||
"scripts": { | ||
"start": "tsdx watch", | ||
"build": "tsdx build", | ||
"test": "tsdx build && tsdx test", | ||
"lint": "tsdx lint src test/*.test.ts test/**/*.test.ts", | ||
"prepublish": "yarn build" | ||
"build": "dts build", | ||
"lint": "tsc --noEmit && dts lint src --ignore-pattern src/test/functional", | ||
"prepare": "husky install", | ||
"prepublish": "dts build", | ||
"start": "dts watch", | ||
"test": "dts test --maxWorkers=4 --verbose" | ||
}, | ||
"peerDependencies": { | ||
"prisma": "^3.4.2", | ||
"zod": "^3.0.0" | ||
}, | ||
"husky": { | ||
"hooks": { | ||
"pre-commit": "tsdx lint" | ||
} | ||
}, | ||
"prettier": { | ||
"printWidth": 80, | ||
"printWidth": 100, | ||
"semi": false, | ||
"singleQuote": true, | ||
"tabWidth": 4, | ||
"trailingComma": "es5", | ||
"tabWidth": 4, | ||
"useTabs": true | ||
}, | ||
"name": "zod-prisma", | ||
"author": "Carter Grimmeisen", | ||
"module": "dist/zod-prisma.esm.js", | ||
"resolutions": { | ||
"eslint": "^7.17.0", | ||
"typescript": "4.5.4" | ||
"eslintConfig": { | ||
"rules": { | ||
"react-hooks/rules-of-hooks": "off" | ||
} | ||
}, | ||
"jest": { | ||
"testEnvironment": "node" | ||
}, | ||
"dependencies": { | ||
"@prisma/generator-helper": "~3.7.0", | ||
"ts-morph": "^13.0.2" | ||
}, | ||
"devDependencies": { | ||
"@types/fs-extra": "^9.0.11", | ||
"@typescript-eslint/eslint-plugin": "^5.7.0", | ||
"@typescript-eslint/parser": "^5.7.0", | ||
"eslint": "^7.17.0", | ||
"eslint-config-react-app": "^6.0.0", | ||
"eslint-plugin-prettier": "^4.0.0", | ||
"@prisma/client": "~3.7.0", | ||
"@prisma/sdk": "~3.7.0", | ||
"@tsconfig/recommended": "^1.0.1", | ||
"@types/fs-extra": "^9.0.13", | ||
"dts-cli": "^1.1.5", | ||
"execa": "^5.1.0", | ||
@@ -63,13 +72,20 @@ "fast-glob": "^3.2.5", | ||
"jest-mock-extended": "^2.0.4", | ||
"prettier": "^2.3.0", | ||
"prisma": "^3.4.2", | ||
"tsdx": "^0.14.1", | ||
"tslib": "^2.2.0", | ||
"tslib": "^2.3.1", | ||
"typescript": "^4.5.4", | ||
"zod": "^3.11.6" | ||
}, | ||
"dependencies": { | ||
"@prisma/generator-helper": "^3.3.0", | ||
"ts-morph": "^13.0.2" | ||
"peerDependencies": { | ||
"decimal.js": "^10.0.0", | ||
"prisma": "^3.0.0", | ||
"zod": "^3.0.0" | ||
}, | ||
"peerDependenciesMeta": { | ||
"decimal.js": { | ||
"optional": true | ||
} | ||
}, | ||
"engines": { | ||
"node": ">=14" | ||
} | ||
} |
185
README.md
@@ -13,4 +13,2 @@ <!-- | ||
<!-- PROJECT SHIELDS --> | ||
@@ -24,2 +22,3 @@ <!-- | ||
--> | ||
[![NPM][npm-shield]][npm-url] | ||
@@ -32,4 +31,2 @@ [![Contributors][contributors-shield]][contributors-url] | ||
<!-- PROJECT LOGO --> | ||
@@ -41,5 +38,3 @@ <br /> | ||
</a> | ||
<h3 align="center">Zod Prisma</h3> | ||
<p align="center"> | ||
@@ -51,3 +46,3 @@ A custom prisma generator that creates Zod schemas from your Prisma model. | ||
<br /> | ||
<a href="https://github.com/CarterGrimmeisen/zod-prisma/blob/main/test/functional">View Demo</a> | ||
<a href="https://github.com/CarterGrimmeisen/zod-prisma/blob/main/src/test/functional">View Demo</a> | ||
· | ||
@@ -60,4 +55,2 @@ <a href="https://github.com/CarterGrimmeisen/zod-prisma/issues">Report Bug</a> | ||
<!-- TABLE OF CONTENTS --> | ||
@@ -84,2 +77,9 @@ <details open="open"> | ||
<li><a href="#extending-zod-fields">Extending Zod Fields</a></li> | ||
<li> | ||
<a href="#importing-helpers">Importing Helpers</a> | ||
<ul> | ||
<li><a href="#custom-zod-schema">Custom Zod Schemas</a></li> | ||
</ul> | ||
</li> | ||
<li><a href="#json-fields">JSON Fields</a></li> | ||
</ul> | ||
@@ -95,5 +95,4 @@ </li> | ||
<!-- ABOUT THE PROJECT --> | ||
<!-- ABOUT THE PROJECT --> | ||
## About The Project | ||
@@ -106,12 +105,10 @@ | ||
### Built With | ||
* [TSDX](https://github.com/formium/tsdx) | ||
* [Zod](https://github.com/colinhacks/zod) | ||
* [Based on this gist](https://gist.github.com/deckchairlabs/8a11c33311c01273deec7e739417dbc9) | ||
- [dts-cli](https://github.com/weiran-zsd/dts-cli) | ||
- [Zod](https://github.com/colinhacks/zod) | ||
- [Based on this gist](https://gist.github.com/deckchairlabs/8a11c33311c01273deec7e739417dbc9) | ||
<!-- GETTING STARTED --> | ||
<!-- GETTING STARTED --> | ||
## Getting Started | ||
@@ -124,36 +121,50 @@ | ||
This project utilizes yarn and if you plan on contributing, you should too. | ||
* npm | ||
```sh | ||
npm install -g yarn | ||
``` | ||
```sh | ||
npm install -g yarn | ||
``` | ||
### Installation | ||
1. Add zod-prisma as a dev dependency | ||
0. **Ensure your tsconfig.json enables the compiler's strict mode.** | ||
**Zod requires it and so do we, you will experience TS errors without strict mode enabled** | ||
1. Add zod-prisma as a dev dependency | ||
```sh | ||
yarn add -D zod-prisma # Not yet published | ||
yarn add -D zod-prisma | ||
``` | ||
2. Add the zod-prisma generator to your schema.prisma | ||
```graphql | ||
2. Add the zod-prisma generator to your schema.prisma | ||
```prisma | ||
generator zod { | ||
provider = "zod-prisma" | ||
output = "./zod" | ||
relationModel = "default" # Do not export model without relations. | ||
# relationModel = true # Create and export both plain and related models. | ||
# relationModel = false # Do not generate related model | ||
provider = "zod-prisma" | ||
output = "./zod" // (default) the directory where generated zod schemas will be saved | ||
modelCase = "PascalCase" # (default) Output models using pascal case (ex. UserModel, PostModel) | ||
# modelCase = "camelCase" # Output models using camel case (ex. userModel, postModel) | ||
modelSuffix = "Model" # (default) Suffix to apply to your prisma models when naming Zod schemas | ||
relationModel = true // (default) Create and export both plain and related models. | ||
// relationModel = "default" // Do not export model without relations. | ||
// relationModel = false // Do not generate related model | ||
modelCase = "PascalCase" // (default) Output models using pascal case (ex. UserModel, PostModel) | ||
// modelCase = "camelCase" // Output models using camel case (ex. userModel, postModel) | ||
modelSuffix = "Model" // (default) Suffix to apply to your prisma models when naming Zod schemas | ||
// useDecimalJs = false // (default) represent the prisma Decimal type using as a JS number | ||
useDecimalJs = true // represent the prisma Decimal type using Decimal.js (as Prisma does) | ||
imports = null // (default) will import the referenced file in generated schemas to be used via imports.someExportedVariable | ||
// https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-by-null-values | ||
prismaJsonNullability = true // (default) uses prisma's scheme for JSON field nullability | ||
// prismaJsonNullability = false // allows null assignment to optional JSON fields | ||
} | ||
``` | ||
3. Run `npx prisma generate` to generate your zod schemas | ||
4. Import the generated schemas form your selected output location | ||
3. Run `npx prisma generate` or `yarn prisma generate` to generate your zod schemas | ||
4. Import the generated schemas form your selected output location | ||
<!-- USAGE EXAMPLES --> | ||
<!-- USAGE EXAMPLES --> | ||
## Usage | ||
@@ -168,3 +179,3 @@ | ||
```graphql | ||
```prisma | ||
model Post { | ||
@@ -187,15 +198,15 @@ /// The unique identifier for the post | ||
export const PostModel = z.object({ | ||
/** | ||
* The unique identifier for the post | ||
* @default {Generated by database} | ||
*/ | ||
id: z.string().uuid(), | ||
/** | ||
* A brief title that describes the contents of the post | ||
*/ | ||
title: z.string(), | ||
/** | ||
* The actual contents of the post. | ||
*/ | ||
contents: z.string(), | ||
/** | ||
* The unique identifier for the post | ||
* @default {Generated by database} | ||
*/ | ||
id: z.string().uuid(), | ||
/** | ||
* A brief title that describes the contents of the post | ||
*/ | ||
title: z.string(), | ||
/** | ||
* The actual contents of the post. | ||
*/ | ||
contents: z.string(), | ||
}) | ||
@@ -206,6 +217,6 @@ ``` | ||
You can also use the `@zod` keyword in rich-comments in the Prisma schema | ||
You can also use the `@zod` keyword in rich-comments in the Prisma schema | ||
to extend your Zod schema fields: | ||
```graphql | ||
```prisma | ||
model Post { | ||
@@ -225,8 +236,48 @@ id String @id @default(uuid()) /// @zod.uuid() | ||
export const PostModel = z.object({ | ||
id: z.string().uuid(), | ||
title: z.string().max(255, { message: "The title must be shorter than 256 characters" }), | ||
contents: z.string().max(10240), | ||
id: z.string().uuid(), | ||
title: z.string().max(255, { message: 'The title must be shorter than 256 characters' }), | ||
contents: z.string().max(10240), | ||
}) | ||
``` | ||
### Importing Helpers | ||
Sometimes its useful to define a custom Zod preprocessor or transformer for your data. | ||
zod-prisma enables you to reuse these by importing them via a config options. For example: | ||
```prisma | ||
generator zod { | ||
provider = "zod-prisma" | ||
output = "./zod" | ||
imports = "../src/zod-schemas" | ||
} | ||
model User { | ||
username String /// @zod.refine(imports.isValidUsername) | ||
} | ||
``` | ||
The referenced file can then be used by simply referring to exported members via `imports.whateverExport`. | ||
The generated zod schema files will now include a namespaced import like the following. | ||
```typescript | ||
import * as imports from '../../src/zod-schemas' | ||
``` | ||
#### Custom Zod Schema | ||
In conjunction with this import option, you may want to utilize an entirely custom zod schema for a field. | ||
This can be accomplished by using the special comment directive `@zod.custom()`. | ||
By specifying the custom schema within the parentheses you can replace the autogenerated type that would normally be assigned to the field. | ||
> For instance if you wanted to use `z.preprocess` | ||
### JSON Fields | ||
JSON fields in Prisma disallow null values. This is to disambiguate between setting a field's value to NULL in the database and having | ||
a value of null stored in the JSON. In accordance with this zod-prisma will default to disallowing null values, even if your JSON field is optional. | ||
If you would like to revert this behavior and allow null assignment to JSON fields, | ||
you can set `prismaJsonNullability` to `false` in the generator options. | ||
## Examples | ||
@@ -236,7 +287,6 @@ | ||
_For examples, please refer to the [Examples Directory](https://github.com/CarterGrimmeisen/zod-prisma/blob/main/examples) or the [Functional Tests](https://github.com/CarterGrimmeisen/zod-prisma/blob/main/test/functional)_ | ||
_For examples, please refer to the [Examples Directory](https://github.com/CarterGrimmeisen/zod-prisma/blob/main/examples) or the [Functional Tests](https://github.com/CarterGrimmeisen/zod-prisma/blob/main/src/test/functional)_ | ||
<!-- ROADMAP --> | ||
<!-- ROADMAP --> | ||
## Roadmap | ||
@@ -246,5 +296,4 @@ | ||
<!-- CONTRIBUTING --> | ||
<!-- CONTRIBUTING --> | ||
## Contributing | ||
@@ -260,5 +309,4 @@ | ||
<!-- LICENSE --> | ||
<!-- LICENSE --> | ||
## License | ||
@@ -268,5 +316,4 @@ | ||
<!-- CONTACT --> | ||
<!-- CONTACT --> | ||
## Contact | ||
@@ -278,7 +325,5 @@ | ||
<!-- MARKDOWN LINKS & IMAGES --> | ||
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links --> | ||
[npm-shield]: https://img.shields.io/npm/v/zod-prisma?style=for-the-badge | ||
@@ -295,2 +340,2 @@ [npm-url]: https://www.npmjs.com/package/zod-prisma | ||
[license-shield]: https://img.shields.io/github/license/CarterGrimmeisen/zod-prisma.svg?style=for-the-badge | ||
[license-url]: https://github.com/CarterGrimmeisen/zod-prisma/blob/main/LICENSE | ||
[license-url]: https://github.com/CarterGrimmeisen/zod-prisma/blob/main/LICENSE |
@@ -5,5 +5,3 @@ export const getJSDocs = (docString?: string) => { | ||
if (docString) { | ||
const docLines = docString | ||
.split('\n') | ||
.filter((dL) => !dL.trimLeft().startsWith('@zod')) | ||
const docLines = docString.split('\n').filter((dL) => !dL.trimLeft().startsWith('@zod')) | ||
@@ -20,7 +18,19 @@ if (docLines.length) { | ||
export const computeModifiers = (docString: string) => { | ||
return docString | ||
export const getZodDocElements = (docString: string) => | ||
docString | ||
.split('\n') | ||
.filter((line) => line.trimLeft().startsWith('@zod')) | ||
.map((line) => line.trim().split('@zod.').slice(-1)[0]) | ||
.filter((line) => line.trimStart().startsWith('@zod')) | ||
.flatMap((line) => | ||
Array.from(line.matchAll(/\.([^().]+\(.*?\))/g), (m) => m.slice(1)).flat() | ||
) | ||
export const computeCustomSchema = (docString: string) => { | ||
return getZodDocElements(docString) | ||
.find((modifier) => modifier.startsWith('custom(')) | ||
?.slice(7) | ||
.slice(0, -1) | ||
} | ||
export const computeModifiers = (docString: string) => { | ||
return getZodDocElements(docString).filter((each) => !each.startsWith('custom(')) | ||
} |
256
src/index.ts
@@ -1,50 +0,31 @@ | ||
import path from 'path' | ||
import { generatorHandler, DMMF } from '@prisma/generator-helper' | ||
import { Project, StructureKind, VariableDeclarationKind } from 'ts-morph' | ||
// @ts-ignore Importing package.json for automated synchronization of version numbers | ||
import { version } from '../package.json' | ||
import { generatorHandler } from '@prisma/generator-helper' | ||
import { SemicolonPreference } from 'typescript' | ||
import { getJSDocs } from './docs' | ||
import { getZodConstructor } from './types' | ||
import { writeArray } from './util' | ||
import z from 'zod' | ||
import { configSchema, PrismaOptions } from './config' | ||
import { populateModelFile, generateBarrelFile } from './generator' | ||
import { Project } from 'ts-morph' | ||
const configSchema = z.object({ | ||
relationModel: z | ||
.enum(['default', 'true', 'false']) | ||
.default('true') | ||
.transform((val) => { | ||
switch (val) { | ||
case 'default': | ||
return val | ||
case 'true': | ||
return true | ||
case 'false': | ||
return false | ||
} | ||
}), | ||
modelSuffix: z.string().default('Model'), | ||
modelCase: z.enum(['PascalCase', 'camelCase']).default('PascalCase'), | ||
}) | ||
generatorHandler({ | ||
onManifest() { | ||
return { | ||
version, | ||
prettyName: 'Zod Schemas', | ||
defaultOutput: 'zod', | ||
version: '0.2.1', | ||
} | ||
}, | ||
onGenerate(options) { | ||
const project = new Project({ | ||
skipAddingFilesFromTsConfig: true, | ||
}) | ||
const project = new Project() | ||
const outputPath = options.generator.output!.value | ||
const models = options.dmmf.datamodel.models | ||
const prismaClient = options.otherGenerators.find( | ||
const { schemaPath } = options | ||
const outputPath = options.generator.output!.value | ||
const clientPath = options.otherGenerators.find( | ||
(each) => each.provider.value === 'prisma-client-js' | ||
) | ||
)!.output!.value! | ||
const parsedConfig = configSchema.safeParse(options.generator.config) | ||
if (!parsedConfig.success) | ||
const results = configSchema.safeParse(options.generator.config) | ||
if (!results.success) | ||
throw new Error( | ||
@@ -54,205 +35,32 @@ 'Incorrect config provided. Please check the values you provided and try again.' | ||
const { relationModel, modelSuffix, modelCase } = parsedConfig.data | ||
const formatModelName = (name: string, prefix = '') => { | ||
if (modelCase === 'camelCase') { | ||
name = name.slice(0, 1).toLowerCase() + name.slice(1) | ||
} | ||
return `${prefix}${name}${modelSuffix}` | ||
const config = results.data | ||
const prismaOptions: PrismaOptions = { | ||
clientPath, | ||
outputPath, | ||
schemaPath, | ||
} | ||
const indexSource = project.createSourceFile( | ||
const indexFile = project.createSourceFile( | ||
`${outputPath}/index.ts`, | ||
{}, | ||
{ | ||
overwrite: true, | ||
} | ||
{ overwrite: true } | ||
) | ||
models.forEach((model) => { | ||
indexSource.addExportDeclaration({ | ||
moduleSpecifier: `./${model.name.toLowerCase()}`, | ||
}) | ||
generateBarrelFile(models, indexFile) | ||
const modelName = (name: string) => | ||
formatModelName(name, relationModel === 'default' ? '_' : '') | ||
indexFile.formatText({ | ||
indentSize: 2, | ||
convertTabsToSpaces: true, | ||
semicolons: SemicolonPreference.Remove, | ||
}) | ||
const relatedModelName = ( | ||
name: | ||
| string | ||
| DMMF.SchemaEnum | ||
| DMMF.OutputType | ||
| DMMF.SchemaArg | ||
) => | ||
formatModelName( | ||
relationModel === 'default' | ||
? name.toString() | ||
: `Related${name.toString()}` | ||
) | ||
models.forEach((model) => { | ||
const sourceFile = project.createSourceFile( | ||
`${outputPath}/${model.name.toLowerCase()}.ts`, | ||
{ | ||
statements: [ | ||
{ | ||
kind: StructureKind.ImportDeclaration, | ||
namespaceImport: 'z', | ||
moduleSpecifier: 'zod', | ||
}, | ||
], | ||
}, | ||
{ | ||
overwrite: true, | ||
} | ||
{}, | ||
{ overwrite: true } | ||
) | ||
const enumFields = model.fields.filter((f) => f.kind === 'enum') | ||
populateModelFile(model, sourceFile, config, prismaOptions) | ||
let relativePath = prismaClient?.output?.value | ||
? path.relative(outputPath, prismaClient.output.value) | ||
: null | ||
if ( | ||
relativePath && | ||
!( | ||
relativePath.startsWith('./') || | ||
relativePath.startsWith('../') | ||
) | ||
) | ||
relativePath = `./${relativePath}` | ||
sourceFile.addImportDeclaration({ | ||
kind: StructureKind.ImportDeclaration, | ||
moduleSpecifier: relativePath ?? '@prisma/client', | ||
namedImports: [model.name, ...enumFields.map((f) => f.type)], | ||
}) | ||
sourceFile.addStatements((writer) => | ||
writeArray(writer, getJSDocs(model.documentation)) | ||
) | ||
sourceFile.addVariableStatement({ | ||
declarationKind: VariableDeclarationKind.Const, | ||
isExported: true, | ||
declarations: [ | ||
{ | ||
name: modelName(model.name), | ||
initializer(writer) { | ||
writer | ||
.write('z.object(') | ||
.inlineBlock(() => { | ||
model.fields | ||
.filter((f) => f.kind !== 'object') | ||
.forEach((field) => { | ||
writeArray( | ||
writer, | ||
getJSDocs(field.documentation) | ||
) | ||
writer | ||
.write( | ||
`${ | ||
field.name | ||
}: ${getZodConstructor( | ||
field | ||
)}` | ||
) | ||
.write(',') | ||
.newLine() | ||
}) | ||
}) | ||
.write(')') | ||
}, | ||
}, | ||
], | ||
}) | ||
const relationFields = model.fields.filter( | ||
(f) => f.kind === 'object' | ||
) | ||
if (relationModel !== false && relationFields.length > 0) { | ||
const filteredFields = relationFields.filter( | ||
(f) => f.type !== model.name | ||
) | ||
if (filteredFields.length > 0) { | ||
sourceFile.addImportDeclaration({ | ||
kind: StructureKind.ImportDeclaration, | ||
moduleSpecifier: './index', | ||
namedImports: Array.from( | ||
new Set( | ||
filteredFields.flatMap((f) => [ | ||
`Complete${f.type}`, | ||
relatedModelName(f.type), | ||
]) | ||
) | ||
), | ||
}) | ||
} | ||
sourceFile.addInterface({ | ||
name: `Complete${model.name}`, | ||
isExported: true, | ||
extends: (writer) => writer.write(model.name), | ||
properties: relationFields.map((f) => ({ | ||
name: f.name, | ||
type: `Complete${f.type}${f.isList ? '[]' : ''}${ | ||
!f.isRequired ? ' | null' : '' | ||
}`, | ||
})), | ||
}) | ||
sourceFile.addStatements((writer) => | ||
writeArray(writer, [ | ||
'', | ||
'/**', | ||
` * ${relatedModelName( | ||
model.name | ||
)} contains all relations on your model in addition to the scalars`, | ||
' *', | ||
' * NOTE: Lazy required in case of potential circular dependencies within schema', | ||
' */', | ||
]) | ||
) | ||
sourceFile.addVariableStatement({ | ||
declarationKind: VariableDeclarationKind.Const, | ||
isExported: true, | ||
declarations: [ | ||
{ | ||
name: relatedModelName(model.name), | ||
type: `z.ZodSchema<Complete${model.name}>`, | ||
initializer(writer) { | ||
writer | ||
.write( | ||
`z.lazy(() => ${modelName( | ||
model.name | ||
)}.extend(` | ||
) | ||
.inlineBlock(() => { | ||
relationFields.forEach((field) => { | ||
writeArray( | ||
writer, | ||
getJSDocs(field.documentation) | ||
) | ||
writer | ||
.write( | ||
`${ | ||
field.name | ||
}: ${getZodConstructor( | ||
field, | ||
relatedModelName | ||
)}` | ||
) | ||
.write(',') | ||
.newLine() | ||
}) | ||
}) | ||
.write('))') | ||
}, | ||
}, | ||
], | ||
}) | ||
} | ||
sourceFile.formatText({ | ||
@@ -259,0 +67,0 @@ indentSize: 2, |
import type { DMMF } from '@prisma/generator-helper' | ||
import { computeModifiers } from './docs' | ||
import { computeCustomSchema, computeModifiers } from './docs' | ||
export const getZodConstructor = ( | ||
field: DMMF.Field, | ||
getRelatedModelName = ( | ||
name: string | DMMF.SchemaEnum | DMMF.OutputType | DMMF.SchemaArg | ||
) => name.toString() | ||
getRelatedModelName = (name: string | DMMF.SchemaEnum | DMMF.OutputType | DMMF.SchemaArg) => | ||
name.toString() | ||
) => { | ||
@@ -34,3 +33,3 @@ let zodType = 'z.unknown()' | ||
case 'Json': | ||
zodType = 'z.any()' | ||
zodType = 'jsonSchema' | ||
break | ||
@@ -40,2 +39,3 @@ case 'Boolean': | ||
break | ||
// TODO: Proper type for bytes fields | ||
case 'Bytes': | ||
@@ -51,9 +51,10 @@ zodType = 'z.unknown()' | ||
if (!field.isRequired) extraModifiers.push('nullable()') | ||
if (field.isList) extraModifiers.push('array()') | ||
if (field.documentation) { | ||
zodType = computeCustomSchema(field.documentation) ?? zodType | ||
extraModifiers.push(...computeModifiers(field.documentation)) | ||
} | ||
if (!field.isRequired && field.type !== 'Json') extraModifiers.push('nullish()') | ||
return `${zodType}${extraModifiers.join('.')}` | ||
} |
@@ -0,7 +1,26 @@ | ||
import { DMMF } from '@prisma/generator-helper' | ||
import type { CodeBlockWriter } from 'ts-morph' | ||
import { Config } from './config' | ||
export const writeArray = ( | ||
writer: CodeBlockWriter, | ||
array: string[], | ||
newLine = true | ||
) => array.forEach((line) => writer.write(line).conditionalNewLine(newLine)) | ||
export const writeArray = (writer: CodeBlockWriter, array: string[], newLine = true) => | ||
array.forEach((line) => writer.write(line).conditionalNewLine(newLine)) | ||
export const useModelNames = ({ modelCase, modelSuffix, relationModel }: Config) => { | ||
const formatModelName = (name: string, prefix = '') => { | ||
if (modelCase === 'camelCase') { | ||
name = name.slice(0, 1).toLowerCase() + name.slice(1) | ||
} | ||
return `${prefix}${name}${modelSuffix}` | ||
} | ||
return { | ||
modelName: (name: string) => formatModelName(name, relationModel === 'default' ? '_' : ''), | ||
relatedModelName: (name: string | DMMF.SchemaEnum | DMMF.OutputType | DMMF.SchemaArg) => | ||
formatModelName( | ||
relationModel === 'default' ? name.toString() : `Related${name.toString()}` | ||
), | ||
} | ||
} | ||
export const needsRelatedModel = (model: DMMF.Model, config: Config) => | ||
model.fields.some((field) => field.kind === 'object') && config.relationModel !== false |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
324181161
322211.75%14
-17.65%238
1152.63%642973
75632.98%0
-100%2
-33.33%322
16.67%5
25%8
300%22
Infinity%