@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.
Files changed (108) hide show
  1. package/package.json +161 -0
  2. package/src/__tests__/dedent.test.ts +45 -0
  3. package/src/__tests__/docker-templates.test.ts +134 -0
  4. package/src/__tests__/go-structs.test.ts +184 -0
  5. package/src/__tests__/naming.test.ts +48 -0
  6. package/src/__tests__/python-dataclasses.test.ts +185 -0
  7. package/src/__tests__/rust-structs.test.ts +176 -0
  8. package/src/__tests__/tags-helpers.test.ts +72 -0
  9. package/src/__tests__/type-mapping.test.ts +332 -0
  10. package/src/__tests__/typescript.test.ts +202 -0
  11. package/src/cobol-copybook/index.ts +220 -0
  12. package/src/cobol-copybook/test/.gitignore +6 -0
  13. package/src/cobol-copybook/test/Dockerfile +7 -0
  14. package/src/csharp-records/index.ts +131 -0
  15. package/src/csharp-records/test/.gitignore +6 -0
  16. package/src/csharp-records/test/Dockerfile +6 -0
  17. package/src/diesel/index.ts +247 -0
  18. package/src/diesel/test/.gitignore +6 -0
  19. package/src/diesel/test/Dockerfile +16 -0
  20. package/src/drizzle/index.ts +255 -0
  21. package/src/drizzle/test/.gitignore +6 -0
  22. package/src/drizzle/test/Dockerfile +8 -0
  23. package/src/drizzle/test/test.ts +71 -0
  24. package/src/efcore/index.ts +190 -0
  25. package/src/efcore/test/.gitignore +6 -0
  26. package/src/efcore/test/Dockerfile +7 -0
  27. package/src/go-structs/index.ts +119 -0
  28. package/src/go-structs/test/.gitignore +6 -0
  29. package/src/go-structs/test/Dockerfile +13 -0
  30. package/src/go-structs/test/test.go +71 -0
  31. package/src/gorm/index.ts +134 -0
  32. package/src/gorm/test/.gitignore +6 -0
  33. package/src/gorm/test/Dockerfile +13 -0
  34. package/src/gorm/test/test.go +65 -0
  35. package/src/helpers/atlas.ts +43 -0
  36. package/src/helpers/enrich.ts +396 -0
  37. package/src/helpers/naming.ts +19 -0
  38. package/src/helpers/tags.ts +63 -0
  39. package/src/index.ts +24 -0
  40. package/src/java-records/index.ts +179 -0
  41. package/src/java-records/test/.gitignore +6 -0
  42. package/src/java-records/test/Dockerfile +11 -0
  43. package/src/java-records/test/Test.java +93 -0
  44. package/src/jpa/index.ts +279 -0
  45. package/src/jpa/test/.gitignore +6 -0
  46. package/src/jpa/test/Dockerfile +14 -0
  47. package/src/jpa/test/Test.java +111 -0
  48. package/src/json-schema/index.ts +351 -0
  49. package/src/json-schema/test/.gitignore +6 -0
  50. package/src/json-schema/test/Dockerfile +18 -0
  51. package/src/knex/index.ts +168 -0
  52. package/src/knex/test/.gitignore +6 -0
  53. package/src/knex/test/Dockerfile +7 -0
  54. package/src/knex/test/test.ts +75 -0
  55. package/src/kotlin-data/index.ts +147 -0
  56. package/src/kotlin-data/test/.gitignore +6 -0
  57. package/src/kotlin-data/test/Dockerfile +14 -0
  58. package/src/kotlin-data/test/Test.kt +82 -0
  59. package/src/kysely/index.ts +165 -0
  60. package/src/kysely/test/.gitignore +6 -0
  61. package/src/kysely/test/Dockerfile +8 -0
  62. package/src/kysely/test/test.ts +82 -0
  63. package/src/prisma/index.ts +387 -0
  64. package/src/prisma/test/.gitignore +6 -0
  65. package/src/prisma/test/Dockerfile +7 -0
  66. package/src/protobuf/index.ts +219 -0
  67. package/src/protobuf/test/.gitignore +6 -0
  68. package/src/protobuf/test/Dockerfile +6 -0
  69. package/src/pydantic/index.ts +272 -0
  70. package/src/pydantic/test/.gitignore +6 -0
  71. package/src/pydantic/test/Dockerfile +8 -0
  72. package/src/pydantic/test/test.py +63 -0
  73. package/src/python-dataclasses/index.ts +217 -0
  74. package/src/python-dataclasses/test/.gitignore +6 -0
  75. package/src/python-dataclasses/test/Dockerfile +8 -0
  76. package/src/python-dataclasses/test/test.py +63 -0
  77. package/src/rust-structs/index.ts +152 -0
  78. package/src/rust-structs/test/.gitignore +6 -0
  79. package/src/rust-structs/test/Dockerfile +22 -0
  80. package/src/rust-structs/test/test.rs +82 -0
  81. package/src/sqlalchemy/index.ts +258 -0
  82. package/src/sqlalchemy/test/.gitignore +6 -0
  83. package/src/sqlalchemy/test/Dockerfile +8 -0
  84. package/src/sqlalchemy/test/test.py +61 -0
  85. package/src/sqlc/index.ts +148 -0
  86. package/src/sqlc/test/.gitignore +6 -0
  87. package/src/sqlc/test/Dockerfile +13 -0
  88. package/src/sqlc/test/test.go +91 -0
  89. package/src/tags/dedent.ts +28 -0
  90. package/src/tags/index.ts +14 -0
  91. package/src/types/index.ts +8 -0
  92. package/src/types/pg-to-csharp.ts +136 -0
  93. package/src/types/pg-to-go.ts +120 -0
  94. package/src/types/pg-to-java.ts +141 -0
  95. package/src/types/pg-to-kotlin.ts +119 -0
  96. package/src/types/pg-to-python.ts +120 -0
  97. package/src/types/pg-to-rust.ts +121 -0
  98. package/src/types/pg-to-ts.ts +173 -0
  99. package/src/typescript/index.ts +168 -0
  100. package/src/typescript/test/.gitignore +6 -0
  101. package/src/typescript/test/Dockerfile +8 -0
  102. package/src/typescript/test/test.ts +89 -0
  103. package/src/xsd/index.ts +191 -0
  104. package/src/xsd/test/.gitignore +6 -0
  105. package/src/xsd/test/Dockerfile +6 -0
  106. package/src/zod/index.ts +289 -0
  107. package/src/zod/test/.gitignore +6 -0
  108. 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,6 @@
1
+ # Generated by codegen — only Dockerfile and test scripts are tracked
2
+ *
3
+ !.gitignore
4
+ !Dockerfile
5
+ !test.*
6
+ !Test.*
@@ -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
+ })
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
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
+ })
@@ -0,0 +1,6 @@
1
+ # Generated by codegen — only Dockerfile and test scripts are tracked
2
+ *
3
+ !.gitignore
4
+ !Dockerfile
5
+ !test.*
6
+ !Test.*
@@ -0,0 +1,6 @@
1
+ FROM alpine:3.20
2
+ RUN apk add --no-cache libxml2-utils
3
+ WORKDIR /app
4
+ COPY . .
5
+ RUN xmllint --noout schema.xsd
6
+ CMD ["echo", "ok"]