@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,332 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { pgToCsharp } from '../types/pg-to-csharp'
3
+ import { pgToGo } from '../types/pg-to-go'
4
+ import { pgToJava } from '../types/pg-to-java'
5
+ import { pgToKotlin } from '../types/pg-to-kotlin'
6
+ import { pgToPython } from '../types/pg-to-python'
7
+ import { pgToRust } from '../types/pg-to-rust'
8
+ import { pgToTs } from '../types/pg-to-ts'
9
+
10
+ describe('pgToTs', () => {
11
+ it('maps text to string', () => {
12
+ expect(pgToTs('text', false)).toBe('string')
13
+ })
14
+
15
+ it('maps bigint to number by default', () => {
16
+ expect(pgToTs('bigint', false)).toBe('number')
17
+ })
18
+
19
+ it('handles null-union nullable style', () => {
20
+ expect(pgToTs('bigint', true, { nullableStyle: 'null-union' })).toBe('number | null')
21
+ })
22
+
23
+ it('maps text[] to string[]', () => {
24
+ expect(pgToTs('text[]', false)).toBe('string[]')
25
+ })
26
+
27
+ it('maps _text array notation to string[]', () => {
28
+ expect(pgToTs('_text', false)).toBe('string[]')
29
+ })
30
+
31
+ it('maps timestamptz to Date by default', () => {
32
+ expect(pgToTs('timestamptz', false)).toBe('Date')
33
+ })
34
+
35
+ it('maps timestamptz with dateType string option', () => {
36
+ expect(pgToTs('timestamptz', false, { dateType: 'string' })).toBe('string')
37
+ })
38
+
39
+ it('strips length specifier from varchar(255)', () => {
40
+ expect(pgToTs('varchar(255)', false)).toBe('string')
41
+ })
42
+
43
+ it('maps jsonb to unknown', () => {
44
+ expect(pgToTs('jsonb', false)).toBe('unknown')
45
+ })
46
+
47
+ it('maps uuid to string', () => {
48
+ expect(pgToTs('uuid', false)).toBe('string')
49
+ })
50
+
51
+ it('maps boolean to boolean', () => {
52
+ expect(pgToTs('boolean', false)).toBe('boolean')
53
+ })
54
+
55
+ it('maps bytea to Buffer', () => {
56
+ expect(pgToTs('bytea', false)).toBe('Buffer')
57
+ })
58
+
59
+ it('maps numeric to string for precision', () => {
60
+ expect(pgToTs('numeric(10,2)', false)).toBe('string')
61
+ })
62
+
63
+ it('applies bigintType option', () => {
64
+ expect(pgToTs('bigint', false, { bigintType: 'bigint' })).toBe('bigint')
65
+ })
66
+
67
+ it('returns unknown for unrecognized types', () => {
68
+ expect(pgToTs('custom_enum', false)).toBe('unknown')
69
+ })
70
+
71
+ it('handles double precision', () => {
72
+ expect(pgToTs('double precision', false)).toBe('number')
73
+ })
74
+
75
+ // Category-based mapping tests
76
+ it('maps string category to string', () => {
77
+ expect(pgToTs('citext', false, {}, 'string')).toBe('string')
78
+ })
79
+
80
+ it('maps integer category to number', () => {
81
+ expect(pgToTs('some_int', false, {}, 'integer')).toBe('number')
82
+ })
83
+
84
+ it('maps boolean category to boolean', () => {
85
+ expect(pgToTs('custom_bool', false, {}, 'boolean')).toBe('boolean')
86
+ })
87
+
88
+ it('maps time category to Date', () => {
89
+ expect(pgToTs('custom_ts', false, {}, 'time')).toBe('Date')
90
+ })
91
+
92
+ it('maps json category to unknown', () => {
93
+ expect(pgToTs('custom_json', false, {}, 'json')).toBe('unknown')
94
+ })
95
+
96
+ it('maps uuid category to string', () => {
97
+ expect(pgToTs('custom_uuid', false, {}, 'uuid')).toBe('string')
98
+ })
99
+
100
+ it('falls back to raw type when no category', () => {
101
+ expect(pgToTs('text', false, {}, undefined)).toBe('string')
102
+ })
103
+
104
+ it('category with nullable null-union', () => {
105
+ expect(pgToTs('custom_int', true, { nullableStyle: 'null-union' }, 'integer')).toBe('number | null')
106
+ })
107
+ })
108
+
109
+ describe('pgToGo', () => {
110
+ it('maps text to string', () => {
111
+ expect(pgToGo('text', false)).toEqual({ type: 'string', imports: [] })
112
+ })
113
+
114
+ it('maps timestamptz nullable to *time.Time', () => {
115
+ expect(pgToGo('timestamptz', true)).toEqual({ type: '*time.Time', imports: ['time'] })
116
+ })
117
+
118
+ it('maps jsonb to json.RawMessage', () => {
119
+ expect(pgToGo('jsonb', false)).toEqual({ type: 'json.RawMessage', imports: ['encoding/json'] })
120
+ })
121
+
122
+ it('maps uuid to uuid.UUID', () => {
123
+ expect(pgToGo('uuid', false)).toEqual({ type: 'uuid.UUID', imports: ['github.com/google/uuid'] })
124
+ })
125
+
126
+ it('maps text[] to []string', () => {
127
+ expect(pgToGo('text[]', false)).toEqual({ type: '[]string', imports: [] })
128
+ })
129
+
130
+ it('maps nullable text to pointer', () => {
131
+ expect(pgToGo('text', true)).toEqual({ type: '*string', imports: [] })
132
+ })
133
+
134
+ it('returns interface{} for unknown types', () => {
135
+ expect(pgToGo('custom_enum', false)).toEqual({ type: 'interface{}', imports: [] })
136
+ })
137
+
138
+ it('maps bigint to int64', () => {
139
+ expect(pgToGo('bigint', false)).toEqual({ type: 'int64', imports: [] })
140
+ })
141
+
142
+ // Category-based mapping tests
143
+ it('maps string category to string', () => {
144
+ expect(pgToGo('custom_text', false, 'string')).toEqual({ type: 'string', imports: [] })
145
+ })
146
+
147
+ it('maps integer category to int64', () => {
148
+ expect(pgToGo('custom_int', false, 'integer')).toEqual({ type: 'int64', imports: [] })
149
+ })
150
+
151
+ it('maps time category to time.Time with import', () => {
152
+ expect(pgToGo('custom_ts', false, 'time')).toEqual({ type: 'time.Time', imports: ['time'] })
153
+ })
154
+
155
+ it('maps uuid category to uuid.UUID with import', () => {
156
+ expect(pgToGo('custom_uuid', false, 'uuid')).toEqual({ type: 'uuid.UUID', imports: ['github.com/google/uuid'] })
157
+ })
158
+
159
+ it('category with nullable uses pointer', () => {
160
+ expect(pgToGo('custom_int', true, 'integer')).toEqual({ type: '*int64', imports: [] })
161
+ })
162
+ })
163
+
164
+ describe('pgToPython', () => {
165
+ it('maps bigint to int', () => {
166
+ expect(pgToPython('bigint', false)).toBe('int')
167
+ })
168
+
169
+ it('maps timestamptz nullable to Optional[datetime]', () => {
170
+ expect(pgToPython('timestamptz', true)).toBe('Optional[datetime]')
171
+ })
172
+
173
+ it('maps text to str', () => {
174
+ expect(pgToPython('text', false)).toBe('str')
175
+ })
176
+
177
+ it('maps boolean to bool', () => {
178
+ expect(pgToPython('boolean', false)).toBe('bool')
179
+ })
180
+
181
+ it('maps jsonb to dict', () => {
182
+ expect(pgToPython('jsonb', false)).toBe('dict')
183
+ })
184
+
185
+ // Category-based mapping tests
186
+ it('maps string category to str', () => {
187
+ expect(pgToPython('custom_text', false, 'string')).toBe('str')
188
+ })
189
+
190
+ it('maps integer category to int', () => {
191
+ expect(pgToPython('custom_int', false, 'integer')).toBe('int')
192
+ })
193
+
194
+ it('maps decimal category to Decimal', () => {
195
+ expect(pgToPython('custom_num', false, 'decimal')).toBe('Decimal')
196
+ })
197
+
198
+ it('category with nullable uses Optional', () => {
199
+ expect(pgToPython('custom_int', true, 'integer')).toBe('Optional[int]')
200
+ })
201
+ })
202
+
203
+ describe('pgToJava', () => {
204
+ it('maps bigint to long', () => {
205
+ expect(pgToJava('bigint', false)).toEqual({ type: 'long', imports: [] })
206
+ })
207
+
208
+ it('maps uuid to UUID with import', () => {
209
+ expect(pgToJava('uuid', false)).toEqual({ type: 'UUID', imports: ['java.util.UUID'] })
210
+ })
211
+
212
+ it('maps text to String', () => {
213
+ expect(pgToJava('text', false)).toEqual({ type: 'String', imports: [] })
214
+ })
215
+
216
+ it('maps nullable bigint to Long wrapper', () => {
217
+ expect(pgToJava('bigint', true)).toEqual({ type: 'Long', imports: [] })
218
+ })
219
+
220
+ // Category-based mapping tests
221
+ it('maps string category to String', () => {
222
+ expect(pgToJava('custom_text', false, 'string')).toEqual({ type: 'String', imports: [] })
223
+ })
224
+
225
+ it('maps integer category to long', () => {
226
+ expect(pgToJava('custom_int', false, 'integer')).toEqual({ type: 'long', imports: [] })
227
+ })
228
+
229
+ it('maps decimal category to BigDecimal with import', () => {
230
+ expect(pgToJava('custom_num', false, 'decimal')).toEqual({ type: 'BigDecimal', imports: ['java.math.BigDecimal'] })
231
+ })
232
+
233
+ it('category integer nullable uses Long wrapper', () => {
234
+ expect(pgToJava('custom_int', true, 'integer')).toEqual({ type: 'Long', imports: [] })
235
+ })
236
+ })
237
+
238
+ describe('pgToKotlin', () => {
239
+ it('maps text nullable to String?', () => {
240
+ expect(pgToKotlin('text', true)).toBe('String?')
241
+ })
242
+
243
+ it('maps text non-nullable to String', () => {
244
+ expect(pgToKotlin('text', false)).toBe('String')
245
+ })
246
+
247
+ it('maps bigint to Long', () => {
248
+ expect(pgToKotlin('bigint', false)).toBe('Long')
249
+ })
250
+
251
+ // Category-based mapping tests
252
+ it('maps string category to String', () => {
253
+ expect(pgToKotlin('custom_text', false, 'string')).toBe('String')
254
+ })
255
+
256
+ it('maps integer category to Long', () => {
257
+ expect(pgToKotlin('custom_int', false, 'integer')).toBe('Long')
258
+ })
259
+
260
+ it('category with nullable uses ? suffix', () => {
261
+ expect(pgToKotlin('custom_int', true, 'integer')).toBe('Long?')
262
+ })
263
+ })
264
+
265
+ describe('pgToRust', () => {
266
+ it('maps bigint to i64', () => {
267
+ expect(pgToRust('bigint', false)).toEqual({ type: 'i64', imports: [] })
268
+ })
269
+
270
+ it('maps uuid to Uuid with import', () => {
271
+ expect(pgToRust('uuid', false)).toEqual({ type: 'Uuid', imports: ['uuid::Uuid'] })
272
+ })
273
+
274
+ it('maps nullable text to Option<String>', () => {
275
+ expect(pgToRust('text', true)).toEqual({ type: 'Option<String>', imports: [] })
276
+ })
277
+
278
+ // Category-based mapping tests
279
+ it('maps string category to String', () => {
280
+ expect(pgToRust('custom_text', false, 'string')).toEqual({ type: 'String', imports: [] })
281
+ })
282
+
283
+ it('maps integer category to i64', () => {
284
+ expect(pgToRust('custom_int', false, 'integer')).toEqual({ type: 'i64', imports: [] })
285
+ })
286
+
287
+ it('maps json category to serde_json::Value with import', () => {
288
+ expect(pgToRust('custom_json', false, 'json')).toEqual({
289
+ type: 'serde_json::Value',
290
+ imports: ['serde_json::Value'],
291
+ })
292
+ })
293
+
294
+ it('category with nullable uses Option', () => {
295
+ expect(pgToRust('custom_int', true, 'integer')).toEqual({ type: 'Option<i64>', imports: [] })
296
+ })
297
+ })
298
+
299
+ describe('pgToCsharp', () => {
300
+ it('maps timestamptz to DateTimeOffset', () => {
301
+ expect(pgToCsharp('timestamptz', false)).toBe('DateTimeOffset')
302
+ })
303
+
304
+ it('maps integer nullable to int?', () => {
305
+ expect(pgToCsharp('integer', true)).toBe('int?')
306
+ })
307
+
308
+ it('maps text to string', () => {
309
+ expect(pgToCsharp('text', false)).toBe('string')
310
+ })
311
+
312
+ it('maps boolean to bool', () => {
313
+ expect(pgToCsharp('boolean', false)).toBe('bool')
314
+ })
315
+
316
+ // Category-based mapping tests
317
+ it('maps string category to string', () => {
318
+ expect(pgToCsharp('custom_text', false, 'string')).toBe('string')
319
+ })
320
+
321
+ it('maps integer category to long', () => {
322
+ expect(pgToCsharp('custom_int', false, 'integer')).toBe('long')
323
+ })
324
+
325
+ it('maps uuid category to Guid', () => {
326
+ expect(pgToCsharp('custom_uuid', false, 'uuid')).toBe('Guid')
327
+ })
328
+
329
+ it('category with nullable uses ? suffix', () => {
330
+ expect(pgToCsharp('custom_int', true, 'integer')).toBe('long?')
331
+ })
332
+ })
@@ -0,0 +1,202 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import typescript from '../typescript/index.ts'
3
+
4
+ const generate = typescript.generate
5
+
6
+ import type { AtlasRealm } from '@sqldoc/atlas'
7
+ import type { TemplateContext } from '@sqldoc/ns-codegen'
8
+
9
+ const testRealm: AtlasRealm = {
10
+ schemas: [
11
+ {
12
+ name: 'public',
13
+ tables: [
14
+ {
15
+ name: 'users',
16
+ columns: [
17
+ { name: 'id', type: { T: 'bigserial', null: false, category: 'integer' } },
18
+ { name: 'email', type: { T: 'character varying', raw: 'varchar(255)', null: false, category: 'string' } },
19
+ { name: 'name', type: { T: 'text', null: true, category: 'string' } },
20
+ { name: 'age', type: { T: 'integer', null: true, category: 'integer' } },
21
+ { name: 'is_active', type: { T: 'boolean', null: false, category: 'boolean' } },
22
+ { name: 'metadata', type: { T: 'jsonb', null: true, category: 'json' } },
23
+ { name: 'created_at', type: { T: 'timestamp with time zone', null: false, category: 'time' } },
24
+ { name: 'tags', type: { T: 'text[]', null: true, category: 'array' } },
25
+ { name: 'avatar', type: { T: 'bytea', null: true, category: 'binary' } },
26
+ { name: 'balance', type: { T: 'numeric(10,2)', null: true, category: 'decimal' } },
27
+ { name: 'external_id', type: { T: 'uuid', null: true, category: 'uuid' } },
28
+ ],
29
+ primary_key: { parts: [{ column: 'id' }] },
30
+ },
31
+ {
32
+ name: 'posts',
33
+ columns: [
34
+ { name: 'id', type: { T: 'bigserial', null: false, category: 'integer' } },
35
+ { name: 'user_id', type: { T: 'bigint', null: false, category: 'integer' } },
36
+ { name: 'title', type: { T: 'text', null: false, category: 'string' } },
37
+ { name: 'body', type: { T: 'text', null: false, category: 'string' } },
38
+ { name: 'published_at', type: { T: 'timestamp with time zone', null: true, category: 'time' } },
39
+ { name: 'view_count', type: { T: 'integer', null: false, category: 'integer' } },
40
+ { name: 'rating', type: { T: 'double precision', null: true, category: 'float' } },
41
+ ],
42
+ primary_key: { parts: [{ column: 'id' }] },
43
+ foreign_keys: [
44
+ { symbol: 'posts_user_id_fkey', columns: ['user_id'], ref_table: 'users', ref_columns: ['id'] },
45
+ ],
46
+ },
47
+ {
48
+ name: 'comments',
49
+ columns: [
50
+ { name: 'id', type: { T: 'bigserial', null: false, category: 'integer' } },
51
+ { name: 'post_id', type: { T: 'bigint', null: false, category: 'integer' } },
52
+ { name: 'user_id', type: { T: 'bigint', null: false, category: 'integer' } },
53
+ { name: 'content', type: { T: 'text', null: false, category: 'string' } },
54
+ { name: 'created_at', type: { T: 'timestamp with time zone', null: false, category: 'time' } },
55
+ ],
56
+ primary_key: { parts: [{ column: 'id' }] },
57
+ foreign_keys: [
58
+ { symbol: 'comments_post_id_fkey', columns: ['post_id'], ref_table: 'posts', ref_columns: ['id'] },
59
+ { symbol: 'comments_user_id_fkey', columns: ['user_id'], ref_table: 'users', ref_columns: ['id'] },
60
+ ],
61
+ },
62
+ ],
63
+ },
64
+ ],
65
+ }
66
+
67
+ function makeCtx(overrides?: Partial<TemplateContext>): TemplateContext {
68
+ return {
69
+ realm: testRealm,
70
+ allFileTags: [],
71
+ docsMeta: [],
72
+ config: {},
73
+ output: './out',
74
+ templateName: 'typescript',
75
+ ...overrides,
76
+ }
77
+ }
78
+
79
+ describe('typescript template', () => {
80
+ it('generates interfaces for all tables', () => {
81
+ const result = generate(makeCtx())
82
+ expect(result.files).toHaveLength(1)
83
+ expect(result.files[0].path).toBe('models.ts')
84
+
85
+ const content = result.files[0].content
86
+ expect(content).toContain('export interface Users {')
87
+ expect(content).toContain('export interface Posts {')
88
+ expect(content).toContain('export interface Comments {')
89
+ })
90
+
91
+ it('maps bigserial to number', () => {
92
+ const result = generate(makeCtx())
93
+ const content = result.files[0].content
94
+ expect(content).toContain('id: number')
95
+ })
96
+
97
+ it('uses optional (?) for nullable columns by default', () => {
98
+ const result = generate(makeCtx())
99
+ const content = result.files[0].content
100
+ expect(content).toContain('name?: string')
101
+ expect(content).toContain('age?: number')
102
+ })
103
+
104
+ it('uses null-union style when configured', () => {
105
+ const result = generate(makeCtx({ config: { nullableStyle: 'null-union' } }))
106
+ const content = result.files[0].content
107
+ expect(content).toContain('name: string | null')
108
+ expect(content).toContain('age: number | null')
109
+ })
110
+
111
+ it('maps text[] to string[] array type', () => {
112
+ const result = generate(makeCtx())
113
+ const content = result.files[0].content
114
+ expect(content).toContain('tags?: string[]')
115
+ })
116
+
117
+ it('maps jsonb to unknown', () => {
118
+ const result = generate(makeCtx())
119
+ const content = result.files[0].content
120
+ expect(content).toContain('metadata?: unknown')
121
+ })
122
+
123
+ it('maps bytea to Buffer', () => {
124
+ const result = generate(makeCtx())
125
+ const content = result.files[0].content
126
+ expect(content).toContain('avatar?: Buffer')
127
+ })
128
+
129
+ it('maps uuid to string', () => {
130
+ const result = generate(makeCtx())
131
+ const content = result.files[0].content
132
+ expect(content).toContain('externalId?: string')
133
+ })
134
+
135
+ it('includes header comment', () => {
136
+ const result = generate(makeCtx())
137
+ const content = result.files[0].content
138
+ expect(content).toContain('// Generated by @sqldoc/templates/typescript -- DO NOT EDIT')
139
+ })
140
+
141
+ it('respects @codegen.skip tag', () => {
142
+ const ctx = makeCtx({
143
+ allFileTags: [
144
+ {
145
+ sourceFile: 'test.sql',
146
+ objects: [
147
+ {
148
+ objectName: 'comments',
149
+ target: 'table',
150
+ tags: [{ namespace: 'codegen', tag: 'skip', args: [] }],
151
+ },
152
+ ],
153
+ },
154
+ ],
155
+ })
156
+ const result = generate(ctx)
157
+ const content = result.files[0].content
158
+ expect(content).not.toContain('export interface Comments')
159
+ expect(content).toContain('export interface Users')
160
+ })
161
+
162
+ it('respects @codegen.rename tag', () => {
163
+ const ctx = makeCtx({
164
+ allFileTags: [
165
+ {
166
+ sourceFile: 'test.sql',
167
+ objects: [
168
+ {
169
+ objectName: 'users',
170
+ target: 'table',
171
+ tags: [{ namespace: 'codegen', tag: 'rename', args: ['Account'] }],
172
+ },
173
+ ],
174
+ },
175
+ ],
176
+ })
177
+ const result = generate(ctx)
178
+ const content = result.files[0].content
179
+ expect(content).toContain('export interface Account {')
180
+ expect(content).not.toContain('export interface Users {')
181
+ })
182
+
183
+ it('respects @codegen.type override on column', () => {
184
+ const ctx = makeCtx({
185
+ allFileTags: [
186
+ {
187
+ sourceFile: 'test.sql',
188
+ objects: [
189
+ {
190
+ objectName: 'users.metadata',
191
+ target: 'column',
192
+ tags: [{ namespace: 'codegen', tag: 'type', args: ['Record<string, any>'] }],
193
+ },
194
+ ],
195
+ },
196
+ ],
197
+ })
198
+ const result = generate(ctx)
199
+ const content = result.files[0].content
200
+ expect(content).toContain('metadata?: Record<string, any>')
201
+ })
202
+ })