@strav/cli 0.4.30 → 1.0.0-alpha.4

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 (63) hide show
  1. package/package.json +17 -41
  2. package/src/binder.ts +88 -0
  3. package/src/command.ts +297 -0
  4. package/src/config_list.ts +42 -0
  5. package/src/config_show.ts +50 -0
  6. package/src/console_provider.ts +46 -0
  7. package/src/exit_codes.ts +26 -0
  8. package/src/index.ts +60 -2
  9. package/src/key_generate.ts +66 -0
  10. package/src/make/index.ts +17 -0
  11. package/src/make/make_command_file.ts +27 -0
  12. package/src/make/make_controller.ts +24 -0
  13. package/src/make/make_factory.ts +25 -0
  14. package/src/make/make_job.ts +25 -0
  15. package/src/make/make_mail.ts +27 -0
  16. package/src/make/make_middleware.ts +23 -0
  17. package/src/make/make_migration.ts +48 -0
  18. package/src/make/make_model.ts +91 -0
  19. package/src/make/make_notification.ts +23 -0
  20. package/src/make/make_policy.ts +24 -0
  21. package/src/make/make_provider.ts +29 -0
  22. package/src/make/make_repository.ts +30 -0
  23. package/src/make/make_request.ts +24 -0
  24. package/src/make/make_seeder.ts +23 -0
  25. package/src/make/make_test.ts +22 -0
  26. package/src/make_command.ts +69 -0
  27. package/src/run_cli.ts +121 -0
  28. package/src/scaffold_console_provider.ts +45 -0
  29. package/src/signature.ts +171 -0
  30. package/src/subset_boot.ts +51 -0
  31. package/src/util_console_provider.ts +18 -0
  32. package/src/cli/bootstrap.ts +0 -77
  33. package/src/cli/command_loader.ts +0 -180
  34. package/src/cli/index.ts +0 -3
  35. package/src/cli/strav.ts +0 -13
  36. package/src/commands/db_seed.ts +0 -77
  37. package/src/commands/db_setup_roles.ts +0 -101
  38. package/src/commands/generate_api.ts +0 -93
  39. package/src/commands/generate_key.ts +0 -47
  40. package/src/commands/generate_models.ts +0 -49
  41. package/src/commands/generate_seeder.ts +0 -68
  42. package/src/commands/migration_compare.ts +0 -167
  43. package/src/commands/migration_fresh.ts +0 -148
  44. package/src/commands/migration_generate.ts +0 -84
  45. package/src/commands/migration_rollback.ts +0 -54
  46. package/src/commands/migration_run.ts +0 -45
  47. package/src/commands/package_install.ts +0 -161
  48. package/src/commands/queue_flush.ts +0 -35
  49. package/src/commands/queue_retry.ts +0 -34
  50. package/src/commands/queue_work.ts +0 -101
  51. package/src/commands/scheduler_work.ts +0 -46
  52. package/src/commands/tenant_create.ts +0 -35
  53. package/src/commands/tenant_delete.ts +0 -64
  54. package/src/commands/tenant_list.ts +0 -39
  55. package/src/config/loader.ts +0 -50
  56. package/src/generators/api_generator.ts +0 -1035
  57. package/src/generators/config.ts +0 -113
  58. package/src/generators/doc_generator.ts +0 -996
  59. package/src/generators/index.ts +0 -11
  60. package/src/generators/model_generator.ts +0 -596
  61. package/src/generators/route_generator.ts +0 -187
  62. package/src/generators/test_generator.ts +0 -1667
  63. package/tsconfig.json +0 -5
