@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,396 @@
1
+ /**
2
+ * Enrichment layer — preprocesses Atlas realm + tags into a rich,
3
+ * template-friendly structure. Computed once, used by all templates.
4
+ */
5
+ import type { AtlasColumn, AtlasTable, TypeCategory } from '@sqldoc/atlas'
6
+ import type { TemplateContext } from '@sqldoc/ns-codegen'
7
+ import { findTagsForObject, getColumnType, getTablesFromRealm, getViewsFromRealm, isNullable } from './atlas.ts'
8
+ import { toPascalCase } from './naming.ts'
9
+ import { findRename, findTypeOverride, isSkipped } from './tags.ts'
10
+
11
+ // ── Public types ─────────────────────────────────────────────────
12
+
13
+ export interface EnrichedSchema {
14
+ tables: EnrichedTable[]
15
+ views: EnrichedView[]
16
+ enums: EnrichedEnum[]
17
+ functions: EnrichedFunction[]
18
+ }
19
+
20
+ export interface EnrichedTable {
21
+ /** Original SQL name */
22
+ name: string
23
+ /** PascalCase name (or @codegen.rename override) */
24
+ pascalName: string
25
+ /** Whether this table is skipped for the current template */
26
+ skipped: boolean
27
+ /** Column definitions */
28
+ columns: EnrichedColumn[]
29
+ /** Primary key column names */
30
+ primaryKey: string[]
31
+ /** FKs on this table pointing to other tables */
32
+ belongsTo: Relation[]
33
+ /** FKs on other tables pointing to this table */
34
+ hasMany: Relation[]
35
+ /** All tags on this table */
36
+ tags: TagEntry[]
37
+ /** Raw Atlas table (escape hatch) */
38
+ raw: AtlasTable
39
+ }
40
+
41
+ export interface EnrichedColumn {
42
+ /** Original SQL name */
43
+ name: string
44
+ /** PascalCase name */
45
+ pascalName: string
46
+ /** camelCase name */
47
+ camelName: string
48
+ /** Raw SQL type string (e.g. "character varying", "bigserial") */
49
+ pgType: string
50
+ /** Dialect-independent type category from Atlas */
51
+ category: TypeCategory
52
+ /** Whether this is a user-defined type (enum, composite, domain) */
53
+ isCustomType: boolean
54
+ /** Enum values (when category is 'enum') */
55
+ enumValues?: string[]
56
+ /** Composite type fields (when category is 'composite') */
57
+ compositeFields?: Array<{ name: string; type: string }>
58
+ /** Whether the column is nullable */
59
+ nullable: boolean
60
+ /** Whether this is a primary key column */
61
+ isPrimaryKey: boolean
62
+ /** Whether this is a serial/auto-increment column */
63
+ isSerial: boolean
64
+ /** Default value expression (if any) */
65
+ defaultValue?: string
66
+ /** FK reference (if this column is a foreign key) */
67
+ foreignKey?: { table: string; column: string }
68
+ /** Type override from @codegen.type tag (if any) */
69
+ typeOverride?: string
70
+ /** All tags on this column */
71
+ tags: TagEntry[]
72
+ /** Raw Atlas column (escape hatch) */
73
+ raw: AtlasColumn
74
+ }
75
+
76
+ export interface Relation {
77
+ /** Constraint name */
78
+ constraintName: string
79
+ /** Column on the source table */
80
+ column: string
81
+ /** The other table */
82
+ foreignTable: string
83
+ /** Column on the other table */
84
+ foreignColumn: string
85
+ }
86
+
87
+ export interface TagEntry {
88
+ namespace: string
89
+ tag: string | null
90
+ args: Record<string, unknown> | unknown[]
91
+ }
92
+
93
+ export interface EnrichedView {
94
+ /** Original SQL name */
95
+ name: string
96
+ /** PascalCase name */
97
+ pascalName: string
98
+ /** Whether this view is skipped for the current template */
99
+ skipped: boolean
100
+ /** Column definitions (no PK, FK, or serial) */
101
+ columns: EnrichedColumn[]
102
+ /** All tags on this view */
103
+ tags: TagEntry[]
104
+ }
105
+
106
+ export interface EnrichedEnum {
107
+ /** Original SQL type name */
108
+ name: string
109
+ /** PascalCase name */
110
+ pascalName: string
111
+ /** Enum variant values */
112
+ values: string[]
113
+ }
114
+
115
+ export interface EnrichedFunction {
116
+ /** Original SQL function name */
117
+ name: string
118
+ /** PascalCase name */
119
+ pascalName: string
120
+ /** Function arguments */
121
+ args: Array<{ name: string; type: string; category: string }>
122
+ /** Return type */
123
+ returnType?: { type: string; category: string }
124
+ /** Language (sql, plpgsql, etc.) */
125
+ language?: string
126
+ }
127
+
128
+ // ── Enrichment ───────────────────────────────────────────────────
129
+
130
+ /**
131
+ * Enrich the raw Atlas realm into a template-friendly structure.
132
+ * Computes relationships, PK/FK lookups, tag indexing, naming, etc.
133
+ */
134
+ export function enrichRealm(ctx: TemplateContext<any>): EnrichedSchema {
135
+ const rawTables = getTablesFromRealm(ctx.realm)
136
+
137
+ // Build reverse FK index: targetTable → relations pointing at it
138
+ const reverseIndex = new Map<string, Relation[]>()
139
+ for (const table of rawTables) {
140
+ for (const fk of table.foreign_keys ?? []) {
141
+ if (!fk.ref_table || !fk.columns?.length) continue
142
+ for (let i = 0; i < fk.columns.length; i++) {
143
+ const rel: Relation = {
144
+ constraintName: fk.symbol ?? '',
145
+ column: fk.ref_columns?.[i] ?? 'id',
146
+ foreignTable: table.name,
147
+ foreignColumn: fk.columns[i],
148
+ }
149
+ const existing = reverseIndex.get(fk.ref_table) ?? []
150
+ existing.push(rel)
151
+ reverseIndex.set(fk.ref_table, existing)
152
+ }
153
+ }
154
+ }
155
+
156
+ const tables: EnrichedTable[] = rawTables.map((table) => {
157
+ const tableTags = findTagsForObject(ctx.allFileTags, table.name)
158
+ const skipped = isSkipped(tableTags, ctx.templateName)
159
+ const pascalName = findRename(tableTags, ctx.templateName) ?? toPascalCase(table.name)
160
+ const pkColumns = new Set(
161
+ (table.primary_key?.parts ?? []).map((p) => p.column).filter((c): c is string => c != null),
162
+ )
163
+
164
+ // Build FK map for this table
165
+ const fkMap = new Map<string, { table: string; column: string }>()
166
+ const belongsTo: Relation[] = []
167
+ for (const fk of table.foreign_keys ?? []) {
168
+ if (!fk.columns?.length || !fk.ref_table) continue
169
+ for (let i = 0; i < fk.columns.length; i++) {
170
+ const ref = { table: fk.ref_table, column: fk.ref_columns?.[i] ?? 'id' }
171
+ fkMap.set(fk.columns[i], ref)
172
+ belongsTo.push({
173
+ constraintName: fk.symbol ?? '',
174
+ column: fk.columns[i],
175
+ foreignTable: fk.ref_table,
176
+ foreignColumn: ref.column,
177
+ })
178
+ }
179
+ }
180
+
181
+ const hasMany = reverseIndex.get(table.name) ?? []
182
+
183
+ const columns: EnrichedColumn[] = (table.columns ?? []).map((col) =>
184
+ enrichColumn(col, table.name, ctx.templateName, ctx.allFileTags, pkColumns, fkMap),
185
+ )
186
+
187
+ return {
188
+ name: table.name,
189
+ pascalName,
190
+ skipped,
191
+ columns,
192
+ primaryKey: [...pkColumns],
193
+ belongsTo,
194
+ hasMany,
195
+ tags: tableTags,
196
+ raw: table,
197
+ }
198
+ })
199
+
200
+ // ── Views ──────────────────────────────────────────────────────
201
+ const rawViews = getViewsFromRealm(ctx.realm)
202
+ const views: EnrichedView[] = rawViews.map((view) => {
203
+ const viewTags = findTagsForObject(ctx.allFileTags, view.name)
204
+ const skipped = isSkipped(viewTags, ctx.templateName)
205
+ const pascalName = findRename(viewTags, ctx.templateName) ?? toPascalCase(view.name)
206
+
207
+ let columns: EnrichedColumn[]
208
+ if (view.columns?.length) {
209
+ // Atlas provided column metadata
210
+ columns = view.columns.map((col) => enrichColumn(col, view.name, ctx.templateName, ctx.allFileTags))
211
+ } else if (view.def) {
212
+ // Atlas didn't provide columns — resolve from the SELECT list + source tables
213
+ columns = resolveViewColumns(view.def, tables, view.name, ctx.templateName, ctx.allFileTags)
214
+ } else {
215
+ columns = []
216
+ }
217
+
218
+ return { name: view.name, pascalName, skipped, columns, tags: viewTags }
219
+ })
220
+
221
+ // ── Enums ─────────────────────────────────────────────────────
222
+ // Extract from all columns (tables + views) since Atlas surfaces enum info per-column
223
+ const enums: EnrichedEnum[] = extractEnums(tables, views)
224
+
225
+ // ── Functions ─────────────────────────────────────────────────
226
+ const rawFuncs = ctx.realm.schemas.flatMap((s) => s.funcs ?? [])
227
+ const functions: EnrichedFunction[] = rawFuncs.map((fn) => ({
228
+ name: fn.name,
229
+ pascalName: toPascalCase(fn.name),
230
+ args: (fn.args ?? []).map((a) => ({
231
+ name: a.name ?? '',
232
+ type: a.type?.T ?? a.type?.raw ?? 'unknown',
233
+ category: a.type?.category ?? 'unknown',
234
+ })),
235
+ returnType: fn.ret
236
+ ? {
237
+ type: fn.ret.T ?? fn.ret.raw ?? 'unknown',
238
+ category: fn.ret.category ?? 'unknown',
239
+ }
240
+ : undefined,
241
+ language: fn.lang,
242
+ }))
243
+
244
+ return { tables, views, enums, functions }
245
+ }
246
+
247
+ /**
248
+ * Get only non-skipped tables from an enriched schema.
249
+ */
250
+ export function activeTables(schema: EnrichedSchema): EnrichedTable[] {
251
+ return schema.tables.filter((t) => !t.skipped)
252
+ }
253
+
254
+ /**
255
+ * Find tags with a specific namespace on a table or column.
256
+ */
257
+ export function findTagsByNamespace(tags: TagEntry[], namespace: string): TagEntry[] {
258
+ return tags.filter((t) => t.namespace === namespace)
259
+ }
260
+
261
+ /**
262
+ * Get the first arg value from a tag (for positional args).
263
+ */
264
+ export function getTagArg(tag: TagEntry, index: number = 0): unknown {
265
+ if (Array.isArray(tag.args)) return tag.args[index]
266
+ return undefined
267
+ }
268
+
269
+ /**
270
+ * Get a named arg value from a tag.
271
+ */
272
+ export function getNamedArg(tag: TagEntry, key: string): unknown {
273
+ if (!Array.isArray(tag.args)) return (tag.args as Record<string, unknown>)[key]
274
+ return undefined
275
+ }
276
+
277
+ // ── Internals ────────────────────────────────────────────────────
278
+
279
+ /**
280
+ * Resolve view columns from its SELECT definition by matching column names
281
+ * to source table columns. Handles "SELECT col1, col2 FROM tablename".
282
+ */
283
+ function resolveViewColumns(
284
+ viewDef: string,
285
+ tables: EnrichedTable[],
286
+ _viewName: string,
287
+ _templateName: string,
288
+ _allFileTags: TemplateContext<any>['allFileTags'],
289
+ ): EnrichedColumn[] {
290
+ // Parse "SELECT col1, col2, ... FROM tablename"
291
+ const selectMatch = viewDef.match(/SELECT\s+([\s\S]+?)\s+FROM\s+(\w+)/i)
292
+ if (!selectMatch) return []
293
+
294
+ const colList = selectMatch[1]
295
+ const sourceTableName = selectMatch[2]
296
+ const sourceTable = tables.find((t) => t.name === sourceTableName)
297
+ if (!sourceTable) return []
298
+
299
+ // Handle SELECT *
300
+ if (colList.trim() === '*') {
301
+ return sourceTable.columns.map((col) => ({
302
+ ...col,
303
+ isPrimaryKey: false,
304
+ isSerial: false,
305
+ foreignKey: undefined,
306
+ }))
307
+ }
308
+
309
+ // Parse column names (strip whitespace, handle aliases)
310
+ const colNames = colList.split(',').map((c) => {
311
+ const trimmed = c.trim()
312
+ // Handle "col AS alias" — use the original column name for lookup
313
+ const asMatch = trimmed.match(/^(\w+)\s+AS\s+/i)
314
+ return asMatch ? asMatch[1] : trimmed
315
+ })
316
+
317
+ return colNames
318
+ .map((name) => sourceTable.columns.find((c) => c.name === name))
319
+ .filter((c): c is EnrichedColumn => c != null)
320
+ .map((col) => ({
321
+ ...col,
322
+ // Views are read-only — no PK/FK/serial
323
+ isPrimaryKey: false,
324
+ isSerial: false,
325
+ foreignKey: undefined,
326
+ }))
327
+ }
328
+
329
+ /** Enrich a single column using Atlas type metadata */
330
+ function enrichColumn(
331
+ col: AtlasColumn,
332
+ parentName: string,
333
+ templateName: string,
334
+ allFileTags: TemplateContext<any>['allFileTags'],
335
+ pkColumns?: Set<string>,
336
+ fkMap?: Map<string, { table: string; column: string }>,
337
+ ): EnrichedColumn {
338
+ const colTags = findTagsForObject(allFileTags, `${parentName}.${col.name}`)
339
+ const pgType = getColumnType(col)
340
+ const nullable = isNullable(col)
341
+ const typeOverride = findTypeOverride(colTags, templateName)
342
+ const defaultValue = extractDefault(col.default)
343
+ const category = (col.type?.category ?? 'unknown') as TypeCategory
344
+ const isSerial = category === 'integer' && pgType.toLowerCase().includes('serial')
345
+
346
+ return {
347
+ name: col.name,
348
+ pascalName: toPascalCase(col.name),
349
+ camelName: toCamelCase(col.name),
350
+ pgType,
351
+ category,
352
+ isCustomType: col.type?.is_custom ?? false,
353
+ enumValues: col.type?.enum_values,
354
+ compositeFields: col.type?.composite_fields,
355
+ nullable,
356
+ isPrimaryKey: pkColumns?.has(col.name) ?? false,
357
+ isSerial,
358
+ defaultValue,
359
+ foreignKey: fkMap?.get(col.name),
360
+ typeOverride,
361
+ tags: colTags,
362
+ raw: col,
363
+ }
364
+ }
365
+
366
+ /** Extract enums from all columns (tables + views) with category 'enum' and enum_values */
367
+ function extractEnums(tables: EnrichedTable[], views: EnrichedView[]): EnrichedEnum[] {
368
+ const found = new Map<string, string[]>()
369
+
370
+ const allColumns = [...tables.flatMap((t) => t.columns), ...views.flatMap((v) => v.columns)]
371
+
372
+ for (const col of allColumns) {
373
+ if (col.category === 'enum' && col.enumValues?.length && !found.has(col.pgType)) {
374
+ found.set(col.pgType, col.enumValues)
375
+ }
376
+ }
377
+
378
+ return [...found.entries()].map(([name, values]) => ({
379
+ name,
380
+ pascalName: toPascalCase(name),
381
+ values,
382
+ }))
383
+ }
384
+
385
+ function extractDefault(def: unknown): string | undefined {
386
+ if (!def || typeof def !== 'object') return undefined
387
+ const d = def as Record<string, unknown>
388
+ if ('X' in d) return String(d.X)
389
+ if ('V' in d) return String(d.V)
390
+ return undefined
391
+ }
392
+
393
+ function toCamelCase(name: string): string {
394
+ const pascal = toPascalCase(name)
395
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1)
396
+ }
@@ -0,0 +1,19 @@
1
+ /** Convert snake_case to PascalCase: "user_accounts" -> "UserAccounts" */
2
+ export function toPascalCase(snake: string): string {
3
+ return snake
4
+ .split('_')
5
+ .filter(Boolean)
6
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
7
+ .join('')
8
+ }
9
+
10
+ /** Convert snake_case to camelCase: "user_accounts" -> "userAccounts" */
11
+ export function toCamelCase(snake: string): string {
12
+ const pascal = toPascalCase(snake)
13
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1)
14
+ }
15
+
16
+ /** Convert snake_case to SCREAMING_SNAKE: "user_status" -> "USER_STATUS" */
17
+ export function toScreamingSnake(snake: string): string {
18
+ return snake.toUpperCase()
19
+ }
@@ -0,0 +1,63 @@
1
+ interface TagInfo {
2
+ namespace: string
3
+ tag: string | null
4
+ args: Record<string, unknown> | unknown[]
5
+ }
6
+
7
+ /** Find @codegen.rename for a specific object, optionally scoped to template */
8
+ export function findRename(tags: TagInfo[], templateName: string): string | undefined {
9
+ // First check template-specific rename
10
+ const specific = tags.find(
11
+ (t) =>
12
+ t.namespace === 'codegen' &&
13
+ t.tag === 'rename' &&
14
+ Array.isArray(t.args) &&
15
+ t.args.length === 2 &&
16
+ t.args[1] === templateName,
17
+ )
18
+ if (specific && Array.isArray(specific.args)) return specific.args[0] as string
19
+
20
+ // Then check global rename (single arg)
21
+ const global = tags.find(
22
+ (t) => t.namespace === 'codegen' && t.tag === 'rename' && Array.isArray(t.args) && t.args.length === 1,
23
+ )
24
+ if (global && Array.isArray(global.args)) return global.args[0] as string
25
+
26
+ return undefined
27
+ }
28
+
29
+ /** Check if @codegen.skip applies to this template */
30
+ export function isSkipped(tags: TagInfo[], templateName: string): boolean {
31
+ return tags.some(
32
+ (t) =>
33
+ t.namespace === 'codegen' &&
34
+ t.tag === 'skip' &&
35
+ // Global skip (no args or empty)
36
+ (!Array.isArray(t.args) ||
37
+ t.args.length === 0 ||
38
+ // Template-specific skip
39
+ (Array.isArray(t.args) && t.args[0] === templateName)),
40
+ )
41
+ }
42
+
43
+ /** Find @codegen.type override for a column, optionally scoped to template */
44
+ export function findTypeOverride(tags: TagInfo[], templateName: string): string | undefined {
45
+ // First check template-specific type override
46
+ const specific = tags.find(
47
+ (t) =>
48
+ t.namespace === 'codegen' &&
49
+ t.tag === 'type' &&
50
+ Array.isArray(t.args) &&
51
+ t.args.length === 2 &&
52
+ t.args[1] === templateName,
53
+ )
54
+ if (specific && Array.isArray(specific.args)) return specific.args[0] as string
55
+
56
+ // Then check global type override (single arg)
57
+ const global = tags.find(
58
+ (t) => t.namespace === 'codegen' && t.tag === 'type' && Array.isArray(t.args) && t.args.length === 1,
59
+ )
60
+ if (global && Array.isArray(global.args)) return global.args[0] as string
61
+
62
+ return undefined
63
+ }
package/src/index.ts ADDED
@@ -0,0 +1,24 @@
1
+ // Type mappers
2
+
3
+ // Atlas helpers
4
+ export { findTagsForObject, getColumnType, getTablesFromRealm, getViewsFromRealm, isNullable } from './helpers/atlas.ts'
5
+ export type {
6
+ EnrichedColumn,
7
+ EnrichedEnum,
8
+ EnrichedFunction,
9
+ EnrichedSchema,
10
+ EnrichedTable,
11
+ EnrichedView,
12
+ Relation,
13
+ TagEntry,
14
+ } from './helpers/enrich.ts'
15
+ // Enrichment layer
16
+ export { activeTables, enrichRealm, findTagsByNamespace, getNamedArg, getTagArg } from './helpers/enrich.ts'
17
+ // Naming helpers
18
+ export { toCamelCase, toPascalCase, toScreamingSnake } from './helpers/naming.ts'
19
+ // Tag lookup helpers
20
+ export { findRename, findTypeOverride, isSkipped } from './helpers/tags.ts'
21
+ // Tag functions (re-exported for convenience)
22
+ export { csharp, dedent, go, java, kotlin, python, rust, sql, ts } from './tags.ts'
23
+ export type { TsTypeOptions } from './types.ts'
24
+ export { pgToCsharp, pgToGo, pgToJava, pgToKotlin, pgToPython, pgToRust, pgToTs } from './types.ts'
@@ -0,0 +1,179 @@
1
+ import { defineTemplate } from '@sqldoc/ns-codegen'
2
+ import { activeTables, enrichRealm } from '../helpers/enrich.ts'
3
+ import { toCamelCase, toPascalCase, toScreamingSnake } from '../helpers/naming.ts'
4
+ import { pgToJava } from '../types/pg-to-java.ts'
5
+
6
+ export default defineTemplate({
7
+ name: 'Java Records',
8
+ description: 'Generate Java record classes from SQL schema',
9
+ language: 'java',
10
+
11
+ generate(ctx) {
12
+ const schema = enrichRealm(ctx)
13
+ const files: Array<{ path: string; content: string }> = []
14
+
15
+ // Enums
16
+ for (const e of schema.enums) {
17
+ const className = toPascalCase(e.name)
18
+ const members = e.values.map((v) => {
19
+ const constName = toScreamingSnake(v)
20
+ return ` ${constName}("${v}")`
21
+ })
22
+
23
+ const parts: string[] = []
24
+ parts.push(`public enum ${className} {`)
25
+ parts.push(`${members.join(',\n')};`)
26
+ parts.push('')
27
+ parts.push(` private final String value;`)
28
+ parts.push('')
29
+ parts.push(` ${className}(String value) {`)
30
+ parts.push(` this.value = value;`)
31
+ parts.push(` }`)
32
+ parts.push('')
33
+ parts.push(` public String getValue() {`)
34
+ parts.push(` return value;`)
35
+ parts.push(` }`)
36
+ parts.push('}')
37
+ parts.push('')
38
+
39
+ files.push({
40
+ path: `${className}.java`,
41
+ content: parts.join('\n'),
42
+ })
43
+ }
44
+
45
+ // Composite types (collected from columns)
46
+ const composites = new Map<string, Array<{ name: string; type: string }>>()
47
+ for (const table of schema.tables) {
48
+ for (const col of table.columns) {
49
+ if (col.category === 'composite' && col.compositeFields?.length && !composites.has(col.pgType)) {
50
+ composites.set(col.pgType, col.compositeFields)
51
+ }
52
+ }
53
+ }
54
+ for (const [name, fields] of composites) {
55
+ const className = toPascalCase(name)
56
+ const allImports = new Set<string>()
57
+
58
+ const recordFields: string[] = []
59
+ for (const f of fields) {
60
+ const mapped = pgToJava(f.type, false)
61
+ for (const imp of mapped.imports) allImports.add(imp)
62
+ recordFields.push(` ${mapped.type} ${toCamelCase(f.name)}`)
63
+ }
64
+
65
+ const importLines: string[] = []
66
+ const sortedImports = [...allImports].sort()
67
+ for (const imp of sortedImports) {
68
+ importLines.push(`import ${imp};`)
69
+ }
70
+
71
+ const parts: string[] = []
72
+ if (importLines.length > 0) {
73
+ parts.push(importLines.join('\n'))
74
+ parts.push('')
75
+ }
76
+ parts.push(`public record ${className}(`)
77
+ parts.push(recordFields.join(',\n'))
78
+ parts.push(') {}')
79
+ parts.push('')
80
+
81
+ files.push({
82
+ path: `${className}.java`,
83
+ content: parts.join('\n'),
84
+ })
85
+ }
86
+
87
+ for (const table of activeTables(schema)) {
88
+ const allImports = new Set<string>()
89
+
90
+ const fields: string[] = []
91
+ for (const col of table.columns) {
92
+ let javaType: string
93
+ if (col.typeOverride) {
94
+ javaType = col.typeOverride
95
+ } else if (col.category === 'enum' && col.enumValues?.length) {
96
+ javaType = toPascalCase(col.pgType)
97
+ } else if (col.category === 'composite' && col.compositeFields?.length) {
98
+ javaType = toPascalCase(col.pgType)
99
+ } else {
100
+ const mapped = pgToJava(col.pgType, col.nullable, col.category)
101
+ javaType = mapped.type
102
+ for (const imp of mapped.imports) allImports.add(imp)
103
+ }
104
+
105
+ fields.push(` ${javaType} ${col.camelName}`)
106
+ }
107
+
108
+ const importLines: string[] = []
109
+ const sortedImports = [...allImports].sort()
110
+ for (const imp of sortedImports) {
111
+ importLines.push(`import ${imp};`)
112
+ }
113
+
114
+ const parts: string[] = []
115
+ if (importLines.length > 0) {
116
+ parts.push(importLines.join('\n'))
117
+ parts.push('')
118
+ }
119
+ parts.push(`public record ${table.pascalName}(`)
120
+ parts.push(fields.join(',\n'))
121
+ parts.push(') {}')
122
+ parts.push('')
123
+
124
+ files.push({
125
+ path: `${table.pascalName}.java`,
126
+ content: parts.join('\n'),
127
+ })
128
+ }
129
+
130
+ // Views (read-only)
131
+ for (const view of schema.views.filter((v) => !v.skipped)) {
132
+ const allImports = new Set<string>()
133
+
134
+ const fields: string[] = []
135
+ for (const col of view.columns) {
136
+ let javaType: string
137
+ if (col.typeOverride) {
138
+ javaType = col.typeOverride
139
+ } else if (col.category === 'enum' && col.enumValues?.length) {
140
+ javaType = toPascalCase(col.pgType)
141
+ } else if (col.category === 'composite' && col.compositeFields?.length) {
142
+ javaType = toPascalCase(col.pgType)
143
+ } else {
144
+ const mapped = pgToJava(col.pgType, col.nullable, col.category)
145
+ javaType = mapped.type
146
+ for (const imp of mapped.imports) allImports.add(imp)
147
+ }
148
+
149
+ fields.push(` ${javaType} ${col.camelName}`)
150
+ }
151
+
152
+ const importLines: string[] = []
153
+ const sortedImports = [...allImports].sort()
154
+ for (const imp of sortedImports) {
155
+ importLines.push(`import ${imp};`)
156
+ }
157
+
158
+ const parts: string[] = []
159
+ if (importLines.length > 0) {
160
+ parts.push(importLines.join('\n'))
161
+ parts.push('')
162
+ }
163
+ parts.push(`/** Read-only (from view) */`)
164
+ parts.push(`public record ${view.pascalName}(`)
165
+ parts.push(fields.join(',\n'))
166
+ parts.push(') {}')
167
+ parts.push('')
168
+
169
+ files.push({
170
+ path: `${view.pascalName}.java`,
171
+ content: parts.join('\n'),
172
+ })
173
+ }
174
+
175
+ // Functions (skip trigger functions — Java doesn't have SQL function type patterns)
176
+
177
+ return { files }
178
+ },
179
+ })
@@ -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,11 @@
1
+ FROM eclipse-temurin:21-jdk-alpine
2
+ WORKDIR /app
3
+ RUN apk add --no-cache curl
4
+ # Download PostgreSQL JDBC driver
5
+ RUN mkdir -p /deps && \
6
+ curl -sL -o /deps/postgresql-42.7.4.jar https://repo1.maven.org/maven2/org/postgresql/postgresql/42.7.4/postgresql-42.7.4.jar
7
+ COPY . .
8
+ # Step 1: compile the generated records + test
9
+ RUN javac -cp "/deps/*:." *.java Test.java
10
+ # Step 2: run integration test against real DB
11
+ CMD ["java", "-cp", "/deps/*:.", "Test"]