@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
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { defineTemplate } from '@sqldoc/ns-codegen'
|
|
2
|
+
import { activeTables, enrichRealm } from '../helpers/enrich.ts'
|
|
3
|
+
import { toCamelCase } from '../helpers/naming.ts'
|
|
4
|
+
import { pgToTs, type TsTypeOptions } from '../types/pg-to-ts.ts'
|
|
5
|
+
|
|
6
|
+
export const configSchema = {
|
|
7
|
+
dateType: {
|
|
8
|
+
type: 'enum',
|
|
9
|
+
values: ['Date', 'temporal', 'dayjs', 'luxon', 'string'],
|
|
10
|
+
description: 'How to represent date/time types',
|
|
11
|
+
},
|
|
12
|
+
nullableStyle: {
|
|
13
|
+
type: 'enum',
|
|
14
|
+
values: ['optional', 'null-union'],
|
|
15
|
+
description: 'Use optional properties (?) or explicit null unions (| null)',
|
|
16
|
+
},
|
|
17
|
+
bigintType: {
|
|
18
|
+
type: 'enum',
|
|
19
|
+
values: ['number', 'bigint', 'string'],
|
|
20
|
+
description: 'How to represent bigint/bigserial columns',
|
|
21
|
+
},
|
|
22
|
+
} as const
|
|
23
|
+
|
|
24
|
+
export default defineTemplate({
|
|
25
|
+
name: 'TypeScript Interfaces',
|
|
26
|
+
description: 'Generate TypeScript interface definitions from SQL schema',
|
|
27
|
+
language: 'typescript',
|
|
28
|
+
configSchema,
|
|
29
|
+
|
|
30
|
+
generate(ctx) {
|
|
31
|
+
const config = ctx.config ?? {}
|
|
32
|
+
const options: TsTypeOptions = {
|
|
33
|
+
dateType: config.dateType,
|
|
34
|
+
nullableStyle: config.nullableStyle,
|
|
35
|
+
bigintType: config.bigintType,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const schema = enrichRealm(ctx)
|
|
39
|
+
const lines: string[] = ['// Generated by @sqldoc/templates/typescript -- DO NOT EDIT', '']
|
|
40
|
+
|
|
41
|
+
// When using Temporal API date types, emit a global declaration so the code typechecks
|
|
42
|
+
// even without a full Temporal polyfill type package installed.
|
|
43
|
+
if (options.dateType === 'temporal') {
|
|
44
|
+
lines.push('declare global {')
|
|
45
|
+
lines.push(' namespace Temporal {')
|
|
46
|
+
lines.push(' interface PlainDate { toString(): string }')
|
|
47
|
+
lines.push(' interface PlainTime { toString(): string }')
|
|
48
|
+
lines.push(' interface PlainDateTime { toString(): string }')
|
|
49
|
+
lines.push(' interface ZonedDateTime { toString(): string }')
|
|
50
|
+
lines.push(' interface Duration { toString(): string }')
|
|
51
|
+
lines.push(' }')
|
|
52
|
+
lines.push('}')
|
|
53
|
+
lines.push('')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Enums
|
|
57
|
+
for (const e of schema.enums) {
|
|
58
|
+
lines.push(`export type ${e.pascalName} = ${e.values.map((v) => `'${v}'`).join(' | ')}`)
|
|
59
|
+
lines.push('')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Composite types (collected from columns)
|
|
63
|
+
const composites = new Map<string, Array<{ name: string; type: string }>>()
|
|
64
|
+
for (const table of schema.tables) {
|
|
65
|
+
for (const col of table.columns) {
|
|
66
|
+
if (col.category === 'composite' && col.compositeFields?.length && !composites.has(col.pgType)) {
|
|
67
|
+
composites.set(col.pgType, col.compositeFields)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (const [name, fields] of composites) {
|
|
72
|
+
const typeName =
|
|
73
|
+
name.charAt(0).toUpperCase() + name.slice(1).replace(/_([a-z])/g, (_: string, c: string) => c.toUpperCase())
|
|
74
|
+
lines.push(`export interface ${typeName} {`)
|
|
75
|
+
for (const f of fields) {
|
|
76
|
+
lines.push(` ${toCamelCase(f.name)}: ${pgToTs(f.type, false, options)}`)
|
|
77
|
+
}
|
|
78
|
+
lines.push('}')
|
|
79
|
+
lines.push('')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Tables
|
|
83
|
+
for (const table of activeTables(schema)) {
|
|
84
|
+
lines.push(`export interface ${table.pascalName} {`)
|
|
85
|
+
for (const col of table.columns) {
|
|
86
|
+
const tsType = resolveType(col, config, options)
|
|
87
|
+
const propName = toCamelCase(col.name)
|
|
88
|
+
if (col.nullable && config.nullableStyle !== 'null-union') {
|
|
89
|
+
lines.push(` ${propName}?: ${tsType}`)
|
|
90
|
+
} else {
|
|
91
|
+
lines.push(` ${propName}: ${tsType}`)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
lines.push('}')
|
|
95
|
+
lines.push('')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Views (read-only)
|
|
99
|
+
for (const view of schema.views.filter((v) => !v.skipped)) {
|
|
100
|
+
lines.push(`/** Read-only (from view) */`)
|
|
101
|
+
lines.push(`export interface ${view.pascalName} {`)
|
|
102
|
+
for (const col of view.columns) {
|
|
103
|
+
const tsType = resolveType(col, config, options)
|
|
104
|
+
const propName = toCamelCase(col.name)
|
|
105
|
+
if (col.nullable && config.nullableStyle !== 'null-union') {
|
|
106
|
+
lines.push(` ${propName}?: ${tsType}`)
|
|
107
|
+
} else {
|
|
108
|
+
lines.push(` ${propName}: ${tsType}`)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
lines.push('}')
|
|
112
|
+
lines.push('')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Functions (skip trigger functions — they're not user-callable)
|
|
116
|
+
for (const fn of schema.functions) {
|
|
117
|
+
const retRaw = fn.returnType?.type?.toLowerCase() ?? ''
|
|
118
|
+
if (retRaw === 'trigger') continue
|
|
119
|
+
|
|
120
|
+
const params = fn.args
|
|
121
|
+
.filter((a) => !a.name?.startsWith('_') && (a as any).mode !== 'OUT')
|
|
122
|
+
.map((a) => {
|
|
123
|
+
const argType = pgToTs(a.type, false, options, a.category as any)
|
|
124
|
+
return `${toCamelCase(a.name || 'arg')}: ${argType}`
|
|
125
|
+
})
|
|
126
|
+
.join(', ')
|
|
127
|
+
|
|
128
|
+
let retType: string
|
|
129
|
+
if (retRaw.startsWith('setof ')) {
|
|
130
|
+
// RETURNS SETOF tablename → Table[]
|
|
131
|
+
const tableName = retRaw.replace('setof ', '')
|
|
132
|
+
const table = schema.tables.find((t) => t.name === tableName)
|
|
133
|
+
retType = table ? `${table.pascalName}[]` : `${pgToTs(tableName, false, options)}[]`
|
|
134
|
+
} else if (fn.returnType) {
|
|
135
|
+
retType = pgToTs(fn.returnType.type, false, options, fn.returnType.category as any)
|
|
136
|
+
} else {
|
|
137
|
+
retType = 'void'
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
lines.push(`export type ${fn.pascalName} = (${params}) => ${retType}`)
|
|
141
|
+
lines.push('')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
files: [
|
|
146
|
+
{
|
|
147
|
+
path: 'models.ts',
|
|
148
|
+
content: lines.join('\n'),
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function resolveType(col: any, config: any, options: TsTypeOptions): string {
|
|
154
|
+
if (col.typeOverride) return col.typeOverride
|
|
155
|
+
const toPascal = (s: string) =>
|
|
156
|
+
s.charAt(0).toUpperCase() + s.slice(1).replace(/_([a-z])/g, (_: string, c: string) => c.toUpperCase())
|
|
157
|
+
if (col.category === 'enum' && col.enumValues?.length) {
|
|
158
|
+
const enumType = toPascal(col.pgType)
|
|
159
|
+
return col.nullable && config.nullableStyle === 'null-union' ? `${enumType} | null` : enumType
|
|
160
|
+
}
|
|
161
|
+
if (col.category === 'composite' && col.compositeFields?.length) {
|
|
162
|
+
const compositeType = toPascal(col.pgType)
|
|
163
|
+
return col.nullable && config.nullableStyle === 'null-union' ? `${compositeType} | null` : compositeType
|
|
164
|
+
}
|
|
165
|
+
return pgToTs(col.pgType, col.nullable && config.nullableStyle === 'null-union', options, col.category)
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
FROM node:23-slim
|
|
2
|
+
WORKDIR /app
|
|
3
|
+
COPY . .
|
|
4
|
+
RUN npm init -y && npm pkg set type=module && npm install typescript@5 pg @types/pg @types/node --save-dev
|
|
5
|
+
# Step 1: typecheck the generated models
|
|
6
|
+
RUN npx tsc --noEmit --strict --esModuleInterop --module nodenext --moduleResolution nodenext models.ts
|
|
7
|
+
# Step 2: run integration test against real DB
|
|
8
|
+
CMD ["node", "--experimental-strip-types", "test.ts"]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test for @sqldoc/templates/typescript
|
|
3
|
+
* Connects to real Postgres, verifies generated types work with actual data.
|
|
4
|
+
*/
|
|
5
|
+
import { Client } from 'pg'
|
|
6
|
+
// Import the generated type -- file is copied from codegen output at test time
|
|
7
|
+
import type { Users, Posts } from './models.ts'
|
|
8
|
+
|
|
9
|
+
const DATABASE_URL = process.env.DATABASE_URL
|
|
10
|
+
if (!DATABASE_URL) {
|
|
11
|
+
console.error('DATABASE_URL not set')
|
|
12
|
+
process.exit(1)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const client = new Client(DATABASE_URL)
|
|
16
|
+
|
|
17
|
+
let failed = 0
|
|
18
|
+
function assert(condition: boolean, msg: string) {
|
|
19
|
+
if (!condition) {
|
|
20
|
+
console.error(`FAIL: ${msg}`)
|
|
21
|
+
failed++
|
|
22
|
+
} else {
|
|
23
|
+
console.log(` ok: ${msg}`)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function run() {
|
|
28
|
+
await client.connect()
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
console.log('--- typescript integration test ---')
|
|
32
|
+
|
|
33
|
+
// 1. Query known seeded user
|
|
34
|
+
const { rows: userRows } = await client.query('SELECT * FROM users WHERE id = 1')
|
|
35
|
+
const user = userRows[0]
|
|
36
|
+
assert(user.email === 'test@example.com', 'user email matches')
|
|
37
|
+
assert(user.name === 'Test User', 'user name matches')
|
|
38
|
+
assert(user.age === 30, 'user age matches')
|
|
39
|
+
assert(user.is_active === true, 'user is_active matches')
|
|
40
|
+
|
|
41
|
+
// Verify the type is assignable (compile-time check; runtime confirms shape)
|
|
42
|
+
const _typedUser: Partial<Users> = {
|
|
43
|
+
id: Number(user.id),
|
|
44
|
+
email: user.email,
|
|
45
|
+
name: user.name,
|
|
46
|
+
age: user.age,
|
|
47
|
+
isActive: user.is_active,
|
|
48
|
+
}
|
|
49
|
+
assert(typeof _typedUser.email === 'string', 'typed user email is string')
|
|
50
|
+
|
|
51
|
+
// 2. Query known seeded post
|
|
52
|
+
const { rows: postRows } = await client.query('SELECT * FROM posts WHERE id = 1')
|
|
53
|
+
assert(postRows.length === 1, 'seeded post found')
|
|
54
|
+
assert(postRows[0].title === 'Hello World', 'post title matches')
|
|
55
|
+
|
|
56
|
+
// Verify Posts type assignability
|
|
57
|
+
const _typedPost: Partial<Posts> = {
|
|
58
|
+
id: Number(postRows[0].id),
|
|
59
|
+
title: postRows[0].title,
|
|
60
|
+
body: postRows[0].body,
|
|
61
|
+
}
|
|
62
|
+
assert(typeof _typedPost.title === 'string', 'typed post title is string')
|
|
63
|
+
|
|
64
|
+
// 3. Insert a new post
|
|
65
|
+
await client.query(
|
|
66
|
+
"INSERT INTO posts (user_id, title, body, view_count) VALUES (1, 'Post from typescript', 'test body', 0)",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
// 4. Read it back
|
|
70
|
+
const { rows: newPosts } = await client.query("SELECT * FROM posts WHERE title = 'Post from typescript'")
|
|
71
|
+
assert(newPosts.length === 1, 'inserted post found')
|
|
72
|
+
assert(newPosts[0].title === 'Post from typescript', 'inserted post title matches')
|
|
73
|
+
// pg returns bigint columns as strings; use Number() for comparison
|
|
74
|
+
assert(Number(newPosts[0].user_id) === 1, 'inserted post user_id matches')
|
|
75
|
+
|
|
76
|
+
if (failed > 0) {
|
|
77
|
+
console.error(`\n${failed} assertion(s) failed`)
|
|
78
|
+
process.exit(1)
|
|
79
|
+
}
|
|
80
|
+
console.log('\nAll assertions passed!')
|
|
81
|
+
} finally {
|
|
82
|
+
await client.end()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
run().catch((err) => {
|
|
87
|
+
console.error(err)
|
|
88
|
+
process.exit(1)
|
|
89
|
+
})
|
package/src/xsd/index.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { defineTemplate } from '@sqldoc/ns-codegen'
|
|
2
|
+
import { activeTables, type EnrichedColumn, enrichRealm } from '../helpers/enrich.ts'
|
|
3
|
+
import { toPascalCase } from '../helpers/naming.ts'
|
|
4
|
+
|
|
5
|
+
const PG_TO_XSD: Record<string, string> = {
|
|
6
|
+
smallint: 'xs:short',
|
|
7
|
+
int2: 'xs:short',
|
|
8
|
+
integer: 'xs:int',
|
|
9
|
+
int: 'xs:int',
|
|
10
|
+
int4: 'xs:int',
|
|
11
|
+
bigint: 'xs:long',
|
|
12
|
+
int8: 'xs:long',
|
|
13
|
+
serial: 'xs:int',
|
|
14
|
+
serial4: 'xs:int',
|
|
15
|
+
bigserial: 'xs:long',
|
|
16
|
+
serial8: 'xs:long',
|
|
17
|
+
smallserial: 'xs:short',
|
|
18
|
+
serial2: 'xs:short',
|
|
19
|
+
real: 'xs:float',
|
|
20
|
+
float4: 'xs:float',
|
|
21
|
+
'double precision': 'xs:double',
|
|
22
|
+
float8: 'xs:double',
|
|
23
|
+
numeric: 'xs:decimal',
|
|
24
|
+
decimal: 'xs:decimal',
|
|
25
|
+
money: 'xs:decimal',
|
|
26
|
+
text: 'xs:string',
|
|
27
|
+
varchar: 'xs:string',
|
|
28
|
+
'character varying': 'xs:string',
|
|
29
|
+
char: 'xs:string',
|
|
30
|
+
character: 'xs:string',
|
|
31
|
+
name: 'xs:string',
|
|
32
|
+
citext: 'xs:string',
|
|
33
|
+
boolean: 'xs:boolean',
|
|
34
|
+
bool: 'xs:boolean',
|
|
35
|
+
timestamp: 'xs:dateTime',
|
|
36
|
+
'timestamp without time zone': 'xs:dateTime',
|
|
37
|
+
timestamptz: 'xs:dateTime',
|
|
38
|
+
'timestamp with time zone': 'xs:dateTime',
|
|
39
|
+
date: 'xs:date',
|
|
40
|
+
time: 'xs:time',
|
|
41
|
+
'time without time zone': 'xs:time',
|
|
42
|
+
timetz: 'xs:time',
|
|
43
|
+
'time with time zone': 'xs:time',
|
|
44
|
+
interval: 'xs:duration',
|
|
45
|
+
bytea: 'xs:base64Binary',
|
|
46
|
+
json: 'xs:string',
|
|
47
|
+
jsonb: 'xs:string',
|
|
48
|
+
uuid: 'xs:string',
|
|
49
|
+
inet: 'xs:string',
|
|
50
|
+
xml: 'xs:string',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function pgToXsd(pgType: string): string {
|
|
54
|
+
const base = pgType
|
|
55
|
+
.toLowerCase()
|
|
56
|
+
.replace(/\(\d+(?:,\s*\d+)?\)/, '')
|
|
57
|
+
.trim()
|
|
58
|
+
if (base.endsWith('[]') || base.startsWith('_')) return 'xs:string'
|
|
59
|
+
return PG_TO_XSD[base] ?? 'xs:string'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildRestrictions(col: EnrichedColumn): string[] {
|
|
63
|
+
const restrictions: string[] = []
|
|
64
|
+
for (const t of col.tags) {
|
|
65
|
+
if (t.namespace !== 'validate') continue
|
|
66
|
+
if (t.tag === 'notEmpty') restrictions.push(' <xs:minLength value="1"/>')
|
|
67
|
+
else if (t.tag === 'length') {
|
|
68
|
+
const args = t.args as Record<string, unknown>
|
|
69
|
+
if (args.min !== undefined) restrictions.push(` <xs:minLength value="${args.min}"/>`)
|
|
70
|
+
if (args.max !== undefined) restrictions.push(` <xs:maxLength value="${args.max}"/>`)
|
|
71
|
+
} else if (t.tag === 'range') {
|
|
72
|
+
const args = t.args as Record<string, unknown>
|
|
73
|
+
if (args.min !== undefined) restrictions.push(` <xs:minInclusive value="${args.min}"/>`)
|
|
74
|
+
if (args.max !== undefined) restrictions.push(` <xs:maxInclusive value="${args.max}"/>`)
|
|
75
|
+
} else if (t.tag === 'pattern') {
|
|
76
|
+
const pattern = Array.isArray(t.args) ? t.args[0] : undefined
|
|
77
|
+
if (pattern) restrictions.push(` <xs:pattern value="${escapeXml(String(pattern))}"/>`)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return restrictions
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function escapeXml(s: string): string {
|
|
84
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default defineTemplate({
|
|
88
|
+
name: 'XML Schema (XSD)',
|
|
89
|
+
description: 'Generate XML Schema Definition types from SQL schema',
|
|
90
|
+
language: 'xml',
|
|
91
|
+
|
|
92
|
+
generate(ctx) {
|
|
93
|
+
const schema = enrichRealm(ctx)
|
|
94
|
+
const lines: string[] = [
|
|
95
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
96
|
+
'<!-- Generated by @sqldoc/templates/xsd. DO NOT EDIT -->',
|
|
97
|
+
'<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">',
|
|
98
|
+
'',
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
// Enums as simpleType restrictions
|
|
102
|
+
for (const e of schema.enums) {
|
|
103
|
+
lines.push(` <xs:simpleType name="${e.pascalName}">`)
|
|
104
|
+
lines.push(` <xs:restriction base="xs:string">`)
|
|
105
|
+
for (const v of e.values) {
|
|
106
|
+
lines.push(` <xs:enumeration value="${escapeXml(v)}"/>`)
|
|
107
|
+
}
|
|
108
|
+
lines.push(` </xs:restriction>`)
|
|
109
|
+
lines.push(` </xs:simpleType>`)
|
|
110
|
+
lines.push('')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const table of activeTables(schema)) {
|
|
114
|
+
lines.push(` <xs:complexType name="${table.pascalName}">`)
|
|
115
|
+
lines.push(' <xs:sequence>')
|
|
116
|
+
|
|
117
|
+
for (const col of table.columns) {
|
|
118
|
+
if (col.category === 'enum' && col.enumValues?.length) {
|
|
119
|
+
// Reference the enum simpleType
|
|
120
|
+
const enumTypeName = toPascalCase(col.pgType)
|
|
121
|
+
lines.push(
|
|
122
|
+
` <xs:element name="${col.camelName}" type="${enumTypeName}"${col.nullable ? ' minOccurs="0"' : ''}/>`,
|
|
123
|
+
)
|
|
124
|
+
} else {
|
|
125
|
+
const xsdType = pgToXsd(col.pgType)
|
|
126
|
+
const restrictions = buildRestrictions(col)
|
|
127
|
+
|
|
128
|
+
if (restrictions.length > 0) {
|
|
129
|
+
lines.push(` <xs:element name="${col.camelName}"${col.nullable ? ' minOccurs="0"' : ''}>`)
|
|
130
|
+
lines.push(' <xs:simpleType>')
|
|
131
|
+
lines.push(` <xs:restriction base="${xsdType}">`)
|
|
132
|
+
lines.push(...restrictions.map((r) => ` ${r}`))
|
|
133
|
+
lines.push(' </xs:restriction>')
|
|
134
|
+
lines.push(' </xs:simpleType>')
|
|
135
|
+
lines.push(' </xs:element>')
|
|
136
|
+
} else {
|
|
137
|
+
lines.push(
|
|
138
|
+
` <xs:element name="${col.camelName}" type="${xsdType}"${col.nullable ? ' minOccurs="0"' : ''}/>`,
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
lines.push(' </xs:sequence>')
|
|
145
|
+
lines.push(' </xs:complexType>')
|
|
146
|
+
lines.push('')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Views (read-only)
|
|
150
|
+
for (const view of schema.views.filter((v) => !v.skipped)) {
|
|
151
|
+
lines.push(` <!-- Read-only (from view) -->`)
|
|
152
|
+
lines.push(` <xs:complexType name="${view.pascalName}">`)
|
|
153
|
+
lines.push(' <xs:sequence>')
|
|
154
|
+
|
|
155
|
+
for (const col of view.columns) {
|
|
156
|
+
if (col.category === 'enum' && col.enumValues?.length) {
|
|
157
|
+
const enumTypeName = toPascalCase(col.pgType)
|
|
158
|
+
lines.push(
|
|
159
|
+
` <xs:element name="${col.camelName}" type="${enumTypeName}"${col.nullable ? ' minOccurs="0"' : ''}/>`,
|
|
160
|
+
)
|
|
161
|
+
} else {
|
|
162
|
+
const xsdType = pgToXsd(col.pgType)
|
|
163
|
+
const restrictions = buildRestrictions(col)
|
|
164
|
+
|
|
165
|
+
if (restrictions.length > 0) {
|
|
166
|
+
lines.push(` <xs:element name="${col.camelName}"${col.nullable ? ' minOccurs="0"' : ''}>`)
|
|
167
|
+
lines.push(' <xs:simpleType>')
|
|
168
|
+
lines.push(` <xs:restriction base="${xsdType}">`)
|
|
169
|
+
lines.push(...restrictions.map((r) => ` ${r}`))
|
|
170
|
+
lines.push(' </xs:restriction>')
|
|
171
|
+
lines.push(' </xs:simpleType>')
|
|
172
|
+
lines.push(' </xs:element>')
|
|
173
|
+
} else {
|
|
174
|
+
lines.push(
|
|
175
|
+
` <xs:element name="${col.camelName}" type="${xsdType}"${col.nullable ? ' minOccurs="0"' : ''}/>`,
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
lines.push(' </xs:sequence>')
|
|
182
|
+
lines.push(' </xs:complexType>')
|
|
183
|
+
lines.push('')
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
lines.push('</xs:schema>')
|
|
187
|
+
lines.push('')
|
|
188
|
+
|
|
189
|
+
return { files: [{ path: 'schema.xsd', content: lines.join('\n') }] }
|
|
190
|
+
},
|
|
191
|
+
})
|