UNPKG

zod-to-x

Version:

Multi language types generation from Zod schemas.

450 lines (449 loc) 20.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Zod2Ast = void 0; const zod_1 = require("zod"); const logger_1 = require("../utils/logger"); const errors_1 = require("./errors"); /** * This class creates AST nodes used to transpile Zod Schemas to other languages. * Simply create an instance and call build with a ZodObject to obtain a list with transpilerable * nodes. */ class Zod2Ast { constructor(opt = {}) { var _a; this.nodes = new Map(); this.lazyPointers = []; this.warnings = []; this.opt = Object.assign(Object.assign({}, opt), { strict: (_a = opt.strict) !== null && _a !== void 0 ? _a : true }); } /** * Check if the layer of the item is compatible with the layer of the schema. If does and the * transpilerable item is in a different file, it returns the file name. * * @param itemName * @param layerMetadata * @returns */ _getTranspilerableFile(itemName, metadata) { var _a; const layer = (_a = metadata === null || metadata === void 0 ? void 0 : metadata.parentLayer) !== null && _a !== void 0 ? _a : metadata === null || metadata === void 0 ? void 0 : metadata.layer; if (this.opt.layer && layer) { if (this.opt.layer.index < layer.index) { throw new errors_1.BadLayerDefinitionError(`${itemName}: Layer with number ${this.opt.layer.index} can only use models` + `from the same or lower layer. Found layer with number ${layer.index}`); } if (this.opt.layer.file !== layer.file) { return { parentFile: layer.file, parentNamespace: layer.namespace, parentTypeName: metadata === null || metadata === void 0 ? void 0 : metadata.parentTypeName, }; } } return {}; } /** * Transpilerable items are treated as references in the AST * @param ref * @param refType * @param discriminantValue * @returns */ _createDefinition(ref, refType, discriminantValue, parentNamespace) { return { type: "definition", reference: ref, referenceType: refType, discriminantValue, parentNamespace, }; } /** * Extracts and formats the enumeration values from a given ZodEnum or ZodNativeEnum schema. * @param schema - A ZodEnum or ZodNativeEnum schema containing the enumeration values. * @returns A list of key-value pairs where the key is a formatted string and the value * is either a string or a number. */ _getEnumValues(schema) { if (schema instanceof zod_1.ZodEnum) { return Object.entries(schema.Enum).map(([key, value]) => { // Creates a string key if it starts with number. key = isNaN(Number(key.at(0))) ? key : `"${key}"`; return [key, value]; }); } else { return Object.entries(schema.enum) .filter(([key, _value]) => isNaN(Number(key))) .map(([key, value]) => { // Creates a string key if it starts with number. key = isNaN(Number(key.at(0))) ? key : `"${key}"`; return [key, value]; }); } } /** * Intersects the properties of two AST nodes and returns the combined properties. * * @param left - The left AST definition to intersect. * @param right - The right AST definition to intersect. * @returns An object containing the combined properties of the left and right AST nodes. */ _intersectAstNodes(left, right) { const leftData = this.nodes.get(left.reference); const rightData = this.nodes.get(right.reference); return { properties: Object.assign(Object.assign({}, leftData.properties), rightData.properties), }; } /** * Merges multiple AST definitions into a single AST object containing combined properties. * - Equal properties mush have the same type and array dimension. * - If a property is optional in one definition and required in another, it will be considered * optional in the merged object. * - If a property is nullable in one definition and non-nullable in another, it will be * considered nullable in the merged object. * * @param options - An array of AST definitions to be merged. * @returns An object containing the merged properties. * @throws AstNodeError - If properties with different types or array dimensions are encountered. */ _unionAstNodes(options) { const data = options.map((i) => this.nodes.get(i.reference)); return { properties: data.reduce((acc, i, j) => { var _a, _b; for (const key in i.properties) { if (acc[key]) { acc[key] = structuredClone(acc[key]); if (acc[key].type !== i.properties[key].type) { this.warnings.push(`Merging properties with different types: ${acc[key].type} ` + `(from ${(_a = data[j - 1]) === null || _a === void 0 ? void 0 : _a.name}) and ${i.properties[key].type} ` + `(from ${i.name})`); acc[key].type = i.properties[key].type; } if (acc[key].arrayDimension !== i.properties[key].arrayDimension) { this.warnings.push(`Merging properties with different array dimensions: ` + `${acc[key].arrayDimension} (from ${(_b = data[j - 1]) === null || _b === void 0 ? void 0 : _b.name}) and ` + `${i.properties[key].arrayDimension} (from ${i.name})`); acc[key].arrayDimension = Math.max(acc[key].arrayDimension || 0, i.properties[key].arrayDimension || 0); } if (acc[key].isNullable !== i.properties[key].isNullable) { acc[key].isNullable = true; } if (acc[key].isOptional !== i.properties[key].isOptional) { acc[key].isOptional = true; } if (i.properties[key].description) { acc[key].description = i.properties[key].description; } } else { acc[key] = i.properties[key]; } } return acc; }, {}), }; } _getNames(schema, errorString) { var _a; const name = (_a = schema._zod2x) === null || _a === void 0 ? void 0 : _a.typeName; if (!name) { throw new errors_1.AstTypeNameDefinitionError(errorString); } return Object.assign({ name, zodTypeName: schema._def.typeName }, this._getTranspilerableFile(name, schema._zod2x)); } _getEnumAst(schema, opt) { const { name, zodTypeName, parentFile, parentNamespace, parentTypeName } = this._getNames(schema, "ZodEnum/ZodNativeEnum type must have a typeName. Use zod2x method to provide one."); const item = { type: zodTypeName, name, values: this._getEnumValues(schema), description: schema._def.description, parentFile, parentNamespace, parentTypeName, isFromDiscriminatedUnion: opt === null || opt === void 0 ? void 0 : opt.isInjectedEnum, }; if (!this.nodes.has(name)) { this.nodes.set(name, item); } return this._createDefinition(name, zodTypeName, undefined, parentNamespace); } _getObjectAst(schema, opt) { const { name, zodTypeName, parentFile, parentNamespace, parentTypeName } = this._getNames(schema, "ZodObject type must have a typeName. Use zod2x method to provide one."); let discriminantValue = undefined; const shape = schema._def.shape(); if (!this.nodes.has(name)) { const properties = {}; for (const key in shape) { properties[key] = this._zodToAST(shape[key]); } this.nodes.set(name, { type: zod_1.ZodFirstPartyTypeKind.ZodObject, name, properties, description: schema.description, parentFile, parentNamespace, parentTypeName, }); } if (opt === null || opt === void 0 ? void 0 : opt.discriminantKey) { const item = this.nodes.get(name); if (Object.keys(item.properties).includes(opt.discriminantKey)) { const key = opt.discriminantKey; if (item.properties[key].type === zod_1.ZodFirstPartyTypeKind.ZodLiteral) { /* Used for serialization purposes, it is parsed as string for * convenience */ discriminantValue = String(item.properties[key].value); } else { console.warn(`Consider to set '${key}' key of '${name}' as ZodLiteral`); } } } return this._createDefinition(name, zodTypeName, discriminantValue, parentTypeName ? undefined : parentNamespace); } _getUnionAst(schema) { const def = schema._def; const discriminator = schema instanceof zod_1.ZodDiscriminatedUnion ? schema._def.discriminator : undefined; const { name, zodTypeName, parentFile, parentNamespace, parentTypeName } = this._getNames(schema, "ZodUnion/ZodDiscriminatedUnion type must have a typeName. " + "Use zod2x method to provide one."); const item = { type: zodTypeName, name, options: def.options.map((i) => this._zodToAST(i, { discriminantKey: discriminator })), description: schema.description, discriminantKey: discriminator, parentFile, parentNamespace, parentTypeName, }; if (!def.options.every((i) => i instanceof zod_1.ZodObject)) { this.warnings.push("Union of non-object types is a bad data modeling practice, " + "and could lead to unexpected results."); } else if (schema instanceof zod_1.ZodUnion) { this.warnings.push("Using ZodUnion is a bad data modeling practice. " + "Use ZodDiscriminatedUnion instead."); item.newObject = { name, type: zod_1.ZodFirstPartyTypeKind.ZodObject, properties: this._unionAstNodes(item.options).properties, description: (schema.description ? `${schema.description} - ` : "") + `Built from union of ` + `${item.options.map((i) => i.reference).join(", ")}`, }; } if (name && !this.nodes.has(name)) { this.nodes.set(name, item); } return this._createDefinition(name, zodTypeName, undefined, parentTypeName ? undefined : parentNamespace); } _getIntersectionAst(schema) { const def = schema._def; const { name, zodTypeName, parentFile, parentNamespace, parentTypeName } = this._getNames(schema, "ZodIntersection type must have a typeName. Use zod2x method to provide one."); const item = { type: zod_1.ZodFirstPartyTypeKind.ZodIntersection, name, left: this._zodToAST(def.left), right: this._zodToAST(def.right), description: schema.description, parentFile, parentNamespace, parentTypeName, }; if (def.left._def.typeName !== "ZodObject" || def.right._def.typeName !== "ZodObject") { this.warnings.push("Intersection of non-object is a bad data modeling practice, " + "and could lead to unexpected results."); } else { item.newObject = { type: zod_1.ZodFirstPartyTypeKind.ZodObject, name, properties: this._intersectAstNodes(item.left, item.right).properties, description: (schema.description ? `${schema.description} - ` : "") + `Built from intersection of ` + `${item.left.reference} and ` + `${item.right.reference}`, }; } if (name && !this.nodes.has(name)) { this.nodes.set(name, item); } return this._createDefinition(name, zodTypeName, undefined, parentTypeName ? undefined : parentNamespace); } /** * Build the AST node of provided Zod Schema * @param schema * @returns */ _zodToAST(schema, opt) { var _a, _b, _c, _d, _e, _f, _g, _h; const def = schema._def; if (schema instanceof zod_1.ZodString) { return { type: zod_1.ZodFirstPartyTypeKind.ZodString, description: schema.description, }; } else if (schema instanceof zod_1.ZodNumber || schema instanceof zod_1.ZodBigInt) { return { type: zod_1.ZodFirstPartyTypeKind.ZodNumber, constraints: { min: (_a = def.checks.find((i) => i.kind === "min")) === null || _a === void 0 ? void 0 : _a.value, max: (_b = def.checks.find((i) => i.kind === "max")) === null || _b === void 0 ? void 0 : _b.value, isInt: schema instanceof zod_1.ZodBigInt || def.checks.find((i) => i.kind === "int") != undefined, }, description: schema.description, }; } else if (schema instanceof zod_1.ZodBoolean) { return { type: zod_1.ZodFirstPartyTypeKind.ZodBoolean, description: schema.description, }; } else if (schema instanceof zod_1.ZodDate) { return { type: zod_1.ZodFirstPartyTypeKind.ZodDate, description: schema.description, }; } else if (schema instanceof zod_1.ZodAny) { return { type: zod_1.ZodFirstPartyTypeKind.ZodAny, description: schema.description, }; } else if (schema instanceof zod_1.ZodNullable) { const subSchema = this._zodToAST(def.innerType); return Object.assign(Object.assign({ isNullable: true }, subSchema), { description: schema.description || subSchema.description }); } else if (schema instanceof zod_1.ZodOptional) { const subSchema = this._zodToAST(def.innerType); return Object.assign(Object.assign({ isOptional: true }, subSchema), { description: schema.description || subSchema.description }); } else if (schema instanceof zod_1.ZodDefault) { const subSchema = this._zodToAST(def.innerType); return Object.assign(Object.assign({}, subSchema), { description: schema.description || subSchema.description }); } else if (schema instanceof zod_1.ZodArray) { const subSchema = this._zodToAST(def.type); return Object.assign(Object.assign({}, subSchema), { description: schema.description || subSchema.description, arrayDimension: Number.isInteger(subSchema.arrayDimension) ? ++subSchema.arrayDimension : 1 }); } else if (schema instanceof zod_1.ZodSet) { return { type: zod_1.ZodFirstPartyTypeKind.ZodSet, value: this._zodToAST(def.valueType), description: schema.description, }; } else if (schema instanceof zod_1.ZodLiteral) { let parentEnumName = undefined; let parentEnumKey = undefined; if ((_c = schema._zod2x) === null || _c === void 0 ? void 0 : _c.parentEnum) { parentEnumName = (_e = (_d = schema._zod2x) === null || _d === void 0 ? void 0 : _d.parentEnum._zod2x) === null || _e === void 0 ? void 0 : _e.typeName; parentEnumKey = (_g = this._getEnumValues((_f = schema._zod2x) === null || _f === void 0 ? void 0 : _f.parentEnum).find((i) => i[1] === def.value)) === null || _g === void 0 ? void 0 : _g[0]; this._zodToAST((_h = schema._zod2x) === null || _h === void 0 ? void 0 : _h.parentEnum, { isInjectedEnum: true }); } return { type: zod_1.ZodFirstPartyTypeKind.ZodLiteral, value: def.value, description: schema.description, parentEnumName, parentEnumKey, }; } else if (schema instanceof zod_1.ZodRecord) { return { type: zod_1.ZodFirstPartyTypeKind.ZodRecord, key: this._zodToAST(def.keyType), value: this._zodToAST(def.valueType), description: schema.description, }; } else if (schema instanceof zod_1.ZodLazy) { /** Lazy items use to be recursive schemas of its own, so the are trated as another * definition */ const lazySchema = def.getter(); const lazyPointer = this._createDefinition("pending", zod_1.ZodFirstPartyTypeKind.ZodAny); this.lazyPointers.push([lazyPointer, lazySchema]); return lazyPointer; } else if (schema instanceof zod_1.ZodTuple) { return { type: zod_1.ZodFirstPartyTypeKind.ZodTuple, items: def.items.map(this._zodToAST.bind(this)), description: schema.description, }; } else if (schema instanceof zod_1.ZodMap) { return { type: zod_1.ZodFirstPartyTypeKind.ZodMap, key: this._zodToAST(def.keyType), value: this._zodToAST(def.valueType), description: schema.description, }; /** * * * Transpilerable items * * * */ } else if (schema instanceof zod_1.ZodNativeEnum || schema instanceof zod_1.ZodEnum) { return this._getEnumAst(schema, opt); } else if (schema instanceof zod_1.ZodObject) { return this._getObjectAst(schema, opt); } else if (schema instanceof zod_1.ZodUnion || schema instanceof zod_1.ZodDiscriminatedUnion) { return this._getUnionAst(schema); } else if (schema instanceof zod_1.ZodIntersection) { return this._getIntersectionAst(schema); } else { logger_1.log.warn(`Unsupported Zod type: ${JSON.stringify(schema)}`); return { type: zod_1.ZodFirstPartyTypeKind.ZodAny, description: `Unsupported Zod type: ${schema._def.typeName}`, }; } } /** * Create the AST identifying the nodes that can be transpiled. * @param schema * @returns Transpilerable nodes. */ build(schema) { this._zodToAST(schema); while (this.lazyPointers.length > 0) { const [pointer, schema] = this.lazyPointers.shift(); const lazyResolve = this._zodToAST(schema); /** Pointer to the pending AST node is updated with the lazy resolve */ Object.keys(pointer).forEach((key) => { delete pointer[key]; }); Object.entries(lazyResolve).forEach(([key, value]) => { pointer[key] = value; }); } if (this.opt.strict !== false && this.warnings.length > 0) { throw new errors_1.AstNodeError(this.warnings.join("\n")); } return { nodes: this.nodes, warnings: this.warnings, }; } } exports.Zod2Ast = Zod2Ast;