@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,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,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"]
|