UNPKG

@proofkit/typegen

Version:

`@proofkit/typegen` is a tool for generating TypeScript types from FileMaker database schemas, making it easier to work with FileMaker data in modern TypeScript projects.

418 lines (396 loc) 10.8 kB
import { VariableDeclarationKind, type SourceFile } from "ts-morph"; import { overrideCommentHeader, varname } from "./constants"; import { BuildSchemaArgs, TSchema } from "./types"; export function buildSchema( schemaFile: SourceFile, { type, ...args }: BuildSchemaArgs, ): void { // make sure schema has unique keys, in case a field is on the layout mulitple times args.schema.reduce( (acc: TSchema[], el) => acc.find((o) => o.name === el.name) ? acc : ([...acc, el] as Array<TSchema>), [], ); // setup const { schema, schemaName, portalSchema = [], valueLists = [], strictNumbers = false, } = args; const hasPortals = portalSchema.length > 0; if (type === "zod" || type === "zod/v4" || type === "zod/v3") { schemaFile.addImportDeclaration({ moduleSpecifier: type, namedImports: ["z"], }); if (hasPortals) { schemaFile.addImportDeclaration({ moduleSpecifier: "@proofkit/fmdapi", namedImports: ["InferZodPortals"], }); } } // build the portals for (const p of portalSchema) { if (type === "ts") { buildTypeTS(schemaFile, { schemaName: p.schemaName, schema: p.schema, strictNumbers, }); } else { buildTypeZod(schemaFile, { schemaName: p.schemaName, schema: p.schema, strictNumbers, }); } } // build the value lists for (const vls of valueLists) { if (vls.values.length > 0) { if (type === "ts") { buildValueListTS(schemaFile, { name: vls.name, values: vls.values, }); } else { buildValueListZod(schemaFile, { name: vls.name, values: vls.values, }); } } } // build the main schema if (type === "ts") { buildTypeTS(schemaFile, { schemaName, schema, strictNumbers, }); } else { buildTypeZod(schemaFile, { schemaName, schema, strictNumbers, }); } // build the final portals object if (portalSchema.length > 0) { if (type === "ts") { schemaFile.addTypeAlias({ name: `T${varname(schemaName)}Portals`, type: (writer) => { writer.block(() => { portalSchema.forEach((p) => { writer.write(`${p.schemaName}: T${varname(p.schemaName)}`); }); }); }, isExported: true, }); } else { schemaFile.addVariableStatement({ isExported: true, declarationKind: VariableDeclarationKind.Const, declarations: [ { name: `Z${varname(schemaName)}Portals`, initializer: (writer) => { writer .write(`{`) .newLine() .indent(() => { portalSchema.forEach((p, i) => { writer .quote(p.schemaName) .write(": ") .write(`Z${varname(p.schemaName)}`); writer.conditionalWrite(i !== portalSchema.length - 1, ","); writer.newLine(); }); }) .write(`}`); }, }, ], }); schemaFile.addTypeAlias({ name: `T${varname(schemaName)}Portals`, type: `InferZodPortals<typeof Z${varname(schemaName)}Portals>`, isExported: true, }); } } } function buildTypeTS( schemaFile: SourceFile, { schemaName, schema, strictNumbers = false, }: { schemaName: string; schema: Array<TSchema>; strictNumbers?: boolean; }, ) { schemaFile.addTypeAlias({ name: `T${varname(schemaName)}`, type: (writer) => { writer.inlineBlock(() => { schema.forEach((field) => { writer.quote(field.name).write(": "); if (field.type === "string") { writer.write("string"); } else if (field.type === "fmnumber") { if (strictNumbers) { writer.write("number | null"); } else { writer.write("string | number"); } } else if (field.type === "valueList") { writer.write(`"${field.values?.join('" | "')}"`); } else { writer.write("any"); } writer.write(",").newLine(); }); }); }, isExported: true, }); } function buildValueListTS( schemaFile: SourceFile, { name, values, }: { name: string; values: Array<string>; }, ) { schemaFile.addTypeAlias({ name: `TVL${varname(name)}`, type: `"${values.join('" | "')}"`, isExported: true, }); } function buildTypeZod( schemaFile: SourceFile, { schemaName, schema, strictNumbers = false, }: { schemaName: string; schema: Array<TSchema>; strictNumbers?: boolean; }, ) { schemaFile.addVariableStatement({ isExported: true, declarationKind: VariableDeclarationKind.Const, declarations: [ { name: `Z${varname(schemaName)}`, initializer: (writer) => { writer .write(`z.object(`) .inlineBlock(() => { schema.forEach((field) => { writer.quote(field.name).write(": "); if (field.type === "string") { writer.write("z.string()"); } else if (field.type === "fmnumber") { if (strictNumbers) { writer.write("z.coerce.number().nullable().catch(null)"); } else { writer.write("z.union([z.string(), z.number()])"); } } else if (field.type === "valueList") { writer.write(`z.enum([`); field.values?.map((v, i) => writer .quote(v) .conditionalWrite( i !== (field.values ?? []).length - 1, ", ", ), ); writer.write("])"); writer.conditionalWrite( field.values?.includes(""), `.catch("")`, ); } else { writer.write("z.any()"); } writer.write(",").newLine(); }); }) .write(")"); }, }, ], }); schemaFile.addTypeAlias({ name: `T${varname(schemaName)}`, type: `z.infer<typeof Z${varname(schemaName)}>`, isExported: true, }); } function buildValueListZod( schemaFile: SourceFile, { name, values, }: { name: string; values: Array<string>; }, ) { schemaFile.addVariableStatement({ isExported: true, declarationKind: VariableDeclarationKind.Const, declarations: [ { name: `ZVL${varname(name)}`, initializer: (writer) => { writer.write(`z.enum([`); values.map((v, i) => writer.quote(v).conditionalWrite(i !== values.length - 1, ", "), ); writer.write("])"); }, }, ], }); schemaFile.addTypeAlias({ name: `TVL${varname(name)}`, type: `z.infer<typeof ZVL${varname(name)}>`, isExported: true, }); } export function buildOverrideFile( overrideFile: SourceFile, schemaFile: SourceFile, { type, ...args }: BuildSchemaArgs, ) { if (type === "zod" || type === "zod/v4" || type === "zod/v3") { overrideFile.addImportDeclaration({ moduleSpecifier: type, namedImports: ["z"], }); } const { schemaName, portalSchema = [] } = args; const namedExportNames = schemaFile .getExportSymbols() .map((symbol) => symbol.getName()) .filter((name) => { if (type === "zod" || type === "zod/v4" || type === "zod/v3") { return name.startsWith("Z"); } else { return name.startsWith("T"); } }) .filter((name) => !name.endsWith("Portals")); overrideFile.addImportDeclaration({ moduleSpecifier: `./generated/${args.schemaName}`, namedImports: namedExportNames.map((name) => ({ name, alias: `${name}_generated`, isTypeOnly: type === "ts", })), }); const hasPortals = portalSchema.length > 0; if ( hasPortals && (type === "zod" || type === "zod/v4" || type === "zod/v3") ) { overrideFile.addImportDeclaration({ moduleSpecifier: "@proofkit/fmdapi", namedImports: ["InferZodPortals"], }); } namedExportNames.forEach((name) => { if (type === "zod" || type === "zod/v4" || type === "zod/v3") { overrideFile.addVariableStatement({ isExported: true, declarationKind: VariableDeclarationKind.Const, declarations: [ { name, initializer: (writer) => { writer.write(`${name}_generated`); }, }, ], }); overrideFile.addTypeAlias({ name: name.replace(/^Z/, "T"), type: `z.infer<typeof ${name}>`, isExported: true, }); } else if (type === "ts") { overrideFile.addTypeAlias({ name, type: `${name}_generated`, isExported: true, }); } }); // build the final portals object if (hasPortals) { if (type === "ts") { overrideFile.addTypeAlias({ name: `T${varname(schemaName)}Portals`, type: (writer) => { writer.block(() => { portalSchema.forEach((p) => { writer.write(`${p.schemaName}: T${varname(p.schemaName)}`); }); }); }, isExported: true, }); } else { overrideFile.addVariableStatement({ isExported: true, declarationKind: VariableDeclarationKind.Const, declarations: [ { name: `Z${varname(schemaName)}Portals`, initializer: (writer) => { writer .write(`{`) .newLine() .indent(() => { portalSchema.forEach((p, i) => { writer .quote(p.schemaName) .write(": ") .write(`Z${varname(p.schemaName)}`); writer.conditionalWrite(i !== portalSchema.length - 1, ","); writer.newLine(); }); }) .write(`}`); }, }, ], }); overrideFile.addTypeAlias({ name: `T${varname(schemaName)}Portals`, type: `InferZodPortals<typeof Z${varname(schemaName)}Portals>`, isExported: true, }); } } }