@sqldoc/templates 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +161 -0
- package/src/__tests__/dedent.test.ts +45 -0
- package/src/__tests__/docker-templates.test.ts +134 -0
- package/src/__tests__/go-structs.test.ts +184 -0
- package/src/__tests__/naming.test.ts +48 -0
- package/src/__tests__/python-dataclasses.test.ts +185 -0
- package/src/__tests__/rust-structs.test.ts +176 -0
- package/src/__tests__/tags-helpers.test.ts +72 -0
- package/src/__tests__/type-mapping.test.ts +332 -0
- package/src/__tests__/typescript.test.ts +202 -0
- package/src/cobol-copybook/index.ts +220 -0
- package/src/cobol-copybook/test/.gitignore +6 -0
- package/src/cobol-copybook/test/Dockerfile +7 -0
- package/src/csharp-records/index.ts +131 -0
- package/src/csharp-records/test/.gitignore +6 -0
- package/src/csharp-records/test/Dockerfile +6 -0
- package/src/diesel/index.ts +247 -0
- package/src/diesel/test/.gitignore +6 -0
- package/src/diesel/test/Dockerfile +16 -0
- package/src/drizzle/index.ts +255 -0
- package/src/drizzle/test/.gitignore +6 -0
- package/src/drizzle/test/Dockerfile +8 -0
- package/src/drizzle/test/test.ts +71 -0
- package/src/efcore/index.ts +190 -0
- package/src/efcore/test/.gitignore +6 -0
- package/src/efcore/test/Dockerfile +7 -0
- package/src/go-structs/index.ts +119 -0
- package/src/go-structs/test/.gitignore +6 -0
- package/src/go-structs/test/Dockerfile +13 -0
- package/src/go-structs/test/test.go +71 -0
- package/src/gorm/index.ts +134 -0
- package/src/gorm/test/.gitignore +6 -0
- package/src/gorm/test/Dockerfile +13 -0
- package/src/gorm/test/test.go +65 -0
- package/src/helpers/atlas.ts +43 -0
- package/src/helpers/enrich.ts +396 -0
- package/src/helpers/naming.ts +19 -0
- package/src/helpers/tags.ts +63 -0
- package/src/index.ts +24 -0
- package/src/java-records/index.ts +179 -0
- package/src/java-records/test/.gitignore +6 -0
- package/src/java-records/test/Dockerfile +11 -0
- package/src/java-records/test/Test.java +93 -0
- package/src/jpa/index.ts +279 -0
- package/src/jpa/test/.gitignore +6 -0
- package/src/jpa/test/Dockerfile +14 -0
- package/src/jpa/test/Test.java +111 -0
- package/src/json-schema/index.ts +351 -0
- package/src/json-schema/test/.gitignore +6 -0
- package/src/json-schema/test/Dockerfile +18 -0
- package/src/knex/index.ts +168 -0
- package/src/knex/test/.gitignore +6 -0
- package/src/knex/test/Dockerfile +7 -0
- package/src/knex/test/test.ts +75 -0
- package/src/kotlin-data/index.ts +147 -0
- package/src/kotlin-data/test/.gitignore +6 -0
- package/src/kotlin-data/test/Dockerfile +14 -0
- package/src/kotlin-data/test/Test.kt +82 -0
- package/src/kysely/index.ts +165 -0
- package/src/kysely/test/.gitignore +6 -0
- package/src/kysely/test/Dockerfile +8 -0
- package/src/kysely/test/test.ts +82 -0
- package/src/prisma/index.ts +387 -0
- package/src/prisma/test/.gitignore +6 -0
- package/src/prisma/test/Dockerfile +7 -0
- package/src/protobuf/index.ts +219 -0
- package/src/protobuf/test/.gitignore +6 -0
- package/src/protobuf/test/Dockerfile +6 -0
- package/src/pydantic/index.ts +272 -0
- package/src/pydantic/test/.gitignore +6 -0
- package/src/pydantic/test/Dockerfile +8 -0
- package/src/pydantic/test/test.py +63 -0
- package/src/python-dataclasses/index.ts +217 -0
- package/src/python-dataclasses/test/.gitignore +6 -0
- package/src/python-dataclasses/test/Dockerfile +8 -0
- package/src/python-dataclasses/test/test.py +63 -0
- package/src/rust-structs/index.ts +152 -0
- package/src/rust-structs/test/.gitignore +6 -0
- package/src/rust-structs/test/Dockerfile +22 -0
- package/src/rust-structs/test/test.rs +82 -0
- package/src/sqlalchemy/index.ts +258 -0
- package/src/sqlalchemy/test/.gitignore +6 -0
- package/src/sqlalchemy/test/Dockerfile +8 -0
- package/src/sqlalchemy/test/test.py +61 -0
- package/src/sqlc/index.ts +148 -0
- package/src/sqlc/test/.gitignore +6 -0
- package/src/sqlc/test/Dockerfile +13 -0
- package/src/sqlc/test/test.go +91 -0
- package/src/tags/dedent.ts +28 -0
- package/src/tags/index.ts +14 -0
- package/src/types/index.ts +8 -0
- package/src/types/pg-to-csharp.ts +136 -0
- package/src/types/pg-to-go.ts +120 -0
- package/src/types/pg-to-java.ts +141 -0
- package/src/types/pg-to-kotlin.ts +119 -0
- package/src/types/pg-to-python.ts +120 -0
- package/src/types/pg-to-rust.ts +121 -0
- package/src/types/pg-to-ts.ts +173 -0
- package/src/typescript/index.ts +168 -0
- package/src/typescript/test/.gitignore +6 -0
- package/src/typescript/test/Dockerfile +8 -0
- package/src/typescript/test/test.ts +89 -0
- package/src/xsd/index.ts +191 -0
- package/src/xsd/test/.gitignore +6 -0
- package/src/xsd/test/Dockerfile +6 -0
- package/src/zod/index.ts +289 -0
- package/src/zod/test/.gitignore +6 -0
- package/src/zod/test/Dockerfile +6 -0
package/src/zod/index.ts
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { defineTemplate } from '@sqldoc/ns-codegen'
|
|
2
|
+
import { activeTables, enrichRealm, type TagEntry } from '../helpers/enrich.ts'
|
|
3
|
+
import { toCamelCase, toPascalCase } from '../helpers/naming.ts'
|
|
4
|
+
|
|
5
|
+
export const configSchema = {
|
|
6
|
+
dateType: {
|
|
7
|
+
type: 'enum',
|
|
8
|
+
values: ['Date', 'dayjs', 'luxon', 'string'],
|
|
9
|
+
description: 'How to represent date/time types',
|
|
10
|
+
},
|
|
11
|
+
nullableStyle: {
|
|
12
|
+
type: 'enum',
|
|
13
|
+
values: ['optional', 'null-union'],
|
|
14
|
+
description: 'Use .optional() or .nullable() for nullable columns',
|
|
15
|
+
},
|
|
16
|
+
bigintType: {
|
|
17
|
+
type: 'enum',
|
|
18
|
+
values: ['number', 'bigint', 'string'],
|
|
19
|
+
description: 'How to represent bigint/bigserial columns',
|
|
20
|
+
},
|
|
21
|
+
strict: {
|
|
22
|
+
type: 'boolean',
|
|
23
|
+
description: 'Enable strict mode for Zod schemas',
|
|
24
|
+
},
|
|
25
|
+
} as const
|
|
26
|
+
|
|
27
|
+
/** Map a PostgreSQL type to a Zod schema call */
|
|
28
|
+
function pgToZod(pgType: string): string {
|
|
29
|
+
const normalized = pgType.toLowerCase().trim()
|
|
30
|
+
|
|
31
|
+
// Handle arrays
|
|
32
|
+
if (normalized.endsWith('[]')) {
|
|
33
|
+
return `z.array(${pgToZod(normalized.slice(0, -2))})`
|
|
34
|
+
}
|
|
35
|
+
if (normalized.startsWith('_')) {
|
|
36
|
+
return `z.array(${pgToZod(normalized.slice(1))})`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Strip length specifiers
|
|
40
|
+
const baseType = normalized.replace(/\(\d+(?:,\s*\d+)?\)/, '').trim()
|
|
41
|
+
|
|
42
|
+
const map: Record<string, string> = {
|
|
43
|
+
// Numeric
|
|
44
|
+
smallint: 'z.number()',
|
|
45
|
+
int2: 'z.number()',
|
|
46
|
+
integer: 'z.number()',
|
|
47
|
+
int: 'z.number()',
|
|
48
|
+
int4: 'z.number()',
|
|
49
|
+
bigint: 'z.number()',
|
|
50
|
+
int8: 'z.number()',
|
|
51
|
+
serial: 'z.number()',
|
|
52
|
+
serial4: 'z.number()',
|
|
53
|
+
bigserial: 'z.number()',
|
|
54
|
+
serial8: 'z.number()',
|
|
55
|
+
smallserial: 'z.number()',
|
|
56
|
+
serial2: 'z.number()',
|
|
57
|
+
real: 'z.number()',
|
|
58
|
+
float4: 'z.number()',
|
|
59
|
+
'double precision': 'z.number()',
|
|
60
|
+
float8: 'z.number()',
|
|
61
|
+
numeric: 'z.string()',
|
|
62
|
+
decimal: 'z.string()',
|
|
63
|
+
money: 'z.string()',
|
|
64
|
+
|
|
65
|
+
// String
|
|
66
|
+
text: 'z.string()',
|
|
67
|
+
varchar: 'z.string()',
|
|
68
|
+
'character varying': 'z.string()',
|
|
69
|
+
char: 'z.string()',
|
|
70
|
+
character: 'z.string()',
|
|
71
|
+
name: 'z.string()',
|
|
72
|
+
citext: 'z.string()',
|
|
73
|
+
|
|
74
|
+
// Boolean
|
|
75
|
+
boolean: 'z.boolean()',
|
|
76
|
+
bool: 'z.boolean()',
|
|
77
|
+
|
|
78
|
+
// Date/Time
|
|
79
|
+
timestamp: 'z.date()',
|
|
80
|
+
'timestamp without time zone': 'z.date()',
|
|
81
|
+
timestamptz: 'z.date()',
|
|
82
|
+
'timestamp with time zone': 'z.date()',
|
|
83
|
+
date: 'z.string()',
|
|
84
|
+
time: 'z.string()',
|
|
85
|
+
'time without time zone': 'z.string()',
|
|
86
|
+
timetz: 'z.string()',
|
|
87
|
+
'time with time zone': 'z.string()',
|
|
88
|
+
interval: 'z.string()',
|
|
89
|
+
|
|
90
|
+
// Binary
|
|
91
|
+
bytea: 'z.instanceof(Buffer)',
|
|
92
|
+
|
|
93
|
+
// JSON
|
|
94
|
+
json: 'z.unknown()',
|
|
95
|
+
jsonb: 'z.unknown()',
|
|
96
|
+
|
|
97
|
+
// UUID
|
|
98
|
+
uuid: 'z.string().uuid()',
|
|
99
|
+
|
|
100
|
+
// Network
|
|
101
|
+
inet: 'z.string()',
|
|
102
|
+
cidr: 'z.string()',
|
|
103
|
+
macaddr: 'z.string()',
|
|
104
|
+
macaddr8: 'z.string()',
|
|
105
|
+
|
|
106
|
+
// Other
|
|
107
|
+
xml: 'z.string()',
|
|
108
|
+
tsvector: 'z.string()',
|
|
109
|
+
tsquery: 'z.string()',
|
|
110
|
+
oid: 'z.number()',
|
|
111
|
+
point: 'z.string()',
|
|
112
|
+
line: 'z.string()',
|
|
113
|
+
box: 'z.string()',
|
|
114
|
+
circle: 'z.string()',
|
|
115
|
+
polygon: 'z.string()',
|
|
116
|
+
path: 'z.string()',
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return map[baseType] ?? 'z.unknown()'
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Extract @validate modifiers and append Zod chain calls */
|
|
123
|
+
function applyValidation(baseSchema: string, tags: TagEntry[]): string {
|
|
124
|
+
let schema = baseSchema
|
|
125
|
+
|
|
126
|
+
for (const t of tags) {
|
|
127
|
+
if (t.namespace !== 'validate') continue
|
|
128
|
+
|
|
129
|
+
if (t.tag === 'notEmpty') {
|
|
130
|
+
schema += '.min(1)'
|
|
131
|
+
} else if (t.tag === 'length') {
|
|
132
|
+
const args = t.args as Record<string, unknown>
|
|
133
|
+
if (args.min !== undefined) schema += `.min(${args.min})`
|
|
134
|
+
if (args.max !== undefined) schema += `.max(${args.max})`
|
|
135
|
+
} else if (t.tag === 'range') {
|
|
136
|
+
const args = t.args as Record<string, unknown>
|
|
137
|
+
if (args.min !== undefined) schema += `.min(${args.min})`
|
|
138
|
+
if (args.max !== undefined) schema += `.max(${args.max})`
|
|
139
|
+
} else if (t.tag === 'pattern') {
|
|
140
|
+
const pattern = Array.isArray(t.args) ? t.args[0] : undefined
|
|
141
|
+
if (pattern) schema += `.regex(/${pattern}/)`
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return schema
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export default defineTemplate({
|
|
149
|
+
name: 'Zod Schemas',
|
|
150
|
+
description: 'Generate Zod validation schemas from SQL schema',
|
|
151
|
+
language: 'typescript',
|
|
152
|
+
configSchema,
|
|
153
|
+
|
|
154
|
+
generate(ctx) {
|
|
155
|
+
const config = ctx.config ?? {}
|
|
156
|
+
const schema = enrichRealm(ctx)
|
|
157
|
+
|
|
158
|
+
const lines: string[] = ['// Generated by @sqldoc/templates/zod -- DO NOT EDIT', '', "import { z } from 'zod'", '']
|
|
159
|
+
|
|
160
|
+
// Enums
|
|
161
|
+
for (const e of schema.enums) {
|
|
162
|
+
const values = e.values.map((v) => `'${v}'`).join(', ')
|
|
163
|
+
lines.push(`export const ${toCamelCase(e.name)}Schema = z.enum([${values}])`)
|
|
164
|
+
lines.push(`export type ${e.pascalName} = z.infer<typeof ${toCamelCase(e.name)}Schema>`)
|
|
165
|
+
lines.push('')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Composite types (collected from columns)
|
|
169
|
+
const composites = new Map<string, Array<{ name: string; type: string }>>()
|
|
170
|
+
for (const table of schema.tables) {
|
|
171
|
+
for (const col of table.columns) {
|
|
172
|
+
if (col.category === 'composite' && col.compositeFields?.length && !composites.has(col.pgType)) {
|
|
173
|
+
composites.set(col.pgType, col.compositeFields)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
for (const [name, fields] of composites) {
|
|
178
|
+
const camelName = toCamelCase(name)
|
|
179
|
+
const typeName = toPascalCase(name)
|
|
180
|
+
lines.push(`export const ${camelName}Schema = z.object({`)
|
|
181
|
+
for (const f of fields) {
|
|
182
|
+
lines.push(` ${toCamelCase(f.name)}: ${pgToZod(f.type)},`)
|
|
183
|
+
}
|
|
184
|
+
lines.push('})')
|
|
185
|
+
lines.push(`export type ${typeName} = z.infer<typeof ${camelName}Schema>`)
|
|
186
|
+
lines.push('')
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for (const table of activeTables(schema)) {
|
|
190
|
+
const camelName = toCamelCase(table.pascalName)
|
|
191
|
+
|
|
192
|
+
lines.push(`export const ${camelName}Schema = z.object({`)
|
|
193
|
+
|
|
194
|
+
for (const col of table.columns) {
|
|
195
|
+
const propName = toCamelCase(col.name)
|
|
196
|
+
const zodSchema = resolveZodType(col, config)
|
|
197
|
+
lines.push(` ${propName}: ${zodSchema},`)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
lines.push('})')
|
|
201
|
+
lines.push('')
|
|
202
|
+
lines.push(`export type ${table.pascalName} = z.infer<typeof ${camelName}Schema>`)
|
|
203
|
+
lines.push('')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Views (read-only)
|
|
207
|
+
for (const view of schema.views.filter((v) => !v.skipped)) {
|
|
208
|
+
const camelName = toCamelCase(view.pascalName)
|
|
209
|
+
|
|
210
|
+
lines.push(`/** Read-only schema (from view) */`)
|
|
211
|
+
lines.push(`export const ${camelName}Schema = z.object({`)
|
|
212
|
+
|
|
213
|
+
for (const col of view.columns) {
|
|
214
|
+
const propName = toCamelCase(col.name)
|
|
215
|
+
const zodSchema = resolveZodType(col, config)
|
|
216
|
+
lines.push(` ${propName}: ${zodSchema},`)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
lines.push('})')
|
|
220
|
+
lines.push('')
|
|
221
|
+
lines.push(`export type ${view.pascalName} = z.infer<typeof ${camelName}Schema>`)
|
|
222
|
+
lines.push('')
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Functions (skip trigger functions)
|
|
226
|
+
for (const fn of schema.functions) {
|
|
227
|
+
const retRaw = fn.returnType?.type?.toLowerCase() ?? ''
|
|
228
|
+
if (retRaw === 'trigger') continue
|
|
229
|
+
|
|
230
|
+
const camelName = toCamelCase(fn.pascalName)
|
|
231
|
+
|
|
232
|
+
// Build input schema
|
|
233
|
+
const inputArgs = fn.args.filter((a) => !a.name?.startsWith('_') && (a as any).mode !== 'OUT')
|
|
234
|
+
lines.push(`export const ${camelName}InputSchema = z.object({`)
|
|
235
|
+
for (const a of inputArgs) {
|
|
236
|
+
lines.push(` ${toCamelCase(a.name || 'arg')}: ${pgToZod(a.type)},`)
|
|
237
|
+
}
|
|
238
|
+
lines.push('})')
|
|
239
|
+
lines.push('')
|
|
240
|
+
|
|
241
|
+
// Build output schema
|
|
242
|
+
let outputSchema: string
|
|
243
|
+
if (retRaw.startsWith('setof ')) {
|
|
244
|
+
const tableName = retRaw.replace('setof ', '')
|
|
245
|
+
const table = schema.tables.find((t) => t.name === tableName)
|
|
246
|
+
if (table) {
|
|
247
|
+
outputSchema = `z.array(${toCamelCase(table.pascalName)}Schema)`
|
|
248
|
+
} else {
|
|
249
|
+
outputSchema = `z.array(${pgToZod(tableName)})`
|
|
250
|
+
}
|
|
251
|
+
} else if (fn.returnType) {
|
|
252
|
+
outputSchema = pgToZod(fn.returnType.type)
|
|
253
|
+
} else {
|
|
254
|
+
outputSchema = 'z.void()'
|
|
255
|
+
}
|
|
256
|
+
lines.push(`export const ${camelName}OutputSchema = ${outputSchema}`)
|
|
257
|
+
lines.push('')
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
files: [
|
|
262
|
+
{
|
|
263
|
+
path: 'schemas.ts',
|
|
264
|
+
content: lines.join('\n'),
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function resolveZodType(col: any, config: any): string {
|
|
270
|
+
let zodSchema: string
|
|
271
|
+
if (col.typeOverride) {
|
|
272
|
+
zodSchema = col.typeOverride
|
|
273
|
+
} else if (col.category === 'enum' && col.enumValues?.length) {
|
|
274
|
+
zodSchema = `${toCamelCase(col.pgType)}Schema`
|
|
275
|
+
} else if (col.category === 'composite' && col.compositeFields?.length) {
|
|
276
|
+
zodSchema = `${toCamelCase(col.pgType)}Schema`
|
|
277
|
+
} else {
|
|
278
|
+
zodSchema = pgToZod(col.pgType)
|
|
279
|
+
zodSchema = applyValidation(zodSchema, col.tags)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (col.nullable) {
|
|
283
|
+
zodSchema += config.nullableStyle === 'null-union' ? '.nullable()' : '.optional()'
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return zodSchema
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
})
|