@@ -1,11 +0,0 @@
1
- export { default as ModelGenerator } from './model_generator.ts'
2
- export { default as ApiGenerator } from './api_generator.ts'
3
- export { default as RouteGenerator } from './route_generator.ts'
4
- export { default as TestGenerator } from './test_generator.ts'
5
- export { default as DocGenerator } from './doc_generator.ts'
6
- export type { GeneratedFile } from './model_generator.ts'
7
- export { ApiRouting } from './route_generator.ts'
8
- export { toRouteSegment, toChildSegment } from './route_generator.ts'
9
- export type { ApiRoutingConfig } from './route_generator.ts'
10
- export type { GeneratorConfig, GeneratorPaths } from './config.ts'
11
- export { resolvePaths, relativeImport, formatAndWrite } from './config.ts'
@@ -1,596 +0,0 @@
1
- import { join } from 'node:path'
2
- import { Archetype } from '@strav/database/schema/types'
3
- import type { SchemaDefinition } from '@strav/database/schema/types'
4
- import type {
5
- DatabaseRepresentation,
6
- TableDefinition,
7
- ColumnDefinition,
8
- EnumDefinition,
9
- } from '@strav/database/schema/database_representation'
10
- import type { PostgreSQLCustomType } from '@strav/database/schema/postgres'
11
- import { toSnakeCase, toCamelCase, toPascalCase } from '@strav/kernel/helpers/strings'
12
- import type { GeneratorConfig, GeneratorPaths, WriteResult } from './config.ts'
13
- import { resolvePaths, relativeImport, formatAndWrite } from './config.ts'
14
-
15
- export interface GeneratedFile {
16
- path: string
17
- content: string
18
- }
19
-
20
- export default class ModelGenerator {
21
- private schemaMap: Map<string, SchemaDefinition>
22
- private paths: GeneratorPaths
23
- private config?: GeneratorConfig
24
-
25
- constructor(
26
- private schemas: SchemaDefinition[],
27
- private representation: DatabaseRepresentation,
28
- config?: GeneratorConfig
29
- ) {
30
- this.schemaMap = new Map(schemas.map(s => [s.name, s]))
31
- this.paths = resolvePaths(config)
32
- this.config = config
33
- }
34
-
35
- /** Generate all file contents without writing to disk. */
36
- generate(): GeneratedFile[] {
37
- const files: GeneratedFile[] = []
38
-
39
- const enumFiles = this.generateEnums()
40
- files.push(...enumFiles)
41
-
42
- const modelFiles = this.generateModels()
43
- files.push(...modelFiles)
44
-
45
- // Barrel exports
46
- if (enumFiles.length > 0) {
47
- files.push(this.generateBarrel(this.paths.enums, enumFiles, 'named'))
48
- }
49
- if (modelFiles.length > 0) {
50
- files.push(this.generateBarrel(this.paths.models, modelFiles, 'default'))
51
- }
52
-
53
- return files
54
- }
55
-
56
- /** Generate, format with Prettier, and write all files to disk. */
57
- async writeAll(force?: boolean): Promise<WriteResult> {
58
- const files = this.generate()
59
- return formatAndWrite(files, { force })
60
- }
61
-
62
- // ---------------------------------------------------------------------------
63
- // Enum generation
64
- // ---------------------------------------------------------------------------
65
-
66
- private generateEnums(): GeneratedFile[] {
67
- const files: GeneratedFile[] = []
68
- const enumsByEntity = new Map<string, EnumDefinition[]>()
69
-
70
- for (const enumDef of this.representation.enums) {
71
- const entity = this.findEnumOwner(enumDef.name)
72
- const group = enumsByEntity.get(entity) ?? []
73
- group.push(enumDef)
74
- enumsByEntity.set(entity, group)
75
- }
76
-
77
- for (const [entity, enums] of enumsByEntity) {
78
- const lines: string[] = []
79
- for (let i = 0; i < enums.length; i++) {
80
- const enumDef = enums[i]!
81
- const enumName = toPascalCase(enumDef.name)
82
- lines.push(`export enum ${enumName} {`)
83
- for (const value of enumDef.values) {
84
- lines.push(` ${toPascalCase(value)} = '${value}',`)
85
- }
86
- lines.push('}')
87
- if (i < enums.length - 1) lines.push('')
88
- }
89
- lines.push('')
90
-
91
- files.push({
92
- path: join(this.paths.enums, `${entity}.ts`),
93
- content: lines.join('\n'),
94
- })
95
- }
96
-
97
- return files
98
- }
99
-
100
- /** Find which schema owns an enum by matching pgType.name across all fields. */
101
- private findEnumOwner(enumName: string): string {
102
- for (const schema of this.schemas) {
103
- for (const fieldDef of Object.values(schema.fields)) {
104
- if (isCustomType(fieldDef.pgType) && fieldDef.pgType.name === enumName) {
105
- return schema.name
106
- }
107
- }
108
- }
109
- // Fallback: derive from name prefix
110
- const idx = enumName.lastIndexOf('_')
111
- return idx > 0 ? enumName.substring(0, idx) : enumName
112
- }
113
-
114
- // ---------------------------------------------------------------------------
115
- // Model generation
116
- // ---------------------------------------------------------------------------
117
-
118
- private generateModels(): GeneratedFile[] {
119
- const files: GeneratedFile[] = []
120
- const assocIndex = this.buildAssociationIndex()
121
-
122
- for (const schema of this.schemas) {
123
- if (schema.archetype === Archetype.Association) continue
124
-
125
- const table = this.representation.tables.find(t => t.name === toSnakeCase(schema.name))
126
- if (!table) continue
127
-
128
- files.push(this.generateModel(schema, table, assocIndex))
129
- }
130
-
131
- return files
132
- }
133
-
134
- private generateModel(
135
- schema: SchemaDefinition,
136
- table: TableDefinition,
137
- assocIndex: Map<string, AssociationEntry[]>
138
- ): GeneratedFile {
139
- const className = toPascalCase(schema.name)
140
- const timestampNames = new Set(['created_at', 'updated_at', 'deleted_at'])
141
-
142
- // Categorize columns
143
- const pkColumns: ColumnDefinition[] = []
144
- const fkColumns: ColumnDefinition[] = []
145
- const normalColumns: ColumnDefinition[] = []
146
- const timestampColumns: ColumnDefinition[] = []
147
-
148
- for (const col of table.columns) {
149
- if (col.primaryKey) {
150
- pkColumns.push(col)
151
- } else if (this.isForeignKey(col.name, table)) {
152
- fkColumns.push(col)
153
- } else if (timestampNames.has(col.name)) {
154
- timestampColumns.push(col)
155
- } else {
156
- normalColumns.push(col)
157
- }
158
- }
159
-
160
- // Sort timestamps in canonical order
161
- const tsOrder = ['created_at', 'updated_at', 'deleted_at']
162
- timestampColumns.sort((a, b) => tsOrder.indexOf(a.name) - tsOrder.indexOf(b.name))
163
-
164
- // Build references and associations
165
- const references = this.buildReferences(schema)
166
- const associations = assocIndex.get(schema.name) ?? []
167
-
168
- // Soft deletes detection
169
- const hasSoftDeletes = timestampColumns.some(c => c.name === 'deleted_at')
170
-
171
- // Track imports
172
- const enumImports = new Map<string, string[]>() // entity → enum names
173
- const modelImports = new Set<string>() // PascalCase model names
174
- let needsPrimaryImport = false
175
- let needsReferenceImport = false
176
- let needsAssociateImport = false
177
-
178
- for (const ref of references) {
179
- modelImports.add(ref.modelClass)
180
- }
181
- for (const assoc of associations) {
182
- modelImports.add(assoc.model)
183
- }
184
-
185
- // Build property lines per section
186
- const sections: string[][] = []
187
-
188
- if (pkColumns.length > 0) {
189
- const lines: string[] = []
190
- for (const col of pkColumns) {
191
- const propName = toCamelCase(col.name)
192
- const tsType = this.mapTsType(col, enumImports)
193
- const schemaDefault = this.formatSchemaDefault(col, schema, tsType)
194
- lines.push(' @primary')
195
- needsPrimaryImport = true
196
- if (schemaDefault) {
197
- lines.push(` ${propName}: ${tsType} = ${schemaDefault}`)
198
- } else {
199
- lines.push(` declare ${propName}: ${tsType}`)
200
- }
201
- }
202
- sections.push(lines)
203
- }
204
-
205
- if (fkColumns.length > 0) {
206
- const lines: string[] = []
207
- for (const col of fkColumns) {
208
- const propName = toCamelCase(col.name)
209
- const tsType = this.mapTsType(col, enumImports)
210
- const nullable = col.notNull ? '' : ' | null'
211
- lines.push(` declare ${propName}: ${tsType}${nullable}`)
212
- }
213
- sections.push(lines)
214
- }
215
-
216
- if (normalColumns.length > 0) {
217
- const lines: string[] = []
218
- for (const col of normalColumns) {
219
- const propName = toCamelCase(col.name)
220
- const tsType = this.mapTsType(col, enumImports)
221
- const schemaDefault = this.formatSchemaDefault(col, schema, tsType)
222
- if (schemaDefault) {
223
- lines.push(` ${propName}: ${tsType} = ${schemaDefault}`)
224
- } else {
225
- const nullable = col.notNull ? '' : ' | null'
226
- lines.push(` declare ${propName}: ${tsType}${nullable}`)
227
- }
228
- }
229
- sections.push(lines)
230
- }
231
-
232
- if (timestampColumns.length > 0) {
233
- const lines: string[] = []
234
- for (const col of timestampColumns) {
235
- const propName = toCamelCase(col.name)
236
- const nullable = col.notNull ? '' : ' | null'
237
- lines.push(` declare ${propName}: DateTime${nullable}`)
238
- }
239
- sections.push(lines)
240
- }
241
-
242
- if (references.length > 0) {
243
- const lines: string[] = []
244
- for (const ref of references) {
245
- lines.push(
246
- ` @reference({ model: '${ref.modelClass}', foreignKey: '${ref.foreignKey}', targetPK: '${ref.targetPK}' })`
247
- )
248
- lines.push(` declare ${ref.propName}: ${ref.modelClass}`)
249
- needsReferenceImport = true
250
- }
251
- sections.push(lines)
252
- }
253
-
254
- if (associations.length > 0) {
255
- const lines: string[] = []
256
- for (const assoc of associations) {
257
- lines.push(
258
- ` @associate({ through: '${assoc.through}', foreignKey: '${assoc.foreignKey}', otherKey: '${assoc.otherKey}', model: '${assoc.model}', targetPK: '${assoc.targetPK}' })`
259
- )
260
- lines.push(` declare ${assoc.property}: ${assoc.model}[]`)
261
- needsAssociateImport = true
262
- }
263
- sections.push(lines)
264
- }
265
-
266
- // Assemble imports
267
- const importLines: string[] = []
268
- importLines.push("import { DateTime } from 'luxon'")
269
- importLines.push("import BaseModel from '@strav/database/orm/base_model'")
270
-
271
- const decoratorImports: string[] = []
272
- if (needsPrimaryImport) decoratorImports.push('primary')
273
- if (needsReferenceImport) decoratorImports.push('reference')
274
- if (needsAssociateImport) decoratorImports.push('associate')
275
- if (decoratorImports.length > 0) {
276
- importLines.push(
277
- `import { ${decoratorImports.join(', ')} } from '@strav/database/orm/decorators'`
278
- )
279
- }
280
-
281
- for (const [entity, enumNames] of enumImports) {
282
- const enumImportPath = relativeImport(this.paths.models, this.paths.enums)
283
- importLines.push(`import { ${enumNames.join(', ')} } from '${enumImportPath}/${entity}'`)
284
- }
285
-
286
- for (const modelName of modelImports) {
287
- if (modelName === className) continue // don't import self
288
- const fileName = toSnakeCase(modelName)
289
- importLines.push(`import type ${modelName} from './${fileName}'`)
290
- }
291
-
292
- // Assemble file
293
- const lines: string[] = []
294
- lines.push('// Generated by Strav — DO NOT EDIT')
295
- lines.push(...importLines)
296
- lines.push('')
297
- lines.push(`export default class ${(className)} extends BaseModel {`)
298
-
299
- if (hasSoftDeletes) {
300
- lines.push(' static override softDeletes = true')
301
- if (sections.length > 0) lines.push('')
302
- }
303
-
304
- for (let i = 0; i < sections.length; i++) {
305
- lines.push(...sections[i]!)
306
- if (i < sections.length - 1) lines.push('')
307
- }
308
-
309
- lines.push('}')
310
- lines.push('')
311
-
312
- return {
313
- path: join(this.paths.models, `${toSnakeCase(schema.name)}.ts`),
314
- content: lines.join('\n'),
315
- }
316
- }
317
-
318
- // ---------------------------------------------------------------------------
319
- // Type mapping
320
- // ---------------------------------------------------------------------------
321
-
322
- /** Map a column to its TypeScript type string. Registers enum imports as a side-effect. */
323
- private mapTsType(col: ColumnDefinition, enumImports: Map<string, string[]>): string {
324
- const pgType = col.pgType
325
-
326
- // Custom enum type
327
- if (isCustomType(pgType)) {
328
- const enumName = toPascalCase(pgType.name)
329
- const ownerEntity = this.findEnumOwner(pgType.name)
330
- const existing = enumImports.get(ownerEntity) ?? []
331
- if (!existing.includes(enumName)) {
332
- existing.push(enumName)
333
- enumImports.set(ownerEntity, existing)
334
- }
335
- return enumName
336
- }
337
-
338
- if (typeof pgType !== 'string') return 'unknown'
339
-
340
- switch (pgType) {
341
- case 'serial':
342
- case 'integer':
343
- case 'smallint':
344
- case 'smallserial':
345
- case 'real':
346
- case 'double_precision':
347
- case 'decimal':
348
- case 'numeric':
349
- case 'money':
350
- return 'number'
351
-
352
- case 'bigserial':
353
- case 'bigint':
354
- return 'bigint'
355
-
356
- case 'varchar':
357
- case 'character_varying':
358
- case 'char':
359
- case 'character':
360
- case 'text':
361
- case 'uuid':
362
- return 'string'
363
-
364
- case 'boolean':
365
- return 'boolean'
366
-
367
- case 'timestamptz':
368
- case 'timestamp':
369
- return 'DateTime'
370
-
371
- case 'json':
372
- case 'jsonb':
373
- return 'Record<string, unknown>'
374
-
375
- case 'date':
376
- case 'time':
377
- case 'timetz':
378
- case 'interval':
379
- return 'string'
380
-
381
- default:
382
- return 'string'
383
- }
384
- }
385
-
386
- /**
387
- * If the column has a schema-level default, return the TS expression for it.
388
- * Returns null if no schema default exists.
389
- */
390
- private formatSchemaDefault(
391
- col: ColumnDefinition,
392
- schema: SchemaDefinition,
393
- tsType: string
394
- ): string | null {
395
- const fieldDef = this.findFieldForColumn(col.name, schema)
396
- if (!fieldDef || fieldDef.defaultValue === undefined) return null
397
-
398
- const defaultValue = fieldDef.defaultValue
399
-
400
- // Enum default
401
- if (isCustomType(col.pgType)) {
402
- const enumName = toPascalCase(col.pgType.name)
403
- const member = toPascalCase(String(defaultValue))
404
- return `${enumName}.${member}`
405
- }
406
-
407
- // Literal defaults
408
- if (typeof defaultValue === 'string') return `'${defaultValue}'`
409
- if (typeof defaultValue === 'number') return String(defaultValue)
410
- if (typeof defaultValue === 'boolean') return String(defaultValue)
411
-
412
- return null
413
- }
414
-
415
- /**
416
- * Find the schema field definition that corresponds to a given column name.
417
- * Only returns non-reference fields (FK columns derived from references have no direct field).
418
- */
419
- private findFieldForColumn(
420
- colName: string,
421
- schema: SchemaDefinition
422
- ): import('@strav/database/schema/field_definition').FieldDefinition | null {
423
- for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
424
- if (fieldDef.references) continue
425
- if (toSnakeCase(fieldName) === colName) {
426
- return fieldDef
427
- }
428
- }
429
- return null
430
- }
431
-
432
- // ---------------------------------------------------------------------------
433
- // Reference detection
434
- // ---------------------------------------------------------------------------
435
-
436
- private buildReferences(
437
- schema: SchemaDefinition
438
- ): { propName: string; modelClass: string; foreignKey: string; targetPK: string }[] {
439
- const refs: { propName: string; modelClass: string; foreignKey: string; targetPK: string }[] =
440
- []
441
-
442
- // Parent references
443
- if (schema.parents) {
444
- for (const parentName of schema.parents) {
445
- const parentPK = this.findSchemaPK(parentName)
446
- const fkCol = `${toSnakeCase(parentName)}_${toSnakeCase(parentPK)}`
447
- refs.push({
448
- propName: toCamelCase(parentName),
449
- modelClass: this.prefixModelName(parentName),
450
- foreignKey: toCamelCase(fkCol),
451
- targetPK: parentPK,
452
- })
453
- }
454
- }
455
-
456
- // Reference fields
457
- for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
458
- if (fieldDef.references) {
459
- const refPK = this.findSchemaPK(fieldDef.references)
460
- const fkCol = `${toSnakeCase(fieldName)}_${toSnakeCase(refPK)}`
461
- refs.push({
462
- propName: toCamelCase(fieldName),
463
- modelClass: this.prefixModelName(fieldDef.references),
464
- foreignKey: toCamelCase(fkCol),
465
- targetPK: refPK,
466
- })
467
- }
468
- }
469
-
470
- return refs
471
- }
472
-
473
- /** Find the primary key field name (camelCase) for a schema. Defaults to 'id'. */
474
- private findSchemaPK(schemaName: string): string {
475
- const schema = this.schemaMap.get(schemaName)
476
- if (!schema) return 'id'
477
- for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
478
- if (fieldDef.primaryKey) return fieldName
479
- }
480
- return 'id'
481
- }
482
-
483
- // ---------------------------------------------------------------------------
484
- // Association index
485
- // ---------------------------------------------------------------------------
486
-
487
- /**
488
- * Build an index: entity name → association entries.
489
- * Only association schemas with an `as` option produce entries.
490
- */
491
- private buildAssociationIndex(): Map<string, AssociationEntry[]> {
492
- const index = new Map<string, AssociationEntry[]>()
493
-
494
- for (const schema of this.schemas) {
495
- if (schema.archetype !== Archetype.Association || !schema.associates || !schema.as) continue
496
-
497
- const [entityA, entityB] = schema.associates!
498
- const pivotTable = toSnakeCase(schema.name)
499
- const pkA = this.findSchemaPK(entityA!)
500
- const pkB = this.findSchemaPK(entityB!)
501
- const fkA = `${toSnakeCase(entityA!)}_${toSnakeCase(pkA)}`
502
- const fkB = `${toSnakeCase(entityB!)}_${toSnakeCase(pkB)}`
503
-
504
- // Entity A gets a property pointing to Entity B
505
- if (schema.as![entityA!]) {
506
- const entries = index.get(entityA!) ?? []
507
- entries.push({
508
- property: schema.as![entityA!]!,
509
- through: pivotTable,
510
- foreignKey: fkA,
511
- otherKey: fkB,
512
- model: this.prefixModelName(entityB!),
513
- targetPK: pkB,
514
- })
515
- index.set(entityA!, entries)
516
- }
517
-
518
- // Entity B gets a property pointing to Entity A
519
- if (schema.as![entityB!]) {
520
- const entries = index.get(entityB!) ?? []
521
- entries.push({
522
- property: schema.as![entityB!]!,
523
- through: pivotTable,
524
- foreignKey: fkB,
525
- otherKey: fkA,
526
- model: this.prefixModelName(entityA!),
527
- targetPK: pkA,
528
- })
529
- index.set(entityB!, entries)
530
- }
531
- }
532
-
533
- return index
534
- }
535
-
536
- // ---------------------------------------------------------------------------
537
- // Barrel generation
538
- // ---------------------------------------------------------------------------
539
-
540
- /**
541
- * Generate a barrel (index.ts) file that re-exports all generated files
542
- * in a directory. `mode` controls the export style:
543
- * - `'default'` → `export { default as ClassName } from './file'`
544
- * - `'named'` → `export * from './file'`
545
- */
546
- private generateBarrel(
547
- dir: string,
548
- files: GeneratedFile[],
549
- mode: 'default' | 'named'
550
- ): GeneratedFile {
551
- const lines: string[] = ['// Generated by Strav — DO NOT EDIT', '']
552
-
553
- for (const file of files) {
554
- const basename = file.path.split('/').pop()!.replace(/\.ts$/, '')
555
- if (mode === 'named') {
556
- lines.push(`export * from './${basename}'`)
557
- } else {
558
- const className = (toPascalCase(basename))
559
- lines.push(`export { default as ${className} } from './${basename}'`)
560
- }
561
- }
562
-
563
- lines.push('')
564
-
565
- return {
566
- path: join(dir, 'index.ts'),
567
- content: lines.join('\n'),
568
- }
569
- }
570
-
571
- // ---------------------------------------------------------------------------
572
- // Helpers
573
- // ---------------------------------------------------------------------------
574
-
575
- /** Convert a schema name to its generated model class name. */
576
- private prefixModelName(modelName: string): string {
577
- return toPascalCase(modelName)
578
- }
579
-
580
- private isForeignKey(columnName: string, table: TableDefinition): boolean {
581
- return table.foreignKeys.some(fk => fk.columns.includes(columnName))
582
- }
583
- }
584
-
585
- interface AssociationEntry {
586
- property: string
587
- through: string
588
- foreignKey: string
589
- otherKey: string
590
- model: string
591
- targetPK: string
592
- }
593
-
594
- function isCustomType(pgType: unknown): pgType is PostgreSQLCustomType {
595
- return typeof pgType === 'object' && pgType !== null && (pgType as any).type === 'custom'
596
- }