@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,387 @@
|
|
|
1
|
+
import { defineTemplate } from '@sqldoc/ns-codegen'
|
|
2
|
+
import { activeTables, type EnrichedTable, enrichRealm } from '../helpers/enrich.ts'
|
|
3
|
+
import { toPascalCase } from '../helpers/naming.ts'
|
|
4
|
+
|
|
5
|
+
export const configSchema = {
|
|
6
|
+
provider: {
|
|
7
|
+
type: 'string',
|
|
8
|
+
description: 'Database provider (default: postgresql)',
|
|
9
|
+
},
|
|
10
|
+
} as const
|
|
11
|
+
|
|
12
|
+
/** Map a PostgreSQL type to a Prisma type */
|
|
13
|
+
function pgToPrisma(pgType: string): string {
|
|
14
|
+
const normalized = pgType.toLowerCase().trim()
|
|
15
|
+
|
|
16
|
+
// Handle arrays
|
|
17
|
+
if (normalized.endsWith('[]')) {
|
|
18
|
+
return `${pgToPrisma(normalized.slice(0, -2))}[]`
|
|
19
|
+
}
|
|
20
|
+
if (normalized.startsWith('_')) {
|
|
21
|
+
return `${pgToPrisma(normalized.slice(1))}[]`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Strip length specifiers
|
|
25
|
+
const baseType = normalized.replace(/\(\d+(?:,\s*\d+)?\)/, '').trim()
|
|
26
|
+
|
|
27
|
+
const map: Record<string, string> = {
|
|
28
|
+
// Numeric
|
|
29
|
+
smallint: 'Int',
|
|
30
|
+
int2: 'Int',
|
|
31
|
+
integer: 'Int',
|
|
32
|
+
int: 'Int',
|
|
33
|
+
int4: 'Int',
|
|
34
|
+
bigint: 'BigInt',
|
|
35
|
+
int8: 'BigInt',
|
|
36
|
+
serial: 'Int',
|
|
37
|
+
serial4: 'Int',
|
|
38
|
+
bigserial: 'BigInt',
|
|
39
|
+
serial8: 'BigInt',
|
|
40
|
+
smallserial: 'Int',
|
|
41
|
+
serial2: 'Int',
|
|
42
|
+
real: 'Float',
|
|
43
|
+
float4: 'Float',
|
|
44
|
+
'double precision': 'Float',
|
|
45
|
+
float8: 'Float',
|
|
46
|
+
numeric: 'Decimal',
|
|
47
|
+
decimal: 'Decimal',
|
|
48
|
+
money: 'String',
|
|
49
|
+
|
|
50
|
+
// String
|
|
51
|
+
text: 'String',
|
|
52
|
+
varchar: 'String',
|
|
53
|
+
'character varying': 'String',
|
|
54
|
+
char: 'String',
|
|
55
|
+
character: 'String',
|
|
56
|
+
name: 'String',
|
|
57
|
+
citext: 'String',
|
|
58
|
+
|
|
59
|
+
// Boolean
|
|
60
|
+
boolean: 'Boolean',
|
|
61
|
+
bool: 'Boolean',
|
|
62
|
+
|
|
63
|
+
// Date/Time
|
|
64
|
+
timestamp: 'DateTime',
|
|
65
|
+
'timestamp without time zone': 'DateTime',
|
|
66
|
+
timestamptz: 'DateTime',
|
|
67
|
+
'timestamp with time zone': 'DateTime',
|
|
68
|
+
date: 'DateTime',
|
|
69
|
+
time: 'String',
|
|
70
|
+
'time without time zone': 'String',
|
|
71
|
+
timetz: 'String',
|
|
72
|
+
'time with time zone': 'String',
|
|
73
|
+
interval: 'String',
|
|
74
|
+
|
|
75
|
+
// Binary
|
|
76
|
+
bytea: 'Bytes',
|
|
77
|
+
|
|
78
|
+
// JSON
|
|
79
|
+
json: 'Json',
|
|
80
|
+
jsonb: 'Json',
|
|
81
|
+
|
|
82
|
+
// UUID
|
|
83
|
+
uuid: 'String',
|
|
84
|
+
|
|
85
|
+
// Network
|
|
86
|
+
inet: 'String',
|
|
87
|
+
cidr: 'String',
|
|
88
|
+
macaddr: 'String',
|
|
89
|
+
macaddr8: 'String',
|
|
90
|
+
|
|
91
|
+
// Other
|
|
92
|
+
xml: 'String',
|
|
93
|
+
tsvector: 'String',
|
|
94
|
+
tsquery: 'String',
|
|
95
|
+
oid: 'Int',
|
|
96
|
+
point: 'String',
|
|
97
|
+
line: 'String',
|
|
98
|
+
box: 'String',
|
|
99
|
+
circle: 'String',
|
|
100
|
+
polygon: 'String',
|
|
101
|
+
path: 'String',
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return map[baseType] ?? 'String'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Check if column has a unique index */
|
|
108
|
+
function isUnique(table: EnrichedTable, colName: string): boolean {
|
|
109
|
+
return (
|
|
110
|
+
table.raw.indexes?.some(
|
|
111
|
+
(idx) => idx.unique === true && idx.parts?.length === 1 && idx.parts[0].column === colName,
|
|
112
|
+
) ?? false
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Format a Prisma default expression */
|
|
117
|
+
function formatPrismaDefault(defaultValue: string | undefined, isSerial: boolean): string | undefined {
|
|
118
|
+
if (isSerial) return '@default(autoincrement())'
|
|
119
|
+
if (!defaultValue) return undefined
|
|
120
|
+
|
|
121
|
+
if (defaultValue === 'now()' || defaultValue === 'CURRENT_TIMESTAMP') return '@default(now())'
|
|
122
|
+
if (defaultValue === 'gen_random_uuid()') return '@default(uuid())'
|
|
123
|
+
if (defaultValue === 'true' || defaultValue === 'false') return `@default(${defaultValue})`
|
|
124
|
+
if (/^\d+$/.test(defaultValue)) return `@default(${defaultValue})`
|
|
125
|
+
|
|
126
|
+
// Check if it looks like an expression (has parens)
|
|
127
|
+
if (defaultValue.includes('(')) return `@default(dbgenerated("${defaultValue}"))`
|
|
128
|
+
|
|
129
|
+
return `@default("${defaultValue}")`
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export default defineTemplate({
|
|
133
|
+
name: 'Prisma Schema',
|
|
134
|
+
description: 'Generate Prisma schema models from SQL schema',
|
|
135
|
+
language: 'sql',
|
|
136
|
+
configSchema,
|
|
137
|
+
|
|
138
|
+
generate(ctx) {
|
|
139
|
+
const schema = enrichRealm(ctx)
|
|
140
|
+
const tables = activeTables(schema)
|
|
141
|
+
|
|
142
|
+
// Build model name map for relations
|
|
143
|
+
const modelNameMap = new Map<string, string>()
|
|
144
|
+
for (const table of tables) {
|
|
145
|
+
modelNameMap.set(table.name, table.pascalName)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const provider = (ctx.config as any)?.provider ?? 'postgresql'
|
|
149
|
+
|
|
150
|
+
// Pre-compute: count how many FK relations target each model, to decide if we need named relations
|
|
151
|
+
// Also track reverse relations that need to be added to target models
|
|
152
|
+
const reverseRelations = new Map<string, Array<{ fromTable: string; relName: string; needsName: boolean }>>()
|
|
153
|
+
// Count per-model how many FKs point to the same target, keyed by "sourceTable -> targetTable"
|
|
154
|
+
const fkCountByTarget = new Map<string, Map<string, number>>()
|
|
155
|
+
|
|
156
|
+
for (const table of tables) {
|
|
157
|
+
if (table.belongsTo.length > 0) {
|
|
158
|
+
const targetCounts = new Map<string, number>()
|
|
159
|
+
for (const rel of table.belongsTo) {
|
|
160
|
+
targetCounts.set(rel.foreignTable, (targetCounts.get(rel.foreignTable) ?? 0) + 1)
|
|
161
|
+
}
|
|
162
|
+
fkCountByTarget.set(table.name, targetCounts)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Determine which relations need explicit names
|
|
167
|
+
interface FkRelation {
|
|
168
|
+
column: string
|
|
169
|
+
refTable: string
|
|
170
|
+
refColumn: string
|
|
171
|
+
constraintName: string
|
|
172
|
+
relName: string
|
|
173
|
+
relationName: string | undefined
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const tableRelations = new Map<string, FkRelation[]>()
|
|
177
|
+
|
|
178
|
+
for (const table of tables) {
|
|
179
|
+
if (table.belongsTo.length === 0) continue
|
|
180
|
+
|
|
181
|
+
const targetCounts = fkCountByTarget.get(table.name) ?? new Map()
|
|
182
|
+
const relations: FkRelation[] = []
|
|
183
|
+
|
|
184
|
+
for (const rel of table.belongsTo) {
|
|
185
|
+
const relName = rel.column.replace(/_id$/, '')
|
|
186
|
+
// Need explicit relation name for self-relations or multiple FKs to the same target
|
|
187
|
+
const isSelfRelation = rel.foreignTable === table.name
|
|
188
|
+
const needsRelationName = isSelfRelation || (targetCounts.get(rel.foreignTable) ?? 0) > 1
|
|
189
|
+
const relationName = needsRelationName ? rel.constraintName || `${table.name}_${rel.column}` : undefined
|
|
190
|
+
|
|
191
|
+
relations.push({
|
|
192
|
+
column: rel.column,
|
|
193
|
+
refTable: rel.foreignTable,
|
|
194
|
+
refColumn: rel.foreignColumn,
|
|
195
|
+
constraintName: rel.constraintName,
|
|
196
|
+
relName,
|
|
197
|
+
relationName,
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
// Track reverse relation
|
|
201
|
+
if (!reverseRelations.has(rel.foreignTable)) {
|
|
202
|
+
reverseRelations.set(rel.foreignTable, [])
|
|
203
|
+
}
|
|
204
|
+
reverseRelations.get(rel.foreignTable)!.push({
|
|
205
|
+
fromTable: table.name,
|
|
206
|
+
relName: needsRelationName ? relName : (modelNameMap.get(table.name) ?? toPascalCase(table.name)),
|
|
207
|
+
needsName: needsRelationName,
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
tableRelations.set(table.name, relations)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const blocks: string[] = [
|
|
215
|
+
'// Generated by @sqldoc/templates/prisma -- DO NOT EDIT',
|
|
216
|
+
'',
|
|
217
|
+
`datasource db {`,
|
|
218
|
+
` provider = "${provider}"`,
|
|
219
|
+
` url = env("DATABASE_URL")`,
|
|
220
|
+
`}`,
|
|
221
|
+
'',
|
|
222
|
+
`generator client {`,
|
|
223
|
+
` provider = "prisma-client-js"`,
|
|
224
|
+
`}`,
|
|
225
|
+
'',
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
// Enums
|
|
229
|
+
for (const e of schema.enums) {
|
|
230
|
+
blocks.push(`enum ${e.pascalName} {`)
|
|
231
|
+
for (const val of e.values) {
|
|
232
|
+
blocks.push(` ${val}`)
|
|
233
|
+
}
|
|
234
|
+
blocks.push('}')
|
|
235
|
+
blocks.push('')
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
for (const table of tables) {
|
|
239
|
+
const modelName = table.pascalName
|
|
240
|
+
const fieldLines: string[] = []
|
|
241
|
+
|
|
242
|
+
// Detect composite primary key (more than one PK column)
|
|
243
|
+
const pkCols = table.columns.filter((c: any) => c.isPrimaryKey)
|
|
244
|
+
const isCompositePk = pkCols.length > 1
|
|
245
|
+
|
|
246
|
+
for (const col of table.columns) {
|
|
247
|
+
let prismaType: string
|
|
248
|
+
if (col.typeOverride) {
|
|
249
|
+
prismaType = col.typeOverride
|
|
250
|
+
} else if (col.category === 'enum' && col.enumValues?.length) {
|
|
251
|
+
prismaType = toPascalCase(col.pgType)
|
|
252
|
+
} else {
|
|
253
|
+
prismaType = pgToPrisma(col.pgType)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (col.nullable && !prismaType.endsWith('[]')) {
|
|
257
|
+
prismaType += '?'
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const attrs: string[] = []
|
|
261
|
+
|
|
262
|
+
// For composite PKs, use @@id at the model level instead of @id on each column
|
|
263
|
+
if (col.isPrimaryKey && !isCompositePk) {
|
|
264
|
+
attrs.push('@id')
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const defaultAttr = formatPrismaDefault(col.defaultValue, col.isSerial)
|
|
268
|
+
if (defaultAttr) {
|
|
269
|
+
attrs.push(defaultAttr)
|
|
270
|
+
} else if (col.isSerial) {
|
|
271
|
+
attrs.push('@default(autoincrement())')
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (isUnique(table, col.name)) {
|
|
275
|
+
attrs.push('@unique')
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Map column name to Prisma field name + @map
|
|
279
|
+
const fieldName = col.name
|
|
280
|
+
const attrStr = attrs.length > 0 ? ` ${attrs.join(' ')}` : ''
|
|
281
|
+
fieldLines.push(` ${fieldName} ${prismaType}${attrStr}`)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Add relation fields for foreign keys on this table
|
|
285
|
+
const relations = tableRelations.get(table.name) ?? []
|
|
286
|
+
for (const rel of relations) {
|
|
287
|
+
const refModelName = modelNameMap.get(rel.refTable) ?? toPascalCase(rel.refTable)
|
|
288
|
+
const relNameAttr = rel.relationName ? `, name: "${rel.relationName}"` : ''
|
|
289
|
+
// If the FK column is nullable, the relation field must also be optional
|
|
290
|
+
const fkCol = table.columns.find((c: any) => c.name === rel.column)
|
|
291
|
+
const optionalMark = fkCol?.nullable ? '?' : ''
|
|
292
|
+
fieldLines.push(
|
|
293
|
+
` ${rel.relName} ${refModelName}${optionalMark} @relation(fields: [${rel.column}], references: [${rel.refColumn}]${relNameAttr})`,
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Add reverse relation fields (other models that reference this one)
|
|
298
|
+
const reverseRels = reverseRelations.get(table.name) ?? []
|
|
299
|
+
// Group by source table to handle naming
|
|
300
|
+
const reverseBySource = new Map<string, typeof reverseRels>()
|
|
301
|
+
for (const rev of reverseRels) {
|
|
302
|
+
if (!reverseBySource.has(rev.fromTable)) {
|
|
303
|
+
reverseBySource.set(rev.fromTable, [])
|
|
304
|
+
}
|
|
305
|
+
reverseBySource.get(rev.fromTable)!.push(rev)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
for (const [sourceTable, rels] of reverseBySource) {
|
|
309
|
+
const sourceModelName = modelNameMap.get(sourceTable) ?? toPascalCase(sourceTable)
|
|
310
|
+
if (rels.length === 1 && !rels[0].needsName) {
|
|
311
|
+
// Simple reverse: just add ModelName[]
|
|
312
|
+
const fieldName = sourceTable
|
|
313
|
+
fieldLines.push(` ${fieldName} ${sourceModelName}[]`)
|
|
314
|
+
} else {
|
|
315
|
+
// Multiple relations from same source: need named relations
|
|
316
|
+
for (const rel of rels) {
|
|
317
|
+
const sourceRelations = tableRelations.get(sourceTable) ?? []
|
|
318
|
+
const matchingRel = sourceRelations.find((r) => r.relName === rel.relName)
|
|
319
|
+
const relationName = matchingRel?.relationName
|
|
320
|
+
const relNameAttr = relationName ? `(name: "${relationName}")` : ''
|
|
321
|
+
const fieldName = `${rel.relName}_${sourceTable}`
|
|
322
|
+
fieldLines.push(` ${fieldName} ${sourceModelName}[] @relation${relNameAttr}`)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Add @@id for composite primary keys
|
|
328
|
+
if (isCompositePk) {
|
|
329
|
+
fieldLines.push('')
|
|
330
|
+
fieldLines.push(` @@id([${pkCols.map((c: any) => c.name).join(', ')}])`)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
blocks.push(`model ${modelName} {`)
|
|
334
|
+
blocks.push(fieldLines.join('\n'))
|
|
335
|
+
blocks.push('}')
|
|
336
|
+
blocks.push('')
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Views (read-only — represented as Prisma models with @@map)
|
|
340
|
+
for (const view of schema.views.filter((v) => !v.skipped)) {
|
|
341
|
+
const fieldLines: string[] = []
|
|
342
|
+
|
|
343
|
+
// Views need a dummy @id — use the first column as a stand-in
|
|
344
|
+
const firstCol = view.columns[0]
|
|
345
|
+
|
|
346
|
+
for (const col of view.columns) {
|
|
347
|
+
let prismaType: string
|
|
348
|
+
if (col.typeOverride) {
|
|
349
|
+
prismaType = col.typeOverride
|
|
350
|
+
} else if (col.category === 'enum' && col.enumValues?.length) {
|
|
351
|
+
prismaType = toPascalCase(col.pgType)
|
|
352
|
+
} else {
|
|
353
|
+
prismaType = pgToPrisma(col.pgType)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (col.nullable && !prismaType.endsWith('[]')) {
|
|
357
|
+
prismaType += '?'
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const attrs: string[] = []
|
|
361
|
+
if (firstCol && col.name === firstCol.name) {
|
|
362
|
+
attrs.push('@id')
|
|
363
|
+
}
|
|
364
|
+
const attrStr = attrs.length > 0 ? ` ${attrs.join(' ')}` : ''
|
|
365
|
+
fieldLines.push(` ${col.name} ${prismaType}${attrStr}`)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
fieldLines.push('')
|
|
369
|
+
fieldLines.push(` @@map("${view.name}")`)
|
|
370
|
+
|
|
371
|
+
blocks.push(`/// Read-only (from view)`)
|
|
372
|
+
blocks.push(`model ${view.pascalName} {`)
|
|
373
|
+
blocks.push(fieldLines.join('\n'))
|
|
374
|
+
blocks.push('}')
|
|
375
|
+
blocks.push('')
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
files: [
|
|
380
|
+
{
|
|
381
|
+
path: 'schema.prisma',
|
|
382
|
+
content: blocks.join('\n'),
|
|
383
|
+
},
|
|
384
|
+
],
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
})
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { defineTemplate } from '@sqldoc/ns-codegen'
|
|
2
|
+
import { activeTables, enrichRealm } from '../helpers/enrich.ts'
|
|
3
|
+
import { toPascalCase, toScreamingSnake } from '../helpers/naming.ts'
|
|
4
|
+
|
|
5
|
+
const PG_TO_PROTO: Record<string, string> = {
|
|
6
|
+
smallint: 'int32',
|
|
7
|
+
int2: 'int32',
|
|
8
|
+
integer: 'int32',
|
|
9
|
+
int: 'int32',
|
|
10
|
+
int4: 'int32',
|
|
11
|
+
bigint: 'int64',
|
|
12
|
+
int8: 'int64',
|
|
13
|
+
serial: 'int32',
|
|
14
|
+
serial4: 'int32',
|
|
15
|
+
bigserial: 'int64',
|
|
16
|
+
serial8: 'int64',
|
|
17
|
+
smallserial: 'int32',
|
|
18
|
+
serial2: 'int32',
|
|
19
|
+
real: 'float',
|
|
20
|
+
float4: 'float',
|
|
21
|
+
'double precision': 'double',
|
|
22
|
+
float8: 'double',
|
|
23
|
+
numeric: 'string',
|
|
24
|
+
decimal: 'string',
|
|
25
|
+
money: 'string',
|
|
26
|
+
text: 'string',
|
|
27
|
+
varchar: 'string',
|
|
28
|
+
'character varying': 'string',
|
|
29
|
+
char: 'string',
|
|
30
|
+
character: 'string',
|
|
31
|
+
name: 'string',
|
|
32
|
+
citext: 'string',
|
|
33
|
+
boolean: 'bool',
|
|
34
|
+
bool: 'bool',
|
|
35
|
+
timestamp: 'google.protobuf.Timestamp',
|
|
36
|
+
'timestamp without time zone': 'google.protobuf.Timestamp',
|
|
37
|
+
timestamptz: 'google.protobuf.Timestamp',
|
|
38
|
+
'timestamp with time zone': 'google.protobuf.Timestamp',
|
|
39
|
+
date: 'string',
|
|
40
|
+
time: 'string',
|
|
41
|
+
'time without time zone': 'string',
|
|
42
|
+
timetz: 'string',
|
|
43
|
+
interval: 'string',
|
|
44
|
+
bytea: 'bytes',
|
|
45
|
+
json: 'string',
|
|
46
|
+
jsonb: 'string',
|
|
47
|
+
uuid: 'string',
|
|
48
|
+
inet: 'string',
|
|
49
|
+
xml: 'string',
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function pgToProto(pgType: string): { type: string; repeated: boolean } {
|
|
53
|
+
const normalized = pgType.toLowerCase().trim()
|
|
54
|
+
|
|
55
|
+
if (normalized.endsWith('[]')) {
|
|
56
|
+
return { type: pgToProto(normalized.slice(0, -2)).type, repeated: true }
|
|
57
|
+
}
|
|
58
|
+
if (normalized.startsWith('_')) {
|
|
59
|
+
return { type: pgToProto(normalized.slice(1)).type, repeated: true }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const base = normalized.replace(/\(\d+(?:,\s*\d+)?\)/, '').trim()
|
|
63
|
+
return { type: PG_TO_PROTO[base] ?? 'string', repeated: false }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default defineTemplate({
|
|
67
|
+
name: 'Protocol Buffers',
|
|
68
|
+
description: 'Generate Protocol Buffers message definitions from SQL schema',
|
|
69
|
+
language: 'protobuf',
|
|
70
|
+
|
|
71
|
+
generate(ctx) {
|
|
72
|
+
const schema = enrichRealm(ctx)
|
|
73
|
+
const allColumns = [...schema.tables.flatMap((t) => t.columns), ...schema.views.flatMap((v) => v.columns)]
|
|
74
|
+
const needsTimestamp = allColumns.some((c) => {
|
|
75
|
+
const base = c.pgType
|
|
76
|
+
.toLowerCase()
|
|
77
|
+
.replace(/\(\d+(?:,\s*\d+)?\)/, '')
|
|
78
|
+
.trim()
|
|
79
|
+
return base.startsWith('timestamp')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const lines: string[] = [
|
|
83
|
+
'// Generated by @sqldoc/templates/protobuf -- DO NOT EDIT',
|
|
84
|
+
'syntax = "proto3";',
|
|
85
|
+
'',
|
|
86
|
+
'package schema;',
|
|
87
|
+
'',
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
if (needsTimestamp) {
|
|
91
|
+
lines.push('import "google/protobuf/timestamp.proto";')
|
|
92
|
+
lines.push('')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Enums
|
|
96
|
+
for (const e of schema.enums) {
|
|
97
|
+
const enumName = toPascalCase(e.name)
|
|
98
|
+
lines.push(`enum ${enumName} {`)
|
|
99
|
+
lines.push(` ${toScreamingSnake(e.name)}_UNSPECIFIED = 0;`)
|
|
100
|
+
e.values.forEach((v, i) => {
|
|
101
|
+
lines.push(` ${toScreamingSnake(e.name)}_${toScreamingSnake(v)} = ${i + 1};`)
|
|
102
|
+
})
|
|
103
|
+
lines.push('}')
|
|
104
|
+
lines.push('')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Composite types as messages
|
|
108
|
+
const composites = new Map<string, Array<{ name: string; type: string }>>()
|
|
109
|
+
for (const table of schema.tables) {
|
|
110
|
+
for (const col of table.columns) {
|
|
111
|
+
if (col.category === 'composite' && col.compositeFields?.length && !composites.has(col.pgType)) {
|
|
112
|
+
composites.set(col.pgType, col.compositeFields)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (const [name, fields] of composites) {
|
|
117
|
+
const msgName = toPascalCase(name)
|
|
118
|
+
lines.push(`message ${msgName} {`)
|
|
119
|
+
let fieldNum = 1
|
|
120
|
+
for (const f of fields) {
|
|
121
|
+
const { type, repeated } = pgToProto(f.type)
|
|
122
|
+
const prefix = repeated ? 'repeated ' : ''
|
|
123
|
+
lines.push(` ${prefix}${type} ${f.name} = ${fieldNum};`)
|
|
124
|
+
fieldNum++
|
|
125
|
+
}
|
|
126
|
+
lines.push('}')
|
|
127
|
+
lines.push('')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const table of activeTables(schema)) {
|
|
131
|
+
lines.push(`message ${table.pascalName} {`)
|
|
132
|
+
|
|
133
|
+
let fieldNum = 1
|
|
134
|
+
for (const col of table.columns) {
|
|
135
|
+
if (col.category === 'enum' && col.enumValues?.length) {
|
|
136
|
+
const enumType = toPascalCase(col.pgType)
|
|
137
|
+
const prefix = col.nullable ? 'optional ' : ''
|
|
138
|
+
lines.push(` ${prefix}${enumType} ${col.name} = ${fieldNum};`)
|
|
139
|
+
} else if (col.category === 'composite' && col.compositeFields?.length) {
|
|
140
|
+
const compositeType = toPascalCase(col.pgType)
|
|
141
|
+
const prefix = col.nullable ? 'optional ' : ''
|
|
142
|
+
lines.push(` ${prefix}${compositeType} ${col.name} = ${fieldNum};`)
|
|
143
|
+
} else {
|
|
144
|
+
const { type, repeated } = pgToProto(col.pgType)
|
|
145
|
+
const prefix = repeated ? 'repeated ' : col.nullable ? 'optional ' : ''
|
|
146
|
+
lines.push(` ${prefix}${type} ${col.name} = ${fieldNum};`)
|
|
147
|
+
}
|
|
148
|
+
fieldNum++
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
lines.push('}')
|
|
152
|
+
lines.push('')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Views (read-only)
|
|
156
|
+
for (const view of schema.views.filter((v) => !v.skipped)) {
|
|
157
|
+
lines.push(`// Read-only (from view)`)
|
|
158
|
+
lines.push(`message ${view.pascalName} {`)
|
|
159
|
+
|
|
160
|
+
let fieldNum = 1
|
|
161
|
+
for (const col of view.columns) {
|
|
162
|
+
if (col.category === 'enum' && col.enumValues?.length) {
|
|
163
|
+
const enumType = toPascalCase(col.pgType)
|
|
164
|
+
const prefix = col.nullable ? 'optional ' : ''
|
|
165
|
+
lines.push(` ${prefix}${enumType} ${col.name} = ${fieldNum};`)
|
|
166
|
+
} else if (col.category === 'composite' && col.compositeFields?.length) {
|
|
167
|
+
const compositeType = toPascalCase(col.pgType)
|
|
168
|
+
const prefix = col.nullable ? 'optional ' : ''
|
|
169
|
+
lines.push(` ${prefix}${compositeType} ${col.name} = ${fieldNum};`)
|
|
170
|
+
} else {
|
|
171
|
+
const { type, repeated } = pgToProto(col.pgType)
|
|
172
|
+
const prefix = repeated ? 'repeated ' : col.nullable ? 'optional ' : ''
|
|
173
|
+
lines.push(` ${prefix}${type} ${col.name} = ${fieldNum};`)
|
|
174
|
+
}
|
|
175
|
+
fieldNum++
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
lines.push('}')
|
|
179
|
+
lines.push('')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Functions (skip trigger functions)
|
|
183
|
+
for (const fn of schema.functions) {
|
|
184
|
+
const retRaw = fn.returnType?.type?.toLowerCase() ?? ''
|
|
185
|
+
if (retRaw === 'trigger') continue
|
|
186
|
+
|
|
187
|
+
// Request message
|
|
188
|
+
const reqName = `${fn.pascalName}Request`
|
|
189
|
+
lines.push(`message ${reqName} {`)
|
|
190
|
+
let fieldNum = 1
|
|
191
|
+
for (const a of fn.args.filter((a) => !a.name?.startsWith('_') && (a as any).mode !== 'OUT')) {
|
|
192
|
+
const { type, repeated } = pgToProto(a.type)
|
|
193
|
+
const prefix = repeated ? 'repeated ' : ''
|
|
194
|
+
lines.push(` ${prefix}${type} ${a.name || 'arg'} = ${fieldNum};`)
|
|
195
|
+
fieldNum++
|
|
196
|
+
}
|
|
197
|
+
lines.push('}')
|
|
198
|
+
lines.push('')
|
|
199
|
+
|
|
200
|
+
// Response message
|
|
201
|
+
const respName = `${fn.pascalName}Response`
|
|
202
|
+
lines.push(`message ${respName} {`)
|
|
203
|
+
if (retRaw.startsWith('setof ')) {
|
|
204
|
+
const tableName = retRaw.replace('setof ', '')
|
|
205
|
+
const table = schema.tables.find((t) => t.name === tableName)
|
|
206
|
+
const retType = table ? table.pascalName : toPascalCase(tableName)
|
|
207
|
+
lines.push(` repeated ${retType} results = 1;`)
|
|
208
|
+
} else if (fn.returnType) {
|
|
209
|
+
const { type, repeated } = pgToProto(fn.returnType.type)
|
|
210
|
+
const prefix = repeated ? 'repeated ' : ''
|
|
211
|
+
lines.push(` ${prefix}${type} result = 1;`)
|
|
212
|
+
}
|
|
213
|
+
lines.push('}')
|
|
214
|
+
lines.push('')
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { files: [{ path: 'schema.proto', content: lines.join('\n') }] }
|
|
218
|
+
},
|
|
219
|
+
})
|