From 534d94557493bdee253af7672847a4aeb3aa2dc9 Mon Sep 17 00:00:00 2001 From: myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Wed, 15 Nov 2023 16:48:09 +0100 Subject: [PATCH 01/15] refactor: Make parsers more general --- package.json | 1 + src/server/server.ts | 65 ++++++++++++++++++++++--------------- src/server/templates/zod.ts | 33 +++++++++++++++++++ src/server/types.ts | 21 ++++++++++++ 4 files changed, 93 insertions(+), 27 deletions(-) create mode 100644 src/server/templates/zod.ts create mode 100644 src/server/types.ts diff --git a/package.json b/package.json index 499a29f1..fc3a6d4b 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "build": "tsc -p tsconfig.json && cpy 'src/lib/sql/*.sql' dist/lib/sql", "docs:export": "PG_META_EXPORT_DOCS=true node --loader ts-node/esm src/server/server.ts > openapi.json", "gen:types:typescript": "PG_META_GENERATE_TYPES=typescript node --loader ts-node/esm src/server/server.ts", + "gen:types:zod": "PG_META_GENERATE_TYPES=zod node --loader ts-node/esm src/server/server.ts", "start": "node dist/server/server.js", "dev": "trap 'npm run db:clean' INT && run-s db:clean db:run && nodemon --exec node --loader ts-node/esm src/server/server.ts | pino-pretty --colorize", "pkg": "run-s clean build && pkg .pkg.config.json", diff --git a/src/server/server.ts b/src/server/server.ts index 760ddf1d..c3e970e3 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -13,6 +13,8 @@ import { PG_META_PORT, } from './constants.js' import { apply as applyTypescriptTemplate } from './templates/typescript.js' +import { apply as applyZodTemplate } from './templates/typescript.js' +import {TemplateProps} from "./types.js"; const logger = pino({ formatters: { @@ -33,6 +35,22 @@ if (EXPORT_DOCS) { console.log(JSON.stringify(app.swagger(), null, 2)) } else if (GENERATE_TYPES === 'typescript') { // TODO: Move to a separate script. + console.log( + await applyTemplate(applyTypescriptTemplate) + ); +} else if (GENERATE_TYPES === "zod") { + console.log( + await applyTemplate(applyZodTemplate) + ); +} +else { + app.listen({ port: PG_META_PORT, host: PG_META_HOST }, () => { + const adminPort = PG_META_PORT + 1 + adminApp.listen({ port: adminPort, host: PG_META_HOST }) + }) +} + +async function applyTemplate(apply: (props: TemplateProps) => string): Promise { const pgMeta: PostgresMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString: PG_CONNECTION, @@ -50,27 +68,27 @@ if (EXPORT_DOCS) { pgMeta.schemas.list(), pgMeta.tables.list({ includedSchemas: - GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, + GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, includeColumns: false, }), pgMeta.views.list({ includedSchemas: - GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, + GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, includeColumns: false, }), pgMeta.materializedViews.list({ includedSchemas: - GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, + GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, includeColumns: false, }), pgMeta.columns.list({ includedSchemas: - GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, + GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, }), pgMeta.relationships.list(), pgMeta.functions.list({ includedSchemas: - GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, + GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, }), pgMeta.types.list({ includeArrayTypes: true, @@ -104,29 +122,22 @@ if (EXPORT_DOCS) { throw new Error(typesError.message) } - console.log( - applyTypescriptTemplate({ - schemas: schemas!.filter( + return apply({ + schemas: schemas!.filter( ({ name }) => - GENERATE_TYPES_INCLUDED_SCHEMAS.length === 0 || - GENERATE_TYPES_INCLUDED_SCHEMAS.includes(name) - ), - tables: tables!, - views: views!, - materializedViews: materializedViews!, - columns: columns!, - relationships: relationships!, - functions: functions!.filter( + GENERATE_TYPES_INCLUDED_SCHEMAS.length === 0 || + GENERATE_TYPES_INCLUDED_SCHEMAS.includes(name) + ), + tables: tables!, + views: views!, + materializedViews: materializedViews!, + columns: columns!, + relationships: relationships!, + functions: functions!.filter( ({ return_type }) => !['trigger', 'event_trigger'].includes(return_type) - ), - types: types!.filter(({ name }) => name[0] !== '_'), - arrayTypes: types!.filter(({ name }) => name[0] === '_'), - detectOneToOneRelationships: GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS, - }) - ) -} else { - app.listen({ port: PG_META_PORT, host: PG_META_HOST }, () => { - const adminPort = PG_META_PORT + 1 - adminApp.listen({ port: adminPort, host: PG_META_HOST }) + ), + types: types!.filter(({ name }) => name[0] !== '_'), + arrayTypes: types!.filter(({ name }) => name[0] === '_'), + detectOneToOneRelationships: GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS, }) } diff --git a/src/server/templates/zod.ts b/src/server/templates/zod.ts new file mode 100644 index 00000000..903434bb --- /dev/null +++ b/src/server/templates/zod.ts @@ -0,0 +1,33 @@ +import { + PostgresColumn, PostgresFunction, + PostgresMaterializedView, PostgresRelationship, + PostgresSchema, + PostgresTable, PostgresType, + PostgresView +} from "../../lib/index.js"; + +export const apply = ({ + schemas, + tables, + views, + materializedViews, + columns, + relationships, + functions, + types, + arrayTypes, + detectOneToOneRelationships, + }: { + schemas: PostgresSchema[] + tables: Omit[] + views: Omit[] + materializedViews: Omit[] + columns: PostgresColumn[] + relationships: PostgresRelationship[] + functions: PostgresFunction[] + types: PostgresType[] + arrayTypes: PostgresType[] + detectOneToOneRelationships: boolean +}): string { + debugger +} diff --git a/src/server/types.ts b/src/server/types.ts new file mode 100644 index 00000000..bd0e5747 --- /dev/null +++ b/src/server/types.ts @@ -0,0 +1,21 @@ +import { + PostgresColumn, PostgresFunction, + PostgresMaterializedView, + PostgresRelationship, + PostgresSchema, + PostgresTable, PostgresType, + PostgresView +} from "../lib/index.js"; + +export interface TemplateProps { + schemas: PostgresSchema[] + tables: Omit[] + views: Omit[] + materializedViews: Omit[] + columns: PostgresColumn[] + relationships: PostgresRelationship[] + functions: PostgresFunction[] + types: PostgresType[] + arrayTypes: PostgresType[] + detectOneToOneRelationships: boolean +} From 064a7af2cbbb9e77cd442f8c981f26e793f91e2e Mon Sep 17 00:00:00 2001 From: myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 16 Nov 2023 10:22:15 +0100 Subject: [PATCH 02/15] feat(zod): Adding basic & insert --- src/server/server.ts | 2 +- src/server/templates/zod.ts | 163 +++++++++++++++++++++++++++++++++++- 2 files changed, 162 insertions(+), 3 deletions(-) diff --git a/src/server/server.ts b/src/server/server.ts index c3e970e3..790406c6 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -13,7 +13,7 @@ import { PG_META_PORT, } from './constants.js' import { apply as applyTypescriptTemplate } from './templates/typescript.js' -import { apply as applyZodTemplate } from './templates/typescript.js' +import { apply as applyZodTemplate } from './templates/zod.js' import {TemplateProps} from "./types.js"; const logger = pino({ diff --git a/src/server/templates/zod.ts b/src/server/templates/zod.ts index 903434bb..9e249e1f 100644 --- a/src/server/templates/zod.ts +++ b/src/server/templates/zod.ts @@ -5,6 +5,11 @@ import { PostgresTable, PostgresType, PostgresView } from "../../lib/index.js"; +import Tables from "../routes/tables.js"; +import pg from "pg"; +import prettier from "prettier"; + +type ColumnsPerTable = Record; export const apply = ({ schemas, @@ -28,6 +33,160 @@ export const apply = ({ types: PostgresType[] arrayTypes: PostgresType[] detectOneToOneRelationships: boolean -}): string { - debugger +}): string => { + const columnsByTableId = columns + .sort(({ name: a }, { name: b }) => a.localeCompare(b)) + .reduce((acc, curr) => { + acc[curr.table_id] ??= [] + acc[curr.table_id].push(curr) + return acc + }, {} as ColumnsPerTable) + + /* + Example: + ```typescript + public.tables.user.insert() + ``` + */ + + const output = ` + const schema = { + ${schemas.map((schema) => `${schema.name}: ${writeSchema(schema, tables, columnsByTableId)}`).join(',\n')} + } + ` + + return prettier.format(output, { + parser: 'typescript', + semi: false, + }) +} + +function writeSchema(schema: PostgresSchema, availableTables: PostgresTable[], columnsByTableId: ColumnsPerTable): string { + const schemaTables = availableTables + .filter((table) => table.schema === schema.name) + .sort(({ name: a }, { name: b }) => a.localeCompare(b)) + + return `{ + insert: { + ${schemaTables.map((table) => `${table.name}: ${writeTable(table, columnsByTableId[table.id])}`).join(',\n')} + } + }` +} + +function writeTable(table: PostgresTable, columns: PostgresColumn[]): string { + return `{ + ${columns.map((column) => `"${column.name}": ${writeColumn(column)}`).join(',\n')}, + _enums: { + ${columns.filter(hasColumnEnum).map((column) => `${getColumnEnum(column)}`).join(',\n')} + } + }` } + +function writeColumn(column: PostgresColumn): string { + return `z.${basicZodType(column.format)}()${joinWithLeading(extraZodMethods(column), ".")}` +} + +function hasColumnEnum(column: PostgresColumn): boolean { + return column.enums.length > 0 +} + +function getColumnEnum(column: PostgresColumn): string { + return `${column.format}: z.enum([${column.enums.map((value) => `"${value}"`).join(', ')} as const])` +} + +function basicZodType(pgType: string): string { + if (pgType === "boolean") { + return "boolean" + } + + if (['int2', 'int4', 'int8', 'float4', 'float8', 'numeric', 'integer'].includes(pgType)) { + return 'number' + } + + if ( + [ + 'bytea', + 'bpchar', + 'varchar', + 'text', + 'citext', + 'uuid', + 'vector', + 'json', + 'jsonb', + ].includes(pgType) + ) { + return 'string' + } + + if (["date", "time", "timetz", "timestamp", "timestamptz"].includes(pgType)) { + return 'date' + } + + console.info(`Unknown type ${pgType}`) + + // Everything else is an enum + return "string" +} + +function extraZodMethods(column: PostgresColumn): string[] { + const methods: string[] = [] + + if (column.format === "uuid") { + methods.push("regex(/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/)") + } + + // Date and time types + if (["date", "time", "timetz", "timestamp", "timestamptz"].includes(column.format)) { + if (column.default_value === "now()") { + methods.push("default(() => new Date())") + } + } + + // Enums + if (column.data_type === "USER-DEFINED") { + methods.push(`enum([${column.enums.map((value) => `"${value}"`).join(', ')}] as const)`) + } + + // General constraints + if (column.is_nullable) { + methods.push("optional()") + } + + return methods +} + +function joinWithLeading(arr: T[], join: string): string { + if (arr.length === 0) { + return "" + } + + return join + arr.join(join) +} + +/** Create a zod object type for a table. + * You probably don't want to call this function, unless you're writing a custom template. + * Example: + * Given a table that looks like this: + * ```sql + * CREATE TABLE public.users ( + * id uuid NOT NULL, + * name text, + * email text NOT NULL, + * created_at timestamp without time zone NOT NULL DEFAULT now() + * ); + * The generated zod object would look like this: + * ```typescript + * const User = z.object({ + * // uuid + * id: z.number().regex(/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/), + * name: z.string().nullable(), + * email: z.string(), + * created_at: z.date().default(() => new Date()), + * }) + * ``` + * @param tableName - The name of the table; e.g. `users` will be snake cased and used as the variable name. + * @param columns - The columns of the table. + * @returns A zod object type. + */ +function createTableObject(tableName: string, columns: PostgresColumn[]) {} From 0d8c87accd46b2616c0a46de570da47fb44f04a0 Mon Sep 17 00:00:00 2001 From: myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 20 Nov 2023 11:43:43 +0100 Subject: [PATCH 03/15] feat(zod): Adding zod --- src/server/templates/_common.ts | 18 ++++ src/server/templates/zod.ts | 140 ++++++++++++++++++++++++++++---- 2 files changed, 143 insertions(+), 15 deletions(-) create mode 100644 src/server/templates/_common.ts diff --git a/src/server/templates/_common.ts b/src/server/templates/_common.ts new file mode 100644 index 00000000..884b98bc --- /dev/null +++ b/src/server/templates/_common.ts @@ -0,0 +1,18 @@ +import {PostgresFunction} from "../../lib/index.js"; + +export const getSchemaFunctions = (functions: PostgresFunction[], schemaName: string): PostgresFunction[] => { + return functions + .filter((func) => { + if (func.schema !== schemaName) { + return false + } + + // Either: + // 1. All input args are be named, or + // 2. There is only one input arg which is unnamed + const inArgs = func.args.filter(({ mode }) => ['in', 'inout', 'variadic'].includes(mode)) + + return inArgs.length === 1 || !inArgs.some(({ name }) => name === '') + }) + .sort(({ name: a }, { name: b }) => a.localeCompare(b)) +} diff --git a/src/server/templates/zod.ts b/src/server/templates/zod.ts index 9e249e1f..6b845a93 100644 --- a/src/server/templates/zod.ts +++ b/src/server/templates/zod.ts @@ -8,6 +8,7 @@ import { import Tables from "../routes/tables.js"; import pg from "pg"; import prettier from "prettier"; +import {getSchemaFunctions} from "./_common.js"; type ColumnsPerTable = Record; @@ -51,7 +52,14 @@ export const apply = ({ const output = ` const schema = { - ${schemas.map((schema) => `${schema.name}: ${writeSchema(schema, tables, columnsByTableId)}`).join(',\n')} + ${schemas.map((schema) => `${schema.name}: ${writeSchema( + schema, + tables, + columnsByTableId, + getSchemaFunctions(functions, schema.name), + types, + arrayTypes, + )}`).join(',\n')} } ` @@ -61,29 +69,111 @@ export const apply = ({ }) } -function writeSchema(schema: PostgresSchema, availableTables: PostgresTable[], columnsByTableId: ColumnsPerTable): string { +function writeSchema( + schema: PostgresSchema, + availableTables: PostgresTable[], + columnsByTableId: ColumnsPerTable, + functions: PostgresFunction[], + types: PostgresType[], + arrayTypes: PostgresType[], +): string { const schemaTables = availableTables .filter((table) => table.schema === schema.name) .sort(({ name: a }, { name: b }) => a.localeCompare(b)) + const schemaEnums = types + .filter((type) => type.schema === schema.name && type.enums.length > 0) + .sort(({ name: a }, { name: b }) => a.localeCompare(b)) return `{ - insert: { - ${schemaTables.map((table) => `${table.name}: ${writeTable(table, columnsByTableId[table.id])}`).join(',\n')} - } + tables: { + ${schemaTables.map(table => `${table.name}: { + row: ${writeRowTable(columnsByTableId[table.id], functions.filter(fn => fn.argument_types === table.name), types)}, + insert: ${writeInsertTable(columnsByTableId[table.id])}, + update: ${writeUpdateTable(columnsByTableId[table.id])}, + }`)} + }, + enums: { + ${schemaEnums.map(enumType => `${enumType.name}: z.enum([${enumType.enums.map((value) => `"${value}"`).join(', ')} as const])`).join(',\n')} + }, + functions: ${writeFunctions(functions, types, arrayTypes)}, }` } -function writeTable(table: PostgresTable, columns: PostgresColumn[]): string { - return `{ +function writeRowTable(columns: PostgresColumn[], readFunctions: PostgresFunction[], types: PostgresType[]): string { + return `z.object({ ${columns.map((column) => `"${column.name}": ${writeColumn(column)}`).join(',\n')}, - _enums: { - ${columns.filter(hasColumnEnum).map((column) => `${getColumnEnum(column)}`).join(',\n')} - } - }` + ${readFunctions.map((func) => `"${func.name}": ${writeReadFunction(func, types)}`).join(',\n')} + })` +} + +function writeInsertTable(columns: PostgresColumn[]): string { + return `z.object({ + ${columns.filter(column => column.identity_generation !== "ALWAYS").map((column) => `"${column.name}": ${writeColumn(column)}`).join(',\n')}, + })` +} + +function writeUpdateTable(columns: PostgresColumn[]): string { + return `z.object({ + ${columns + .filter(column => column.identity_generation !== "ALWAYS") + .map((column) => `"${column.name}": z.${basicZodType(column.format)}()${joinWithLeading(uniq([...extractGeneralZodMethods(column), "optional()"]), ".")}${joinWithLeading(extractExtraZodMethods(column), ".")}`).join(',\n')}, + })` } function writeColumn(column: PostgresColumn): string { - return `z.${basicZodType(column.format)}()${joinWithLeading(extraZodMethods(column), ".")}` + return `z.${basicZodType(column.format)}()${joinWithLeading(extractGeneralZodMethods(column), ".")}${joinWithLeading(extractExtraZodMethods(column), ".")}` +} + +function writeReadFunction(func: PostgresFunction, types: PostgresType[]): string { + const type = types.find(({ id }) => id === func.return_type_id) + const zodType = type ? basicZodType(type.format) : 'unknown' + + return `z.${zodType}().nullable()` +} + +function writeFunctions( + functions: PostgresFunction[], + types: PostgresType[], + arrayTypes: PostgresType[], +): string { + const schemaFunctionsGroupedByName = functions.reduce((acc, curr) => { + acc[curr.name] ??= [] + acc[curr.name].push(curr) + return acc + }, {} as Record) + + return `{ + ${Object.entries(schemaFunctionsGroupedByName).map(([name, functions]) => { + if (functions.length === 1) { + return `${functions[0].name}: ${writeFunction(functions[0], types, arrayTypes)}` + } + + return "test: 1" + }).join(',\n')} + }` +} + +function writeFunction(func: PostgresFunction, types: PostgresType[], arrayTypes: PostgresType[]): string { + const inArgs = func.args.filter(({ mode }) => mode === 'in') + + return `z.object({ + ${inArgs.map(arg => `${JSON.stringify(arg.name)}: ${writeFunctionArg(arg, types, arrayTypes)}`).join(',\n')} + })` +} + +function writeFunctionArg(arg: PostgresFunction['args'][0], types: PostgresType[], arrayTypes: PostgresType[]): string { + let type = arrayTypes.find(({ id }) => id === arg.type_id) + if (type) { + // If it's an array type, the name looks like `_int8`. + const elementTypeName = type.name.substring(1) + return `z.array(z.${basicZodType(elementTypeName)}())` + (arg.has_default ? '.optional()' : '') + } + type = types.find(({ id }) => id === arg.type_id) + if (type) { + return `z.${basicZodType(type.format)}()` + (arg.has_default ? '.optional()' : '') + } + + return `z.unknown()` + (arg.has_default ? '.optional()' : '') } function hasColumnEnum(column: PostgresColumn): boolean { @@ -129,7 +219,7 @@ function basicZodType(pgType: string): string { return "string" } -function extraZodMethods(column: PostgresColumn): string[] { +function extractExtraZodMethods(column: PostgresColumn): string[] { const methods: string[] = [] if (column.format === "uuid") { @@ -148,10 +238,22 @@ function extraZodMethods(column: PostgresColumn): string[] { methods.push(`enum([${column.enums.map((value) => `"${value}"`).join(', ')}] as const)`) } - // General constraints - if (column.is_nullable) { + return methods +} + +function extractGeneralZodMethods(column: PostgresColumn): string[] { + const methods: string[] = [] + + if ( + column.is_nullable || + column.is_identity || + column.default_value !== null + ) { methods.push("optional()") } + if (column.is_nullable) { + methods.push("nullable()") + } return methods } @@ -164,6 +266,14 @@ function joinWithLeading(arr: T[], join: string): string { return join + arr.join(join) } +/** Remove duplicate values from an array. + * Creates a new array. + * @param arr - The array to remove duplicates from. + */ +function uniq(arr: T[]): T[] { + return [...new Set(arr)] +} + /** Create a zod object type for a table. * You probably don't want to call this function, unless you're writing a custom template. * Example: From 89b11b90e6e6b40daa7ba4806c8cf4d4b3e4fb7b Mon Sep 17 00:00:00 2001 From: myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Wed, 22 Nov 2023 10:01:00 +0100 Subject: [PATCH 04/15] feat: Add views --- src/server/templates/_common.ts | 6 +++- src/server/templates/zod.ts | 50 +++++++++++++++++---------------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/server/templates/_common.ts b/src/server/templates/_common.ts index 884b98bc..a09d1de2 100644 --- a/src/server/templates/_common.ts +++ b/src/server/templates/_common.ts @@ -1,4 +1,8 @@ -import {PostgresFunction} from "../../lib/index.js"; +import {PostgresFunction, PostgresTable, PostgresType} from "../../lib/index.js"; + +export const filterFromSchema = (items: T[], schemaName: string): T[] => { + return items.filter((item) => item.schema === schemaName).sort(({name: a}, {name: b}) => a.localeCompare(b)) +} export const getSchemaFunctions = (functions: PostgresFunction[], schemaName: string): PostgresFunction[] => { return functions diff --git a/src/server/templates/zod.ts b/src/server/templates/zod.ts index 6b845a93..5bbb7581 100644 --- a/src/server/templates/zod.ts +++ b/src/server/templates/zod.ts @@ -5,10 +5,8 @@ import { PostgresTable, PostgresType, PostgresView } from "../../lib/index.js"; -import Tables from "../routes/tables.js"; -import pg from "pg"; import prettier from "prettier"; -import {getSchemaFunctions} from "./_common.js"; +import {filterFromSchema, getSchemaFunctions} from "./_common.js"; type ColumnsPerTable = Record; @@ -51,13 +49,17 @@ export const apply = ({ */ const output = ` + import * as z from 'zod' + import { v4 as uuidv4 } from 'uuid' + const schema = { ${schemas.map((schema) => `${schema.name}: ${writeSchema( schema, - tables, + filterFromSchema(tables, schema.name), columnsByTableId, getSchemaFunctions(functions, schema.name), - types, + filterFromSchema([...views, ...materializedViews], schema.name), + filterFromSchema(types, schema.name), arrayTypes, )}`).join(',\n')} } @@ -74,28 +76,25 @@ function writeSchema( availableTables: PostgresTable[], columnsByTableId: ColumnsPerTable, functions: PostgresFunction[], + views: PostgresView[], types: PostgresType[], arrayTypes: PostgresType[], ): string { - const schemaTables = availableTables - .filter((table) => table.schema === schema.name) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - const schemaEnums = types - .filter((type) => type.schema === schema.name && type.enums.length > 0) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - return `{ tables: { - ${schemaTables.map(table => `${table.name}: { + ${availableTables.map(table => `${table.name}: { row: ${writeRowTable(columnsByTableId[table.id], functions.filter(fn => fn.argument_types === table.name), types)}, insert: ${writeInsertTable(columnsByTableId[table.id])}, update: ${writeUpdateTable(columnsByTableId[table.id])}, }`)} }, enums: { - ${schemaEnums.map(enumType => `${enumType.name}: z.enum([${enumType.enums.map((value) => `"${value}"`).join(', ')} as const])`).join(',\n')} + ${types.filter(enumType => enumType.enums.length > 0).map(enumType => `${enumType.name}: z.enum([${enumType.enums.map((value) => `"${value}"`).join(', ')}] as const)`).join(',\n')} }, functions: ${writeFunctions(functions, types, arrayTypes)}, + views: { + ${views.map(view => `${JSON.stringify(view.name)}: ${writeView(columnsByTableId[view.id])}`)} + } }` } @@ -124,6 +123,12 @@ function writeColumn(column: PostgresColumn): string { return `z.${basicZodType(column.format)}()${joinWithLeading(extractGeneralZodMethods(column), ".")}${joinWithLeading(extractExtraZodMethods(column), ".")}` } +function writeView(columns: PostgresColumn[]): string { + return `z.object({ + ${columns.filter(column => column.is_updatable).map((column) => `${JSON.stringify(column.name)}: ${writeColumn(column)}`).join(',\n')} + })` +} + function writeReadFunction(func: PostgresFunction, types: PostgresType[]): string { const type = types.find(({ id }) => id === func.return_type_id) const zodType = type ? basicZodType(type.format) : 'unknown' @@ -145,7 +150,7 @@ function writeFunctions( return `{ ${Object.entries(schemaFunctionsGroupedByName).map(([name, functions]) => { if (functions.length === 1) { - return `${functions[0].name}: ${writeFunction(functions[0], types, arrayTypes)}` + return `"${functions[0].name}": ${writeFunction(functions[0], types, arrayTypes)}` } return "test: 1" @@ -176,16 +181,8 @@ function writeFunctionArg(arg: PostgresFunction['args'][0], types: PostgresType[ return `z.unknown()` + (arg.has_default ? '.optional()' : '') } -function hasColumnEnum(column: PostgresColumn): boolean { - return column.enums.length > 0 -} - -function getColumnEnum(column: PostgresColumn): string { - return `${column.format}: z.enum([${column.enums.map((value) => `"${value}"`).join(', ')} as const])` -} - function basicZodType(pgType: string): string { - if (pgType === "boolean") { + if ([ 'bool', 'boolean' ].includes(pgType)) { return "boolean" } @@ -222,8 +219,13 @@ function basicZodType(pgType: string): string { function extractExtraZodMethods(column: PostgresColumn): string[] { const methods: string[] = [] + // UUID if (column.format === "uuid") { methods.push("regex(/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/)") + + if (column.default_value === "gen_random_uuid()") { + methods.push("default(() => uuidv4())") + } } // Date and time types From f10e77f5f7c39c19a50bf221eb940ed5ca629944 Mon Sep 17 00:00:00 2001 From: myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Wed, 22 Nov 2023 16:25:46 +0100 Subject: [PATCH 05/15] feat: Improving zod generator --- src/server/templates/zod.ts | 40 ++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/server/templates/zod.ts b/src/server/templates/zod.ts index 5bbb7581..25525a8f 100644 --- a/src/server/templates/zod.ts +++ b/src/server/templates/zod.ts @@ -59,7 +59,7 @@ export const apply = ({ columnsByTableId, getSchemaFunctions(functions, schema.name), filterFromSchema([...views, ...materializedViews], schema.name), - filterFromSchema(types, schema.name), + types, arrayTypes, )}`).join(',\n')} } @@ -115,12 +115,12 @@ function writeUpdateTable(columns: PostgresColumn[]): string { return `z.object({ ${columns .filter(column => column.identity_generation !== "ALWAYS") - .map((column) => `"${column.name}": z.${basicZodType(column.format)}()${joinWithLeading(uniq([...extractGeneralZodMethods(column), "optional()"]), ".")}${joinWithLeading(extractExtraZodMethods(column), ".")}`).join(',\n')}, + .map((column) => `"${column.name}": z.${basicZodType(column.format)}${joinWithLeading(uniq([...extractGeneralZodMethods(column), "optional()"]), ".")}${joinWithLeading(extractExtraZodMethods(column), ".")}`).join(',\n')}, })` } function writeColumn(column: PostgresColumn): string { - return `z.${basicZodType(column.format)}()${joinWithLeading(extractGeneralZodMethods(column), ".")}${joinWithLeading(extractExtraZodMethods(column), ".")}` + return `z.${basicZodType(column.format)}${joinWithLeading(extractGeneralZodMethods(column), ".")}${joinWithLeading(extractExtraZodMethods(column), ".")}` } function writeView(columns: PostgresColumn[]): string { @@ -171,23 +171,30 @@ function writeFunctionArg(arg: PostgresFunction['args'][0], types: PostgresType[ if (type) { // If it's an array type, the name looks like `_int8`. const elementTypeName = type.name.substring(1) - return `z.array(z.${basicZodType(elementTypeName)}())` + (arg.has_default ? '.optional()' : '') + return `z.array(z.${basicZodType(elementTypeName)})` + (arg.has_default ? '.optional()' : '') } type = types.find(({ id }) => id === arg.type_id) if (type) { - return `z.${basicZodType(type.format)}()` + (arg.has_default ? '.optional()' : '') + return "z." + basicZodType(type.format) + (arg.has_default ? '.optional()' : '') } + console.info(`Function: Unknown type ${arg.type_id}`) + return `z.unknown()` + (arg.has_default ? '.optional()' : '') } function basicZodType(pgType: string): string { + // Array + if (pgType.startsWith("_")) { + return basicZodType(pgType.substring(1)) + ".array()" + } + if ([ 'bool', 'boolean' ].includes(pgType)) { - return "boolean" + return "boolean()" } - if (['int2', 'int4', 'int8', 'float4', 'float8', 'numeric', 'integer'].includes(pgType)) { - return 'number' + if (['int2', 'int4', 'int8', 'float4', 'float8', 'numeric', 'integer', 'bigint', 'oid'].includes(pgType)) { + return 'number()' } if ( @@ -201,21 +208,25 @@ function basicZodType(pgType: string): string { 'vector', 'json', 'jsonb', + 'inet' ].includes(pgType) ) { - return 'string' + return 'string()' } if (["date", "time", "timetz", "timestamp", "timestamptz"].includes(pgType)) { - return 'date' + return 'date()' } - console.info(`Unknown type ${pgType}`) + + console.info(`Basic Zod Type: Unknown type ${pgType}`) // Everything else is an enum - return "string" + return "string()" } +const IP_REGEX = "^((((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))|((([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))))\\/[0-9]{1,3}$" + function extractExtraZodMethods(column: PostgresColumn): string[] { const methods: string[] = [] @@ -240,6 +251,11 @@ function extractExtraZodMethods(column: PostgresColumn): string[] { methods.push(`enum([${column.enums.map((value) => `"${value}"`).join(', ')}] as const)`) } + if (column.format === "inet") { + // Zods `ip` method doesn't check for subnets, so we use our own regex instead. + methods.push(`regex(/${IP_REGEX}/)`) + } + return methods } From 871265223e8cd5bc06ff06a503fe0720806670e5 Mon Sep 17 00:00:00 2001 From: myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Wed, 22 Nov 2023 17:09:35 +0100 Subject: [PATCH 06/15] feat: Improve functions --- src/server/templates/zod.ts | 81 ++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 38 deletions(-) diff --git a/src/server/templates/zod.ts b/src/server/templates/zod.ts index 25525a8f..f631cb71 100644 --- a/src/server/templates/zod.ts +++ b/src/server/templates/zod.ts @@ -10,6 +10,31 @@ import {filterFromSchema, getSchemaFunctions} from "./_common.js"; type ColumnsPerTable = Record; +/** Create a zod object type for a table. + * You probably don't want to call this function, unless you're writing a custom template. + * Example: + * Given a table that looks like this: + * ```sql + * CREATE TABLE public.users ( + * id uuid NOT NULL, + * name text, + * email text NOT NULL, + * created_at timestamp without time zone NOT NULL DEFAULT now() + * ); + * The generated zod object would look like this: + * ```typescript + * const User = z.object({ + * // uuid + * id: z.number().regex(/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/), + * name: z.string().nullable(), + * email: z.string(), + * created_at: z.date().default(() => new Date()), + * }) + * ``` + * @param tableName - The name of the table; e.g. `users` will be snake cased and used as the variable name. + * @param columns - The columns of the table. + * @returns A zod object type. + */ export const apply = ({ schemas, tables, @@ -54,7 +79,6 @@ export const apply = ({ const schema = { ${schemas.map((schema) => `${schema.name}: ${writeSchema( - schema, filterFromSchema(tables, schema.name), columnsByTableId, getSchemaFunctions(functions, schema.name), @@ -72,7 +96,6 @@ export const apply = ({ } function writeSchema( - schema: PostgresSchema, availableTables: PostgresTable[], columnsByTableId: ColumnsPerTable, functions: PostgresFunction[], @@ -148,12 +171,18 @@ function writeFunctions( }, {} as Record) return `{ - ${Object.entries(schemaFunctionsGroupedByName).map(([name, functions]) => { + ${Object.entries(schemaFunctionsGroupedByName).map(([rawFnName, functions]) => { + const name = JSON.stringify(rawFnName) + if (functions.length === 1) { - return `"${functions[0].name}": ${writeFunction(functions[0], types, arrayTypes)}` + return `name: ${writeFunction(functions[0], types, arrayTypes)}` } - return "test: 1" + return ` + ${name}: z.union([ + ${functions.map((func) => writeFunction(func, types, arrayTypes)).join(',\n')} + ]) + ` }).join(',\n')} }` } @@ -178,7 +207,7 @@ function writeFunctionArg(arg: PostgresFunction['args'][0], types: PostgresType[ return "z." + basicZodType(type.format) + (arg.has_default ? '.optional()' : '') } - console.info(`Function: Unknown type ${arg.type_id}`) + console.debug(`Function: Unknown type ${arg.type_id}`) return `z.unknown()` + (arg.has_default ? '.optional()' : '') } @@ -208,18 +237,21 @@ function basicZodType(pgType: string): string { 'vector', 'json', 'jsonb', - 'inet' + 'inet', + 'cidr', + 'macaddr', + 'macaddr8', + 'character varying', ].includes(pgType) ) { return 'string()' } - if (["date", "time", "timetz", "timestamp", "timestamptz"].includes(pgType)) { + if (["date", "time", "timetz", "timestamp", "timestamptz", "timestamp with time zone"].includes(pgType)) { return 'date()' } - - console.info(`Basic Zod Type: Unknown type ${pgType}`) + console.debug(`Basic Zod Type: Unknown type ${pgType}`) // Everything else is an enum return "string()" @@ -232,7 +264,7 @@ function extractExtraZodMethods(column: PostgresColumn): string[] { // UUID if (column.format === "uuid") { - methods.push("regex(/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/)") + methods.push("regex(/^[0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12}$/)") if (column.default_value === "gen_random_uuid()") { methods.push("default(() => uuidv4())") @@ -291,30 +323,3 @@ function joinWithLeading(arr: T[], join: string): string { function uniq(arr: T[]): T[] { return [...new Set(arr)] } - -/** Create a zod object type for a table. - * You probably don't want to call this function, unless you're writing a custom template. - * Example: - * Given a table that looks like this: - * ```sql - * CREATE TABLE public.users ( - * id uuid NOT NULL, - * name text, - * email text NOT NULL, - * created_at timestamp without time zone NOT NULL DEFAULT now() - * ); - * The generated zod object would look like this: - * ```typescript - * const User = z.object({ - * // uuid - * id: z.number().regex(/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/), - * name: z.string().nullable(), - * email: z.string(), - * created_at: z.date().default(() => new Date()), - * }) - * ``` - * @param tableName - The name of the table; e.g. `users` will be snake cased and used as the variable name. - * @param columns - The columns of the table. - * @returns A zod object type. - */ -function createTableObject(tableName: string, columns: PostgresColumn[]) {} From c71a869882817baa9713b87ea3bebb3358b9f257 Mon Sep 17 00:00:00 2001 From: myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 23 Nov 2023 11:34:58 +0100 Subject: [PATCH 07/15] chore: Migrate typescript to common utils --- src/server/templates/_common.ts | 10 +++++++-- src/server/templates/typescript.ts | 36 +++++------------------------- src/server/templates/zod.ts | 13 +++-------- 3 files changed, 16 insertions(+), 43 deletions(-) diff --git a/src/server/templates/_common.ts b/src/server/templates/_common.ts index a09d1de2..6e271f83 100644 --- a/src/server/templates/_common.ts +++ b/src/server/templates/_common.ts @@ -1,10 +1,10 @@ -import {PostgresFunction, PostgresTable, PostgresType} from "../../lib/index.js"; +import {PostgresFunction, PostgresType} from "../../lib/index.js"; export const filterFromSchema = (items: T[], schemaName: string): T[] => { return items.filter((item) => item.schema === schemaName).sort(({name: a}, {name: b}) => a.localeCompare(b)) } -export const getSchemaFunctions = (functions: PostgresFunction[], schemaName: string): PostgresFunction[] => { +export const filterSchemaFunctions = (functions: PostgresFunction[], schemaName: string): PostgresFunction[] => { return functions .filter((func) => { if (func.schema !== schemaName) { @@ -20,3 +20,9 @@ export const getSchemaFunctions = (functions: PostgresFunction[], schemaName: st }) .sort(({ name: a }, { name: b }) => a.localeCompare(b)) } + +export const filterSchemaEnums = (types: PostgresType[], schemaName: string): PostgresType[] => + types + .filter((type) => type.schema === schemaName && type.enums.length > 0) + .sort(({ name: a }, { name: b }) => a.localeCompare(b)) + diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts index 96a30fe4..9a96aebf 100644 --- a/src/server/templates/typescript.ts +++ b/src/server/templates/typescript.ts @@ -9,6 +9,7 @@ import type { PostgresType, PostgresView, } from '../../lib/index.js' +import {filterFromSchema, filterSchemaEnums, filterSchemaFunctions} from "./_common.js"; export const apply = ({ schemas, @@ -48,37 +49,10 @@ export interface Database { ${schemas .sort(({ name: a }, { name: b }) => a.localeCompare(b)) .map((schema) => { - const schemaTables = tables - .filter((table) => table.schema === schema.name) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - const schemaViews = [...views, ...materializedViews] - .filter((view) => view.schema === schema.name) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - const schemaFunctions = functions - .filter((func) => { - if (func.schema !== schema.name) { - return false - } - - // Either: - // 1. All input args are be named, or - // 2. There is only one input arg which is unnamed - const inArgs = func.args.filter(({ mode }) => ['in', 'inout', 'variadic'].includes(mode)) - - if (!inArgs.some(({ name }) => name === '')) { - return true - } - - if (inArgs.length === 1) { - return true - } - - return false - }) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - const schemaEnums = types - .filter((type) => type.schema === schema.name && type.enums.length > 0) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) + const schemaTables = filterFromSchema(tables, schema.name) + const schemaViews = filterFromSchema([...views, ...materializedViews], schema.name) + const schemaFunctions = filterSchemaFunctions(functions, schema.name) + const schemaEnums = filterSchemaEnums(types, schema.name) const schemaCompositeTypes = types .filter((type) => type.schema === schema.name && type.attributes.length > 0) .sort(({ name: a }, { name: b }) => a.localeCompare(b)) diff --git a/src/server/templates/zod.ts b/src/server/templates/zod.ts index f631cb71..3e499581 100644 --- a/src/server/templates/zod.ts +++ b/src/server/templates/zod.ts @@ -6,7 +6,7 @@ import { PostgresView } from "../../lib/index.js"; import prettier from "prettier"; -import {filterFromSchema, getSchemaFunctions} from "./_common.js"; +import {filterFromSchema, filterSchemaEnums, filterSchemaFunctions} from "./_common.js"; type ColumnsPerTable = Record; @@ -66,13 +66,6 @@ export const apply = ({ return acc }, {} as ColumnsPerTable) - /* - Example: - ```typescript - public.tables.user.insert() - ``` - */ - const output = ` import * as z from 'zod' import { v4 as uuidv4 } from 'uuid' @@ -81,9 +74,9 @@ export const apply = ({ ${schemas.map((schema) => `${schema.name}: ${writeSchema( filterFromSchema(tables, schema.name), columnsByTableId, - getSchemaFunctions(functions, schema.name), + filterSchemaFunctions(functions, schema.name), filterFromSchema([...views, ...materializedViews], schema.name), - types, + filterSchemaEnums(types, schema.name), arrayTypes, )}`).join(',\n')} } From 33b9ea92942c1160f00c93e362e6aefc4d4fe885 Mon Sep 17 00:00:00 2001 From: myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 23 Nov 2023 11:41:43 +0100 Subject: [PATCH 08/15] fix: Improve enums --- src/server/templates/zod.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/server/templates/zod.ts b/src/server/templates/zod.ts index 3e499581..b6aeed06 100644 --- a/src/server/templates/zod.ts +++ b/src/server/templates/zod.ts @@ -72,11 +72,12 @@ export const apply = ({ const schema = { ${schemas.map((schema) => `${schema.name}: ${writeSchema( + schema.name, filterFromSchema(tables, schema.name), columnsByTableId, filterSchemaFunctions(functions, schema.name), filterFromSchema([...views, ...materializedViews], schema.name), - filterSchemaEnums(types, schema.name), + types, arrayTypes, )}`).join(',\n')} } @@ -89,6 +90,7 @@ export const apply = ({ } function writeSchema( + schemaName: string, availableTables: PostgresTable[], columnsByTableId: ColumnsPerTable, functions: PostgresFunction[], @@ -96,6 +98,8 @@ function writeSchema( types: PostgresType[], arrayTypes: PostgresType[], ): string { + const schemaEnums = filterSchemaEnums(types, schemaName) + return `{ tables: { ${availableTables.map(table => `${table.name}: { @@ -105,7 +109,7 @@ function writeSchema( }`)} }, enums: { - ${types.filter(enumType => enumType.enums.length > 0).map(enumType => `${enumType.name}: z.enum([${enumType.enums.map((value) => `"${value}"`).join(', ')}] as const)`).join(',\n')} + ${schemaEnums.filter(enumType => enumType.enums.length > 0).map(enumType => `${enumType.name}: z.enum([${enumType.enums.map((value) => `"${value}"`).join(', ')}] as const)`).join(',\n')} }, functions: ${writeFunctions(functions, types, arrayTypes)}, views: { @@ -168,7 +172,7 @@ function writeFunctions( const name = JSON.stringify(rawFnName) if (functions.length === 1) { - return `name: ${writeFunction(functions[0], types, arrayTypes)}` + return `${name}: ${writeFunction(functions[0], types, arrayTypes)}` } return ` From 537f4f74e1386bfea0a40c911064d9012fc0b74f Mon Sep 17 00:00:00 2001 From: myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 23 Nov 2023 11:50:23 +0100 Subject: [PATCH 09/15] fix: Improve uuid --- src/server/templates/zod.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/server/templates/zod.ts b/src/server/templates/zod.ts index b6aeed06..09eda7c3 100644 --- a/src/server/templates/zod.ts +++ b/src/server/templates/zod.ts @@ -255,16 +255,17 @@ function basicZodType(pgType: string): string { } const IP_REGEX = "^((((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))|((([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))))\\/[0-9]{1,3}$" +const UUID_REGEX = "^[0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12}$" function extractExtraZodMethods(column: PostgresColumn): string[] { const methods: string[] = [] // UUID if (column.format === "uuid") { - methods.push("regex(/^[0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12}$/)") + methods.push(`regex(/${UUID_REGEX}/)`) if (column.default_value === "gen_random_uuid()") { - methods.push("default(() => uuidv4())") + methods.push("default(uuidv4)") } } From 55e7763ddbf51c2b903342b05ec5a15df876eb88 Mon Sep 17 00:00:00 2001 From: myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 23 Nov 2023 18:06:31 +0100 Subject: [PATCH 10/15] fix: Improve generator --- src/server/templates/zod.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/server/templates/zod.ts b/src/server/templates/zod.ts index 09eda7c3..fa3c5160 100644 --- a/src/server/templates/zod.ts +++ b/src/server/templates/zod.ts @@ -68,7 +68,6 @@ export const apply = ({ const output = ` import * as z from 'zod' - import { v4 as uuidv4 } from 'uuid' const schema = { ${schemas.map((schema) => `${schema.name}: ${writeSchema( @@ -245,7 +244,7 @@ function basicZodType(pgType: string): string { } if (["date", "time", "timetz", "timestamp", "timestamptz", "timestamp with time zone"].includes(pgType)) { - return 'date()' + return "string()" } console.debug(`Basic Zod Type: Unknown type ${pgType}`) @@ -255,25 +254,22 @@ function basicZodType(pgType: string): string { } const IP_REGEX = "^((((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))|((([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))))\\/[0-9]{1,3}$" -const UUID_REGEX = "^[0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12}$" function extractExtraZodMethods(column: PostgresColumn): string[] { const methods: string[] = [] // UUID if (column.format === "uuid") { - methods.push(`regex(/${UUID_REGEX}/)`) + methods.push("uuid()") + } - if (column.default_value === "gen_random_uuid()") { - methods.push("default(uuidv4)") - } + // Dates + if (["date", "time"].includes(column.format)) { + methods.push("datetime()") } - // Date and time types - if (["date", "time", "timetz", "timestamp", "timestamptz"].includes(column.format)) { - if (column.default_value === "now()") { - methods.push("default(() => new Date())") - } + if (["timetz", "timestamp", "timestamptz", "timestamp with time zone"].includes(column.format)) { + methods.push("datetime({ offset: true })") } // Enums From 0fd64a500707d4fc4714b31abfc397d13b849401 Mon Sep 17 00:00:00 2001 From: myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 23 Nov 2023 18:13:31 +0100 Subject: [PATCH 11/15] fix: Improve generator --- src/server/templates/zod.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/server/templates/zod.ts b/src/server/templates/zod.ts index fa3c5160..6229de6b 100644 --- a/src/server/templates/zod.ts +++ b/src/server/templates/zod.ts @@ -69,6 +69,14 @@ export const apply = ({ const output = ` import * as z from 'zod' + // Used for JSON types, taken from https://github.com/colinhacks/zod#json-type + const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); + type Literal = z.infer; + type Json = Literal | { [key: string]: Json } | Json[]; + const jsonSchema: z.ZodType = z.lazy(() => + z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]) + ); + const schema = { ${schemas.map((schema) => `${schema.name}: ${writeSchema( schema.name, @@ -222,6 +230,11 @@ function basicZodType(pgType: string): string { return 'number()' } + + if (['json', 'jsonb'].includes(pgType)) { + return "object(jsonSchema)" + } + if ( [ 'bytea', @@ -231,8 +244,6 @@ function basicZodType(pgType: string): string { 'citext', 'uuid', 'vector', - 'json', - 'jsonb', 'inet', 'cidr', 'macaddr', @@ -240,7 +251,7 @@ function basicZodType(pgType: string): string { 'character varying', ].includes(pgType) ) { - return 'string()' + return "string()" } if (["date", "time", "timetz", "timestamp", "timestamptz", "timestamp with time zone"].includes(pgType)) { From bc087c78d2c6bdd5f403a34acb4137e320f6f279 Mon Sep 17 00:00:00 2001 From: myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 23 Nov 2023 18:14:02 +0100 Subject: [PATCH 12/15] apply linter --- src/server/templates/zod.ts | 462 ++++++++++++++++++++---------------- 1 file changed, 262 insertions(+), 200 deletions(-) diff --git a/src/server/templates/zod.ts b/src/server/templates/zod.ts index 6229de6b..f608722a 100644 --- a/src/server/templates/zod.ts +++ b/src/server/templates/zod.ts @@ -1,14 +1,17 @@ import { - PostgresColumn, PostgresFunction, - PostgresMaterializedView, PostgresRelationship, - PostgresSchema, - PostgresTable, PostgresType, - PostgresView -} from "../../lib/index.js"; -import prettier from "prettier"; -import {filterFromSchema, filterSchemaEnums, filterSchemaFunctions} from "./_common.js"; - -type ColumnsPerTable = Record; + PostgresColumn, + PostgresFunction, + PostgresMaterializedView, + PostgresRelationship, + PostgresSchema, + PostgresTable, + PostgresType, + PostgresView, +} from '../../lib/index.js' +import prettier from 'prettier' +import { filterFromSchema, filterSchemaEnums, filterSchemaFunctions } from './_common.js' + +type ColumnsPerTable = Record /** Create a zod object type for a table. * You probably don't want to call this function, unless you're writing a custom template. @@ -36,37 +39,37 @@ type ColumnsPerTable = Record; * @returns A zod object type. */ export const apply = ({ - schemas, - tables, - views, - materializedViews, - columns, - relationships, - functions, - types, - arrayTypes, - detectOneToOneRelationships, - }: { - schemas: PostgresSchema[] - tables: Omit[] - views: Omit[] - materializedViews: Omit[] - columns: PostgresColumn[] - relationships: PostgresRelationship[] - functions: PostgresFunction[] - types: PostgresType[] - arrayTypes: PostgresType[] - detectOneToOneRelationships: boolean + schemas, + tables, + views, + materializedViews, + columns, + relationships, + functions, + types, + arrayTypes, + detectOneToOneRelationships, +}: { + schemas: PostgresSchema[] + tables: Omit[] + views: Omit[] + materializedViews: Omit[] + columns: PostgresColumn[] + relationships: PostgresRelationship[] + functions: PostgresFunction[] + types: PostgresType[] + arrayTypes: PostgresType[] + detectOneToOneRelationships: boolean }): string => { - const columnsByTableId = columns - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - .reduce((acc, curr) => { - acc[curr.table_id] ??= [] - acc[curr.table_id].push(curr) - return acc - }, {} as ColumnsPerTable) - - const output = ` + const columnsByTableId = columns + .sort(({ name: a }, { name: b }) => a.localeCompare(b)) + .reduce((acc, curr) => { + acc[curr.table_id] ??= [] + acc[curr.table_id].push(curr) + return acc + }, {} as ColumnsPerTable) + + const output = ` import * as z from 'zod' // Used for JSON types, taken from https://github.com/colinhacks/zod#json-type @@ -78,247 +81,306 @@ export const apply = ({ ); const schema = { - ${schemas.map((schema) => `${schema.name}: ${writeSchema( - schema.name, - filterFromSchema(tables, schema.name), - columnsByTableId, - filterSchemaFunctions(functions, schema.name), - filterFromSchema([...views, ...materializedViews], schema.name), - types, - arrayTypes, - )}`).join(',\n')} + ${schemas + .map( + (schema) => + `${schema.name}: ${writeSchema( + schema.name, + filterFromSchema(tables, schema.name), + columnsByTableId, + filterSchemaFunctions(functions, schema.name), + filterFromSchema([...views, ...materializedViews], schema.name), + types, + arrayTypes + )}` + ) + .join(',\n')} } ` - return prettier.format(output, { - parser: 'typescript', - semi: false, - }) + return prettier.format(output, { + parser: 'typescript', + semi: false, + }) } function writeSchema( - schemaName: string, - availableTables: PostgresTable[], - columnsByTableId: ColumnsPerTable, - functions: PostgresFunction[], - views: PostgresView[], - types: PostgresType[], - arrayTypes: PostgresType[], + schemaName: string, + availableTables: PostgresTable[], + columnsByTableId: ColumnsPerTable, + functions: PostgresFunction[], + views: PostgresView[], + types: PostgresType[], + arrayTypes: PostgresType[] ): string { - const schemaEnums = filterSchemaEnums(types, schemaName) + const schemaEnums = filterSchemaEnums(types, schemaName) - return `{ + return `{ tables: { - ${availableTables.map(table => `${table.name}: { - row: ${writeRowTable(columnsByTableId[table.id], functions.filter(fn => fn.argument_types === table.name), types)}, + ${availableTables.map( + (table) => `${table.name}: { + row: ${writeRowTable( + columnsByTableId[table.id], + functions.filter((fn) => fn.argument_types === table.name), + types + )}, insert: ${writeInsertTable(columnsByTableId[table.id])}, update: ${writeUpdateTable(columnsByTableId[table.id])}, - }`)} + }` + )} }, enums: { - ${schemaEnums.filter(enumType => enumType.enums.length > 0).map(enumType => `${enumType.name}: z.enum([${enumType.enums.map((value) => `"${value}"`).join(', ')}] as const)`).join(',\n')} + ${schemaEnums + .filter((enumType) => enumType.enums.length > 0) + .map( + (enumType) => + `${enumType.name}: z.enum([${enumType.enums + .map((value) => `"${value}"`) + .join(', ')}] as const)` + ) + .join(',\n')} }, functions: ${writeFunctions(functions, types, arrayTypes)}, views: { - ${views.map(view => `${JSON.stringify(view.name)}: ${writeView(columnsByTableId[view.id])}`)} + ${views.map( + (view) => `${JSON.stringify(view.name)}: ${writeView(columnsByTableId[view.id])}` + )} } }` } -function writeRowTable(columns: PostgresColumn[], readFunctions: PostgresFunction[], types: PostgresType[]): string { - return `z.object({ +function writeRowTable( + columns: PostgresColumn[], + readFunctions: PostgresFunction[], + types: PostgresType[] +): string { + return `z.object({ ${columns.map((column) => `"${column.name}": ${writeColumn(column)}`).join(',\n')}, - ${readFunctions.map((func) => `"${func.name}": ${writeReadFunction(func, types)}`).join(',\n')} + ${readFunctions + .map((func) => `"${func.name}": ${writeReadFunction(func, types)}`) + .join(',\n')} })` } function writeInsertTable(columns: PostgresColumn[]): string { - return `z.object({ - ${columns.filter(column => column.identity_generation !== "ALWAYS").map((column) => `"${column.name}": ${writeColumn(column)}`).join(',\n')}, + return `z.object({ + ${columns + .filter((column) => column.identity_generation !== 'ALWAYS') + .map((column) => `"${column.name}": ${writeColumn(column)}`) + .join(',\n')}, })` } function writeUpdateTable(columns: PostgresColumn[]): string { - return `z.object({ + return `z.object({ ${columns - .filter(column => column.identity_generation !== "ALWAYS") - .map((column) => `"${column.name}": z.${basicZodType(column.format)}${joinWithLeading(uniq([...extractGeneralZodMethods(column), "optional()"]), ".")}${joinWithLeading(extractExtraZodMethods(column), ".")}`).join(',\n')}, + .filter((column) => column.identity_generation !== 'ALWAYS') + .map( + (column) => + `"${column.name}": z.${basicZodType(column.format)}${joinWithLeading( + uniq([...extractGeneralZodMethods(column), 'optional()']), + '.' + )}${joinWithLeading(extractExtraZodMethods(column), '.')}` + ) + .join(',\n')}, })` } function writeColumn(column: PostgresColumn): string { - return `z.${basicZodType(column.format)}${joinWithLeading(extractGeneralZodMethods(column), ".")}${joinWithLeading(extractExtraZodMethods(column), ".")}` + return `z.${basicZodType(column.format)}${joinWithLeading( + extractGeneralZodMethods(column), + '.' + )}${joinWithLeading(extractExtraZodMethods(column), '.')}` } function writeView(columns: PostgresColumn[]): string { - return `z.object({ - ${columns.filter(column => column.is_updatable).map((column) => `${JSON.stringify(column.name)}: ${writeColumn(column)}`).join(',\n')} + return `z.object({ + ${columns + .filter((column) => column.is_updatable) + .map((column) => `${JSON.stringify(column.name)}: ${writeColumn(column)}`) + .join(',\n')} })` } function writeReadFunction(func: PostgresFunction, types: PostgresType[]): string { - const type = types.find(({ id }) => id === func.return_type_id) - const zodType = type ? basicZodType(type.format) : 'unknown' + const type = types.find(({ id }) => id === func.return_type_id) + const zodType = type ? basicZodType(type.format) : 'unknown' - return `z.${zodType}().nullable()` + return `z.${zodType}().nullable()` } function writeFunctions( - functions: PostgresFunction[], - types: PostgresType[], - arrayTypes: PostgresType[], + functions: PostgresFunction[], + types: PostgresType[], + arrayTypes: PostgresType[] ): string { - const schemaFunctionsGroupedByName = functions.reduce((acc, curr) => { - acc[curr.name] ??= [] - acc[curr.name].push(curr) - return acc - }, {} as Record) - - return `{ - ${Object.entries(schemaFunctionsGroupedByName).map(([rawFnName, functions]) => { + const schemaFunctionsGroupedByName = functions.reduce((acc, curr) => { + acc[curr.name] ??= [] + acc[curr.name].push(curr) + return acc + }, {} as Record) + + return `{ + ${Object.entries(schemaFunctionsGroupedByName) + .map(([rawFnName, functions]) => { const name = JSON.stringify(rawFnName) - + if (functions.length === 1) { - return `${name}: ${writeFunction(functions[0], types, arrayTypes)}` + return `${name}: ${writeFunction(functions[0], types, arrayTypes)}` } - + return ` ${name}: z.union([ ${functions.map((func) => writeFunction(func, types, arrayTypes)).join(',\n')} ]) ` - }).join(',\n')} + }) + .join(',\n')} }` } -function writeFunction(func: PostgresFunction, types: PostgresType[], arrayTypes: PostgresType[]): string { - const inArgs = func.args.filter(({ mode }) => mode === 'in') +function writeFunction( + func: PostgresFunction, + types: PostgresType[], + arrayTypes: PostgresType[] +): string { + const inArgs = func.args.filter(({ mode }) => mode === 'in') - return `z.object({ - ${inArgs.map(arg => `${JSON.stringify(arg.name)}: ${writeFunctionArg(arg, types, arrayTypes)}`).join(',\n')} + return `z.object({ + ${inArgs + .map((arg) => `${JSON.stringify(arg.name)}: ${writeFunctionArg(arg, types, arrayTypes)}`) + .join(',\n')} })` } -function writeFunctionArg(arg: PostgresFunction['args'][0], types: PostgresType[], arrayTypes: PostgresType[]): string { - let type = arrayTypes.find(({ id }) => id === arg.type_id) - if (type) { - // If it's an array type, the name looks like `_int8`. - const elementTypeName = type.name.substring(1) - return `z.array(z.${basicZodType(elementTypeName)})` + (arg.has_default ? '.optional()' : '') - } - type = types.find(({ id }) => id === arg.type_id) - if (type) { - return "z." + basicZodType(type.format) + (arg.has_default ? '.optional()' : '') - } - - console.debug(`Function: Unknown type ${arg.type_id}`) - - return `z.unknown()` + (arg.has_default ? '.optional()' : '') +function writeFunctionArg( + arg: PostgresFunction['args'][0], + types: PostgresType[], + arrayTypes: PostgresType[] +): string { + let type = arrayTypes.find(({ id }) => id === arg.type_id) + if (type) { + // If it's an array type, the name looks like `_int8`. + const elementTypeName = type.name.substring(1) + return `z.array(z.${basicZodType(elementTypeName)})` + (arg.has_default ? '.optional()' : '') + } + type = types.find(({ id }) => id === arg.type_id) + if (type) { + return 'z.' + basicZodType(type.format) + (arg.has_default ? '.optional()' : '') + } + + console.debug(`Function: Unknown type ${arg.type_id}`) + + return `z.unknown()` + (arg.has_default ? '.optional()' : '') } function basicZodType(pgType: string): string { - // Array - if (pgType.startsWith("_")) { - return basicZodType(pgType.substring(1)) + ".array()" - } - - if ([ 'bool', 'boolean' ].includes(pgType)) { - return "boolean()" - } - - if (['int2', 'int4', 'int8', 'float4', 'float8', 'numeric', 'integer', 'bigint', 'oid'].includes(pgType)) { - return 'number()' - } - - - if (['json', 'jsonb'].includes(pgType)) { - return "object(jsonSchema)" - } - - if ( - [ - 'bytea', - 'bpchar', - 'varchar', - 'text', - 'citext', - 'uuid', - 'vector', - 'inet', - 'cidr', - 'macaddr', - 'macaddr8', - 'character varying', - ].includes(pgType) - ) { - return "string()" - } - - if (["date", "time", "timetz", "timestamp", "timestamptz", "timestamp with time zone"].includes(pgType)) { - return "string()" - } - - console.debug(`Basic Zod Type: Unknown type ${pgType}`) - - // Everything else is an enum - return "string()" + // Array + if (pgType.startsWith('_')) { + return basicZodType(pgType.substring(1)) + '.array()' + } + + if (['bool', 'boolean'].includes(pgType)) { + return 'boolean()' + } + + if ( + ['int2', 'int4', 'int8', 'float4', 'float8', 'numeric', 'integer', 'bigint', 'oid'].includes( + pgType + ) + ) { + return 'number()' + } + + if (['json', 'jsonb'].includes(pgType)) { + return 'object(jsonSchema)' + } + + if ( + [ + 'bytea', + 'bpchar', + 'varchar', + 'text', + 'citext', + 'uuid', + 'vector', + 'inet', + 'cidr', + 'macaddr', + 'macaddr8', + 'character varying', + ].includes(pgType) + ) { + return 'string()' + } + + if ( + ['date', 'time', 'timetz', 'timestamp', 'timestamptz', 'timestamp with time zone'].includes( + pgType + ) + ) { + return 'string()' + } + + console.debug(`Basic Zod Type: Unknown type ${pgType}`) + + // Everything else is an enum + return 'string()' } -const IP_REGEX = "^((((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))|((([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))))\\/[0-9]{1,3}$" +const IP_REGEX = + '^((((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))|((([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))))\\/[0-9]{1,3}$' function extractExtraZodMethods(column: PostgresColumn): string[] { - const methods: string[] = [] + const methods: string[] = [] - // UUID - if (column.format === "uuid") { - methods.push("uuid()") - } + // UUID + if (column.format === 'uuid') { + methods.push('uuid()') + } - // Dates - if (["date", "time"].includes(column.format)) { - methods.push("datetime()") - } + // Dates + if (['date', 'time'].includes(column.format)) { + methods.push('datetime()') + } - if (["timetz", "timestamp", "timestamptz", "timestamp with time zone"].includes(column.format)) { - methods.push("datetime({ offset: true })") - } + if (['timetz', 'timestamp', 'timestamptz', 'timestamp with time zone'].includes(column.format)) { + methods.push('datetime({ offset: true })') + } - // Enums - if (column.data_type === "USER-DEFINED") { - methods.push(`enum([${column.enums.map((value) => `"${value}"`).join(', ')}] as const)`) - } + // Enums + if (column.data_type === 'USER-DEFINED') { + methods.push(`enum([${column.enums.map((value) => `"${value}"`).join(', ')}] as const)`) + } - if (column.format === "inet") { - // Zods `ip` method doesn't check for subnets, so we use our own regex instead. - methods.push(`regex(/${IP_REGEX}/)`) - } + if (column.format === 'inet') { + // Zods `ip` method doesn't check for subnets, so we use our own regex instead. + methods.push(`regex(/${IP_REGEX}/)`) + } - return methods + return methods } function extractGeneralZodMethods(column: PostgresColumn): string[] { - const methods: string[] = [] - - if ( - column.is_nullable || - column.is_identity || - column.default_value !== null - ) { - methods.push("optional()") - } - if (column.is_nullable) { - methods.push("nullable()") - } + const methods: string[] = [] - return methods + if (column.is_nullable || column.is_identity || column.default_value !== null) { + methods.push('optional()') + } + if (column.is_nullable) { + methods.push('nullable()') + } + + return methods } function joinWithLeading(arr: T[], join: string): string { - if (arr.length === 0) { - return "" - } + if (arr.length === 0) { + return '' + } - return join + arr.join(join) + return join + arr.join(join) } /** Remove duplicate values from an array. @@ -326,5 +388,5 @@ function joinWithLeading(arr: T[], join: string): string { * @param arr - The array to remove duplicates from. */ function uniq(arr: T[]): T[] { - return [...new Set(arr)] + return [...new Set(arr)] } From 5c7088643ed2a14d6e9ba0ccac011747c482774e Mon Sep 17 00:00:00 2001 From: myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 27 Nov 2023 10:27:58 +0100 Subject: [PATCH 13/15] feat: Add exports --- src/server/templates/zod.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server/templates/zod.ts b/src/server/templates/zod.ts index f608722a..c3fbd625 100644 --- a/src/server/templates/zod.ts +++ b/src/server/templates/zod.ts @@ -75,12 +75,12 @@ export const apply = ({ // Used for JSON types, taken from https://github.com/colinhacks/zod#json-type const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); type Literal = z.infer; - type Json = Literal | { [key: string]: Json } | Json[]; - const jsonSchema: z.ZodType = z.lazy(() => + export type Json = Literal | { [key: string]: Json } | Json[]; + export const jsonSchema: z.ZodType = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]) ); - const schema = { + export const schema = { ${schemas .map( (schema) => From e3b639bc33cc6c47cbc19dbdef5c1aeb657fa719 Mon Sep 17 00:00:00 2001 From: myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 27 Nov 2023 10:56:53 +0100 Subject: [PATCH 14/15] fix: Fix generator --- src/server/templates/zod.ts | 46 ++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/src/server/templates/zod.ts b/src/server/templates/zod.ts index c3fbd625..767c9aad 100644 --- a/src/server/templates/zod.ts +++ b/src/server/templates/zod.ts @@ -177,20 +177,27 @@ function writeUpdateTable(columns: PostgresColumn[]): string { .filter((column) => column.identity_generation !== 'ALWAYS') .map( (column) => - `"${column.name}": z.${basicZodType(column.format)}${joinWithLeading( - uniq([...extractGeneralZodMethods(column), 'optional()']), - '.' - )}${joinWithLeading(extractExtraZodMethods(column), '.')}` + column.name + + ': z' + + // 'basicZodType' returns an empty string for enums, so we need to check for that. + (basicZodType(column.format) ? '.' : '') + + basicZodType(column.format) + + joinWithLeading(extractExtraZodMethods(column), '.') + + joinWithLeading(uniq([...extractGeneralZodMethods(column), 'optional()']), '.') ) .join(',\n')}, })` } function writeColumn(column: PostgresColumn): string { - return `z.${basicZodType(column.format)}${joinWithLeading( - extractGeneralZodMethods(column), - '.' - )}${joinWithLeading(extractExtraZodMethods(column), '.')}` + return ( + 'z' + + // 'basicZodType' returns an empty string for enums, so we need to check for that. + (basicZodType(column.format) ? '.' : '') + + basicZodType(column.format) + + joinWithLeading(extractExtraZodMethods(column), '.') + + joinWithLeading(extractGeneralZodMethods(column), '.') + ) } function writeView(columns: PostgresColumn[]): string { @@ -204,7 +211,7 @@ function writeView(columns: PostgresColumn[]): string { function writeReadFunction(func: PostgresFunction, types: PostgresType[]): string { const type = types.find(({ id }) => id === func.return_type_id) - const zodType = type ? basicZodType(type.format) : 'unknown' + const zodType = type ? basicZodType(type.format) || 'unknown' : 'unknown' return `z.${zodType}().nullable()` } @@ -262,11 +269,15 @@ function writeFunctionArg( if (type) { // If it's an array type, the name looks like `_int8`. const elementTypeName = type.name.substring(1) - return `z.array(z.${basicZodType(elementTypeName)})` + (arg.has_default ? '.optional()' : '') + return ( + `z.array(z.${basicZodType(elementTypeName) || 'unknown'})` + + (arg.has_default ? '.optional()' : '') + ) } type = types.find(({ id }) => id === arg.type_id) if (type) { - return 'z.' + basicZodType(type.format) + (arg.has_default ? '.optional()' : '') + const func = basicZodType(type.format) + return 'z.' + (func || 'unknown()') + (arg.has_default ? '.optional()' : '') } console.debug(`Function: Unknown type ${arg.type_id}`) @@ -277,7 +288,9 @@ function writeFunctionArg( function basicZodType(pgType: string): string { // Array if (pgType.startsWith('_')) { - return basicZodType(pgType.substring(1)) + '.array()' + const subtype = basicZodType(pgType.substring(1)) + + return subtype ? 'array(' + subtype + ')' : '' } if (['bool', 'boolean'].includes(pgType)) { @@ -326,7 +339,8 @@ function basicZodType(pgType: string): string { console.debug(`Basic Zod Type: Unknown type ${pgType}`) // Everything else is an enum - return 'string()' + // Enums are handled in `extractExtraZodMethods`. + return '' } const IP_REGEX = @@ -365,12 +379,12 @@ function extractExtraZodMethods(column: PostgresColumn): string[] { function extractGeneralZodMethods(column: PostgresColumn): string[] { const methods: string[] = [] - if (column.is_nullable || column.is_identity || column.default_value !== null) { - methods.push('optional()') - } if (column.is_nullable) { methods.push('nullable()') } + if (column.is_nullable || column.is_identity || column.default_value !== null) { + methods.push('optional()') + } return methods } From 7cff3155881b0e4cdc3d94496130967ac69f4e76 Mon Sep 17 00:00:00 2001 From: myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 30 Nov 2023 17:01:50 +0100 Subject: [PATCH 15/15] feat: Add description --- src/server/templates/zod.ts | 86 +++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 32 deletions(-) diff --git a/src/server/templates/zod.ts b/src/server/templates/zod.ts index 767c9aad..27d7629b 100644 --- a/src/server/templates/zod.ts +++ b/src/server/templates/zod.ts @@ -155,7 +155,7 @@ function writeRowTable( types: PostgresType[] ): string { return `z.object({ - ${columns.map((column) => `"${column.name}": ${writeColumn(column)}`).join(',\n')}, + ${columns.map((column) => `"${column.name}": ${writeRowColumn(column)}`).join(',\n')}, ${readFunctions .map((func) => `"${func.name}": ${writeReadFunction(func, types)}`) .join(',\n')} @@ -175,31 +175,44 @@ function writeUpdateTable(columns: PostgresColumn[]): string { return `z.object({ ${columns .filter((column) => column.identity_generation !== 'ALWAYS') - .map( - (column) => + .map((column) => { + const extra = joinWithLeading(extractExtraZodMethods(column), '.') + const general = joinWithLeading( + uniq([...extractGeneralZodMethods(column), 'optional()']), + '.' + ) + + return ( + '"' + column.name + - ': z' + - // 'basicZodType' returns an empty string for enums, so we need to check for that. - (basicZodType(column.format) ? '.' : '') + - basicZodType(column.format) + - joinWithLeading(extractExtraZodMethods(column), '.') + - joinWithLeading(uniq([...extractGeneralZodMethods(column), 'optional()']), '.') - ) + '"' + + ': ' + + basicZodType(column.format, !extra ? 'z.string()' : 'z') + + extra + + general + ) + }) .join(',\n')}, })` } -function writeColumn(column: PostgresColumn): string { +function writeRowColumn(column: PostgresColumn): string { + const extra = joinWithLeading(extractExtraZodMethods(column), '.') + return ( - 'z' + - // 'basicZodType' returns an empty string for enums, so we need to check for that. - (basicZodType(column.format) ? '.' : '') + - basicZodType(column.format) + - joinWithLeading(extractExtraZodMethods(column), '.') + - joinWithLeading(extractGeneralZodMethods(column), '.') + basicZodType(column.format, !extra ? 'z.string()' : 'z') + + extra + + (column.is_nullable ? '.nullable()' : '') ) } +function writeColumn(column: PostgresColumn): string { + const extra = joinWithLeading(extractExtraZodMethods(column), '.') + const general = joinWithLeading(extractGeneralZodMethods(column), '.') + + return basicZodType(column.format, !extra ? 'z.string()' : 'z') + extra + general +} + function writeView(columns: PostgresColumn[]): string { return `z.object({ ${columns @@ -211,9 +224,9 @@ function writeView(columns: PostgresColumn[]): string { function writeReadFunction(func: PostgresFunction, types: PostgresType[]): string { const type = types.find(({ id }) => id === func.return_type_id) - const zodType = type ? basicZodType(type.format) || 'unknown' : 'unknown' + const zodType = type ? basicZodType(type.format, 'z.unknown()') : 'unknown' - return `z.${zodType}().nullable()` + return zodType } function writeFunctions( @@ -270,14 +283,13 @@ function writeFunctionArg( // If it's an array type, the name looks like `_int8`. const elementTypeName = type.name.substring(1) return ( - `z.array(z.${basicZodType(elementTypeName) || 'unknown'})` + + `z.array(${basicZodType(elementTypeName, 'z.unknown()')})` + (arg.has_default ? '.optional()' : '') ) } type = types.find(({ id }) => id === arg.type_id) if (type) { - const func = basicZodType(type.format) - return 'z.' + (func || 'unknown()') + (arg.has_default ? '.optional()' : '') + return basicZodType(type.format, 'z.unknown()') + (arg.has_default ? '.optional()' : '') } console.debug(`Function: Unknown type ${arg.type_id}`) @@ -285,16 +297,16 @@ function writeFunctionArg( return `z.unknown()` + (arg.has_default ? '.optional()' : '') } -function basicZodType(pgType: string): string { +function basicZodType(pgType: string, enumFallback = 'z', fallback = 'z.unknown()'): string { // Array if (pgType.startsWith('_')) { - const subtype = basicZodType(pgType.substring(1)) + const subtype = basicZodType(pgType.substring(1), 'z.unknown()') - return subtype ? 'array(' + subtype + ')' : '' + return subtype ? 'z.array(' + subtype + ')' : fallback } if (['bool', 'boolean'].includes(pgType)) { - return 'boolean()' + return 'z.boolean()' } if ( @@ -302,11 +314,11 @@ function basicZodType(pgType: string): string { pgType ) ) { - return 'number()' + return 'z.number()' } if (['json', 'jsonb'].includes(pgType)) { - return 'object(jsonSchema)' + return 'jsonSchema' } if ( @@ -325,7 +337,7 @@ function basicZodType(pgType: string): string { 'character varying', ].includes(pgType) ) { - return 'string()' + return 'z.string()' } if ( @@ -333,14 +345,15 @@ function basicZodType(pgType: string): string { pgType ) ) { - return 'string()' + return 'z.string()' } console.debug(`Basic Zod Type: Unknown type ${pgType}`) // Everything else is an enum // Enums are handled in `extractExtraZodMethods`. - return '' + // dot is automatically added by the joinWithLeading function + return enumFallback } const IP_REGEX = @@ -364,7 +377,7 @@ function extractExtraZodMethods(column: PostgresColumn): string[] { } // Enums - if (column.data_type === 'USER-DEFINED') { + if (column.data_type === 'USER-DEFINED' && column.enums.length > 0) { methods.push(`enum([${column.enums.map((value) => `"${value}"`).join(', ')}] as const)`) } @@ -373,6 +386,10 @@ function extractExtraZodMethods(column: PostgresColumn): string[] { methods.push(`regex(/${IP_REGEX}/)`) } + if (column.comment) { + methods.push(`describe("${escapeString(column.comment)}")`) + } + return methods } @@ -404,3 +421,8 @@ function joinWithLeading(arr: T[], join: string): string { function uniq(arr: T[]): T[] { return [...new Set(arr)] } + +// Replaces " with \" +function escapeString(str: string): string { + return str.replace(/"/g, '\\"') +}