@stacksjs/orm 0.64.6 → 0.65.0

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/src/utils.ts CHANGED
@@ -1,9 +1,18 @@
1
+ import type {
2
+ Attributes,
3
+ FieldArrayElement,
4
+ Model,
5
+ ModelElement,
6
+ RelationConfig,
7
+ } from '@stacksjs/types'
8
+ import process from 'node:process'
1
9
  import { generator, parser, traverse } from '@stacksjs/build'
10
+ import { italic, log } from '@stacksjs/cli'
2
11
  import { path } from '@stacksjs/path'
3
- import { fs } from '@stacksjs/storage'
4
- import { plural, snakeCase } from '@stacksjs/strings'
5
- import type { Attributes } from '@stacksjs/types'
6
- import type { Model } from '@stacksjs/types'
12
+ import { fs, globSync } from '@stacksjs/storage'
13
+ import { pascalCase, plural, singular, snakeCase } from '@stacksjs/strings'
14
+ import { ExitCode } from '@stacksjs/types'
15
+ import { isString } from '@stacksjs/validation'
7
16
 
8
17
  type ModelPath = string
9
18
 
@@ -16,7 +25,8 @@ export async function modelTableName(model: Model | ModelPath): Promise<string>
16
25
  }
17
26
 
18
27
  export function getModelName(model: Model, modelPath: string): string {
19
- if (model.name) return model.name
28
+ if (model.name)
29
+ return model.name
20
30
 
21
31
  const baseName = path.basename(modelPath)
22
32
 
@@ -24,12 +34,1850 @@ export function getModelName(model: Model, modelPath: string): string {
24
34
  }
25
35
 
26
36
  export function getTableName(model: Model, modelPath: string): string {
27
- if (model.table) return model.table
37
+ if (model.table)
38
+ return model.table
28
39
 
29
40
  return snakeCase(plural(getModelName(model, modelPath)))
30
41
  }
31
42
 
32
- export async function extractFieldsFromModel(filePath: string) {
43
+ export function getPivotTableName(formattedModelName: string, modelRelationTable: string): string {
44
+ // Create an array of the model names
45
+ const models = [formattedModelName, modelRelationTable]
46
+
47
+ // Sort the array alphabetically
48
+ models.sort()
49
+
50
+ models[0] = singular(models[0] || '')
51
+
52
+ // Join the sorted array with an underscore
53
+ const pivotTableName = models.join('_')
54
+
55
+ return pivotTableName
56
+ }
57
+
58
+ export function hasRelations(obj: any, key: string): boolean {
59
+ return key in obj
60
+ }
61
+
62
+ export async function getRelations(model: Model, modelName: string): Promise<RelationConfig[]> {
63
+ const relationsArray = ['hasOne', 'belongsTo', 'hasMany', 'belongsToMany', 'hasOneThrough']
64
+ const relationships = []
65
+
66
+ for (const relation of relationsArray) {
67
+ if (hasRelations(model, relation)) {
68
+ for (const relationInstance of (model[relation as keyof Model] as any[]) || []) {
69
+ let relationModel = relationInstance.model
70
+
71
+ if (isString(relationInstance)) {
72
+ relationModel = relationInstance
73
+ }
74
+
75
+ const modelRelationPath = path.userModelsPath(`${relationModel}.ts`)
76
+ const modelRelation = (await import(modelRelationPath)).default as Model
77
+ const modelRelationTable = getTableName(modelRelation, modelRelationPath)
78
+ const formattedModelName = modelName.toLowerCase()
79
+
80
+ relationships.push({
81
+ relationship: relation,
82
+ model: relationModel,
83
+ table: modelRelationTable,
84
+ foreignKey: relationInstance.foreignKey || `${formattedModelName}_id`,
85
+ relationName: relationInstance.relationName || '',
86
+ throughModel: relationInstance.through || '',
87
+ throughForeignKey: relationInstance.throughForeignKey || '',
88
+ pivotTable:
89
+ relationInstance?.pivotTable
90
+ || getPivotTableName(plural(formattedModelName), plural(modelRelation.table || '')),
91
+ })
92
+ }
93
+ }
94
+ }
95
+
96
+ return relationships
97
+ }
98
+
99
+ export function getRelationType(relation: string): string {
100
+ const belongToType = /belongs/
101
+ const hasType = /has/
102
+ const throughType = /Through/
103
+
104
+ if (throughType.test(relation))
105
+ return 'throughType'
106
+
107
+ if (belongToType.test(relation))
108
+ return 'belongsType'
109
+
110
+ if (hasType.test(relation))
111
+ return 'hasType'
112
+
113
+ return ''
114
+ }
115
+
116
+ export function getRelationCount(relation: string): string {
117
+ const singular = /One/
118
+ const plural = /Many/
119
+
120
+ if (plural.test(relation))
121
+ return 'many'
122
+
123
+ if (singular.test(relation))
124
+ return 'one'
125
+
126
+ return ''
127
+ }
128
+
129
+ export async function getPivotTables(
130
+ model: Model,
131
+ modelPath: string,
132
+ ): Promise<{ table: string, firstForeignKey?: string, secondForeignKey?: string }[]> {
133
+ const pivotTable = []
134
+
135
+ if ('belongsToMany' in model) {
136
+ const belongsToManyArr = model.belongsToMany || []
137
+ for (const belongsToManyRelation of belongsToManyArr) {
138
+ const modelRelationPath = path.userModelsPath(`${belongsToManyRelation}.ts`)
139
+ const modelRelation = (await import(modelRelationPath)).default as Model
140
+ const modelName = getModelName(model, modelPath)
141
+ const formattedModelName = modelName.toLowerCase()
142
+
143
+ const firstForeignKey
144
+ = typeof belongsToManyRelation === 'object' && 'firstForeignKey' in belongsToManyRelation
145
+ ? belongsToManyRelation.firstForeignKey
146
+ : `${modelName.toLowerCase()}_${model.primaryKey}`
147
+
148
+ const secondForeignKey
149
+ = typeof belongsToManyRelation === 'object' && 'secondForeignKey' in belongsToManyRelation
150
+ ? belongsToManyRelation.secondForeignKey
151
+ : `${modelRelation.name?.toLowerCase()}_${model.primaryKey}`
152
+
153
+ pivotTable.push({
154
+ table:
155
+ (typeof belongsToManyRelation === 'object' && 'pivotTable' in belongsToManyRelation
156
+ ? belongsToManyRelation.pivotTable
157
+ : undefined) ?? `${formattedModelName}_${modelRelation.table}`,
158
+ firstForeignKey,
159
+ secondForeignKey,
160
+ })
161
+ }
162
+
163
+ return pivotTable
164
+ }
165
+
166
+ return []
167
+ }
168
+
169
+ export async function fetchOtherModelRelations(model: Model, modelName?: string): Promise<RelationConfig[]> {
170
+ const modelFiles = globSync([path.userModelsPath('*.ts')], { absolute: true })
171
+ const modelRelations = []
172
+
173
+ for (let i = 0; i < modelFiles.length; i++) {
174
+ const modelFileElement = modelFiles[i] as string
175
+ const modelFile = await import(modelFileElement)
176
+
177
+ if (modelName === modelFile.default.name)
178
+ continue
179
+
180
+ const otherModelName = getModelName(modelFile, modelFileElement)
181
+ const relations = await getRelations(modelFile.default, otherModelName)
182
+
183
+ if (!relations.length)
184
+ continue
185
+
186
+ const relation = relations.find(relation => relation.model === modelName)
187
+
188
+ if (relation)
189
+ modelRelations.push(relation)
190
+ }
191
+
192
+ return modelRelations
193
+ }
194
+
195
+ export function getHiddenAttributes(attributes: Attributes | undefined): string[] {
196
+ if (attributes === undefined)
197
+ return []
198
+
199
+ return Object.keys(attributes).filter((key) => {
200
+ if (attributes === undefined)
201
+ return false
202
+
203
+ return attributes[key]?.hidden === true
204
+ })
205
+ }
206
+
207
+ export function getFillableAttributes(attributes: Attributes | undefined): string[] {
208
+ if (attributes === undefined)
209
+ return []
210
+
211
+ return Object.keys(attributes)
212
+ .filter((key) => {
213
+ if (attributes === undefined)
214
+ return false
215
+
216
+ return attributes[key]?.fillable === true
217
+ })
218
+ .map(attribute => snakeCase(attribute))
219
+ }
220
+
221
+ export async function writeModelNames(): Promise<void> {
222
+ const models = globSync([path.userModelsPath('*.ts')], { absolute: true })
223
+ let fileString = `export type ModelNames = `
224
+
225
+ for (let i = 0; i < models.length; i++) {
226
+ const modelPath = models[i] as string
227
+ const model = (await import(modelPath)).default as Model
228
+ const modelName = getModelName(model, modelPath)
229
+
230
+ fileString += `'${modelName}'`
231
+
232
+ if (i < models.length - 1) {
233
+ fileString += ' | '
234
+ }
235
+
236
+ // Ensure the directory exists
237
+ const typesDir = path.dirname(path.typesPath(`src/model-names.ts`))
238
+ await fs.promises.mkdir(typesDir, { recursive: true })
239
+
240
+ // Write to the file
241
+ const typeFilePath = path.typesPath(`src/model-names.ts`)
242
+ await fs.promises.writeFile(typeFilePath, fileString, 'utf8')
243
+ }
244
+ }
245
+
246
+ export async function writeModelRequest(): Promise<void> {
247
+ const modelFiles = globSync([path.userModelsPath('*.ts')], { absolute: true })
248
+ const requestD = Bun.file(path.frameworkPath('types/requests.d.ts'))
249
+
250
+ let importTypes = ``
251
+ let importTypesString = ``
252
+ let typeString = `import { Request } from '../core/router/src/request'\nimport type { VineType } from '@stacksjs/types'\n\n`
253
+
254
+ typeString += `interface ValidationField {
255
+ rule: VineType
256
+ message: Record<string, string>
257
+ }\n\n`
258
+
259
+ typeString += `interface CustomAttributes {
260
+ [key: string]: ValidationField
261
+ }\n\n`
262
+
263
+ for (let i = 0; i < modelFiles.length; i++) {
264
+ let fieldStringType = ``
265
+ let fieldString = ``
266
+ let fieldStringInt = ``
267
+ let fileString = `import { Request } from '@stacksjs/router'\nimport { validateField } from '@stacksjs/validation'\nimport { customValidate } from '@stacksjs/validation'\n\n`
268
+
269
+ const modeFileElement = modelFiles[i] as string
270
+ const model = (await import(modeFileElement)).default as Model
271
+ const modelName = getModelName(model, modeFileElement)
272
+ const useTimestamps = model?.traits?.useTimestamps ?? model?.traits?.timestampable ?? true
273
+ const useSoftDeletes = model?.traits?.useSoftDeletes ?? model?.traits?.softDeletable ?? false
274
+ const attributes = await extractFields(model, modeFileElement)
275
+
276
+ fieldString += ` id: number\n`
277
+ fieldStringInt += `public id = 1\n`
278
+
279
+ fieldStringType += ` get(key: 'id'): number\n`
280
+
281
+ const entityGroups: Record<string, string[]> = {}
282
+
283
+ // Group attributes by their entity type
284
+ for (const attribute of attributes) {
285
+ const entity = attribute.fieldArray?.entity === 'enum' ? 'string[]' : attribute.fieldArray?.entity
286
+ let defaultValue: any = `''`
287
+
288
+ if (attribute.fieldArray?.entity === 'boolean')
289
+ defaultValue = false
290
+ if (attribute.fieldArray?.entity === 'number')
291
+ defaultValue = 0
292
+
293
+ // Convert the field name to snake_case
294
+ const snakeField = snakeCase(attribute.field)
295
+
296
+ if (typeof entity === 'string') {
297
+ if (entityGroups[entity]) {
298
+ entityGroups[entity].push(`'${snakeField}'`)
299
+ }
300
+ else {
301
+ entityGroups[entity] = [`'${snakeField}'`]
302
+ }
303
+
304
+ fieldString += ` ${snakeCase(attribute.field)}: ${entity}\n `
305
+ fieldStringInt += `public ${snakeField} = ${defaultValue}\n`
306
+ }
307
+ }
308
+
309
+ // Generate fieldStringType with grouped fields
310
+ for (const [entity, fields] of Object.entries(entityGroups)) {
311
+ const concatenatedFields = fields.join(' | ')
312
+ fieldStringType += ` get(key: ${concatenatedFields}): ${entity}\n`
313
+ }
314
+
315
+ const otherModelRelations = await fetchOtherModelRelations(model, modelName)
316
+ for (const otherModel of otherModelRelations) {
317
+ fieldString += ` ${otherModel.foreignKey}: number\n `
318
+ fieldStringType += ` get(key: '${otherModel.foreignKey}'): string \n`
319
+ fieldStringInt += `public ${otherModel.foreignKey} = 0\n`
320
+ }
321
+
322
+ if (useTimestamps) {
323
+ fieldStringInt += `public created_at = new Date
324
+ public updated_at = new Date
325
+ `
326
+ }
327
+
328
+ if (useSoftDeletes) {
329
+ fieldStringInt += `
330
+ public deleted_at = ''
331
+ `
332
+ }
333
+
334
+ fieldString += `created_at?: Date
335
+ updated_at?: Date
336
+ deleted_at?: Date`
337
+
338
+ const requestFile = Bun.file(path.frameworkPath(`requests/${modelName}Request.ts`))
339
+
340
+ importTypes = `${modelName}RequestType`
341
+ importTypesString += `${importTypes}`
342
+
343
+ if (i < modelFiles.length - 1)
344
+ importTypesString += ` | `
345
+
346
+ fileString += `import type { ${importTypes} } from '../types/requests'\n\n`
347
+ fileString += `interface ValidationField {
348
+ rule: ReturnType<typeof schema.string>
349
+ message: Record<string, string>
350
+ }\n\n`
351
+ fileString += `interface CustomAttributes {
352
+ [key: string]: ValidationField
353
+ }\n`
354
+
355
+ const types = `export interface ${modelName}RequestType extends Request {
356
+ validate(attributes?: CustomAttributes): void
357
+ ${fieldStringType}
358
+ all(): RequestData${modelName}
359
+ ${fieldString}
360
+ }\n\n`
361
+
362
+ typeString += `interface RequestData${modelName} {
363
+ ${fieldString}
364
+ }\n`
365
+
366
+ fileString += `interface RequestData${modelName} {
367
+ ${fieldString}
368
+ }\n`
369
+
370
+ typeString += types
371
+
372
+ fileString += `export class ${modelName}Request extends Request<RequestData${modelName}> implements ${modelName}RequestType {
373
+ ${fieldStringInt}
374
+ public async validate(attributes?: CustomAttributes): Promise<void> {
375
+ if (attributes === undefined || attributes === null) {
376
+ await validateField('${modelName}', this.all())
377
+ } else {
378
+ await customValidate(attributes, this.all())
379
+ }
380
+
381
+ }
382
+ }
383
+
384
+ export const request = new ${modelName}Request()
385
+ `
386
+
387
+ const writer = requestFile.writer()
388
+
389
+ writer.write(fileString)
390
+ }
391
+
392
+ typeString += `export type ModelRequest = ${importTypesString}`
393
+
394
+ const requestWrite = requestD.writer()
395
+
396
+ requestWrite.write(typeString)
397
+ }
398
+
399
+ export async function writeOrmActions(apiRoute: string, modelName: string, actionPath?: string): Promise<void> {
400
+ const formattedApiRoute = apiRoute.charAt(0).toUpperCase() + apiRoute.slice(1)
401
+
402
+ let method = 'GET'
403
+ let actionString = `import { Action } from '@stacksjs/actions'\n`
404
+ actionString += `import ${modelName} from '../../orm/src/models/${modelName}'\n`
405
+ let handleString = ``
406
+ actionString += ` import type { ${modelName}RequestType } from '../../types/requests'\n\n`
407
+
408
+ if (apiRoute === 'index') {
409
+ handleString += `async handle(request: ${modelName}RequestType) {
410
+ return await ${modelName}.all()
411
+ },`
412
+
413
+ method = 'GET'
414
+ }
415
+
416
+ if (apiRoute === 'show') {
417
+ handleString += `async handle(request: ${modelName}RequestType) {
418
+ const id = await request.getParam('id')
419
+
420
+ return await ${modelName}.findOrFail(Number(id))
421
+ },`
422
+
423
+ method = 'GET'
424
+ }
425
+
426
+ if (apiRoute === 'destroy') {
427
+ handleString += `async handle(request: ${modelName}RequestType) {
428
+ const id = request.getParam('id')
429
+
430
+ const model = await ${modelName}.findOrFail(Number(id))
431
+
432
+ model.delete()
433
+
434
+ return 'Model deleted!'
435
+ },`
436
+
437
+ method = 'DELETE'
438
+ }
439
+
440
+ if (apiRoute === 'store') {
441
+ handleString += `async handle(request: ${modelName}RequestType) {
442
+ await request.validate()
443
+ const model = await ${modelName}.create(request.all())
444
+
445
+ return model
446
+ },`
447
+
448
+ method = 'POST'
449
+ }
450
+
451
+ if (apiRoute === 'update') {
452
+ handleString += `async handle(request: ${modelName}RequestType) {
453
+ await request.validate()
454
+
455
+ const id = request.getParam('id')
456
+ const model = await ${modelName}.findOrFail(Number(id))
457
+
458
+ return model.update(request.all())
459
+ },`
460
+
461
+ method = 'PATCH'
462
+ }
463
+
464
+ actionString += `export default new Action({
465
+ name: '${modelName} ${formattedApiRoute}',
466
+ description: '${modelName} ${formattedApiRoute} ORM Action',
467
+ method: '${method}',
468
+ ${handleString}
469
+ })
470
+ `
471
+
472
+ const actionName = actionPath || `${modelName}${formattedApiRoute}OrmAction.ts`
473
+
474
+ const actionFile = path.builtUserActionsPath(`src/${actionName}`)
475
+
476
+ if (fs.existsSync(actionFile))
477
+ return
478
+
479
+ const file = Bun.file(actionFile)
480
+
481
+ const writer = file.writer()
482
+
483
+ writer.write(actionString)
484
+ }
485
+
486
+ export async function extractFields(model: Model, modelFile: string): Promise<ModelElement[]> {
487
+ // TODO: we can improve this type
488
+ let fields: Record<string, any> | undefined = model.attributes
489
+
490
+ if (!fields)
491
+ fields = {}
492
+
493
+ const fieldKeys = Object.keys(fields)
494
+ const rules: string[] = []
495
+ const file = Bun.file(modelFile)
496
+ const code = await file.text()
497
+ const regex = /rule:.*$/gm
498
+ let match: RegExpExecArray | null
499
+ match = regex.exec(code)
500
+
501
+ while (match !== null) {
502
+ rules.push(match[0])
503
+ match = regex.exec(code)
504
+ }
505
+
506
+ const input: ModelElement[] = fieldKeys.map((field, index) => {
507
+ const fieldExist = fields[field]
508
+ let defaultValue = null
509
+ let uniqueValue = false
510
+
511
+ if (fieldExist) {
512
+ defaultValue = fieldExist || null
513
+ uniqueValue = fieldExist.unique || false
514
+ }
515
+
516
+ return {
517
+ field,
518
+ default: defaultValue,
519
+ unique: uniqueValue,
520
+ fieldArray: parseRule(rules[index] ?? ''),
521
+ }
522
+ })
523
+
524
+ return input
525
+ }
526
+
527
+ function parseRule(rule: string): FieldArrayElement | null {
528
+ const parts = rule.split('rule: schema.')
529
+
530
+ if (parts.length !== 2)
531
+ return null
532
+ if (!parts[1])
533
+ parts[1] = ''
534
+
535
+ const extractedString = parts[1].replace(/,/g, '')
536
+
537
+ if (!extractedString)
538
+ return null
539
+
540
+ const extractedParts = extractedString.split('.')
541
+ const regex = /\(([^)]+)\)/
542
+
543
+ return (
544
+ extractedParts.map((input) => {
545
+ const match = regex.exec(input)
546
+ const value = match ? match[1] : null
547
+ const field = input.replace(regex, '').replace(/\(|\)/g, '')
548
+ return { entity: field, charValue: value }
549
+ })[0] || null
550
+ )
551
+ }
552
+
553
+ export async function generateApiRoutes(modelFiles: string[]): Promise<void> {
554
+ const file = Bun.file(path.frameworkPath(`orm/routes.ts`))
555
+ const writer = file.writer()
556
+ let routeString = `import { route } from '@stacksjs/router'\n\n\n`
557
+
558
+ for (const modelFile of modelFiles) {
559
+ log.info(`Generating API Routes for: ${italic(modelFile)}`)
560
+ let middlewareString = ''
561
+ const model = (await import(modelFile)).default as Model
562
+ const modelName = getModelName(model, modelFile)
563
+ const tableName = getTableName(model, modelFile)
564
+
565
+ if (model.traits?.useApi) {
566
+ if (model.traits?.useApi && typeof model.traits.useApi === 'object') {
567
+ const middlewares = model.traits.useApi?.middleware
568
+ const uri = model.traits.useApi?.uri || tableName
569
+
570
+ if (middlewares) {
571
+ middlewareString = `.middleware([`
572
+ if (middlewares.length) {
573
+ for (let i = 0; i < middlewares.length; i++) {
574
+ middlewareString += `'${middlewares[i]}'`
575
+
576
+ if (i < middlewares.length - 1) {
577
+ middlewareString += ','
578
+ }
579
+ }
580
+ }
581
+
582
+ middlewareString += `])`
583
+ }
584
+
585
+ if (model.traits.useApi.routes && Object.keys(model.traits.useApi.routes).length > 0) {
586
+ const apiRoutes = model.traits.useApi.routes
587
+
588
+ if (Array.isArray(apiRoutes)) {
589
+ if (apiRoutes.length) {
590
+ for (const apiRoute of apiRoutes) {
591
+ if (typeof apiRoute === 'string') {
592
+ await writeOrmActions(apiRoute, modelName)
593
+
594
+ const formattedApiRoute = apiRoute.charAt(0).toUpperCase() + apiRoute.slice(1)
595
+
596
+ if (apiRoute === 'index')
597
+ routeString += `route.get('${uri}', '${modelName}${formattedApiRoute}OrmAction').middleware(['Api'])\n\n`
598
+ if (apiRoute === 'show')
599
+ routeString += `route.get('${uri}/{id}', '${modelName}${formattedApiRoute}OrmAction').middleware(['Api'])\n\n`
600
+ if (apiRoute === 'store')
601
+ routeString += `route.post('${uri}', '${modelName}${formattedApiRoute}OrmAction').middleware(['Api'])\n\n`
602
+ if (apiRoute === 'update')
603
+ routeString += `route.patch('${uri}/{id}', '${modelName}${formattedApiRoute}OrmAction').middleware(['Api'])\n\n`
604
+ if (apiRoute === 'destroy')
605
+ routeString += `route.delete('${uri}/{id}', '${modelName}${formattedApiRoute}OrmAction').middleware(['Api'])\n\n`
606
+ }
607
+ }
608
+ }
609
+ }
610
+ else {
611
+ if (typeof apiRoutes === 'object') {
612
+ for (const apiRoute in apiRoutes) {
613
+ if (Object.prototype.hasOwnProperty.call(apiRoutes, apiRoute)) {
614
+ const routePath = apiRoutes[apiRoute as keyof typeof apiRoutes]
615
+ await writeOrmActions(apiRoute, modelName, routePath)
616
+ if (typeof routePath !== 'string') {
617
+ throw new TypeError(`Invalid route path for ${apiRoute}`)
618
+ }
619
+ const pathAction = `${routePath}.ts`
620
+ if (apiRoute === 'index')
621
+ routeString += `route.get('${uri}', '${pathAction}').${middlewareString}\n\n`
622
+ if (apiRoute === 'show')
623
+ routeString += `route.get('${uri}/{id}', '${pathAction}').${middlewareString}\n\n`
624
+ if (apiRoute === 'store')
625
+ routeString += `route.post('${uri}', '${pathAction}').${middlewareString}\n\n`
626
+ if (apiRoute === 'update')
627
+ routeString += `route.patch('${uri}/{id}', '${pathAction}').${middlewareString}\n\n`
628
+ if (apiRoute === 'destroy')
629
+ routeString += `route.delete('${uri}/{id}', '${pathAction}').${middlewareString}\n\n`
630
+ }
631
+ }
632
+ }
633
+ }
634
+ }
635
+ }
636
+
637
+ if (typeof model.traits.useApi === 'boolean' && model.traits?.useApi) {
638
+ const uri = tableName
639
+
640
+ const apiRoutes = ['index', 'show', 'store', 'update', 'destroy']
641
+
642
+ for (const apiRoute of apiRoutes) {
643
+ await writeOrmActions(apiRoute as string, modelName)
644
+
645
+ const formattedApiRoute = apiRoute.charAt(0).toUpperCase() + apiRoute.slice(1)
646
+
647
+ const pathAction = path.builtUserActionsPath(`src/${modelName}${formattedApiRoute}OrmAction.ts`, {
648
+ relative: true,
649
+ })
650
+
651
+ if (apiRoute === 'index')
652
+ routeString += `route.get('${uri}', '${pathAction}').middleware(['Api'])\n\n`
653
+ if (apiRoute === 'show')
654
+ routeString += `route.get('${uri}/{id}', '${pathAction}').middleware(['Api'])\n\n`
655
+ if (apiRoute === 'store')
656
+ routeString += `route.post('${uri}', '${pathAction}').middleware(['Api'])\n\n`
657
+ if (apiRoute === 'update')
658
+ routeString += `route.patch('${uri}/{id}', '${pathAction}').middleware(['Api'])\n\n`
659
+ if (apiRoute === 'destroy')
660
+ routeString += `route.delete('${uri}/{id}', '${pathAction}').middleware(['Api'])\n\n`
661
+ }
662
+ }
663
+ }
664
+ }
665
+
666
+ writer.write(routeString)
667
+ await writer.end()
668
+ }
669
+
670
+ export async function deleteExistingModels(modelStringFile?: string): Promise<void> {
671
+ const typePath = path.frameworkPath(`orm/src/types.ts`)
672
+ if (fs.existsSync(typePath))
673
+ await fs.promises.unlink(typePath)
674
+
675
+ if (modelStringFile) {
676
+ const modelPath = path.frameworkPath(`orm/src/models/${modelStringFile}.ts`)
677
+ if (fs.existsSync(modelPath))
678
+ await fs.promises.unlink(modelPath)
679
+
680
+ return
681
+ }
682
+
683
+ const modelPaths = globSync([path.frameworkPath(`orm/src/models/*.ts`)], { absolute: true })
684
+ await Promise.all(
685
+ modelPaths.map(async (modelPath) => {
686
+ if (fs.existsSync(modelPath)) {
687
+ log.info(`Deleting Model: ${italic(modelPath)}`)
688
+ await fs.promises.unlink(modelPath)
689
+ log.success(`Deleted Model: ${italic(modelPath)}`)
690
+ }
691
+ }),
692
+ )
693
+ }
694
+
695
+ export async function deleteExistingOrmActions(modelStringFile?: string): Promise<void> {
696
+ if (modelStringFile) {
697
+ const ormPath = path.builtUserActionsPath(`src/${modelStringFile}.ts`)
698
+ if (fs.existsSync(ormPath))
699
+ await fs.promises.unlink(ormPath)
700
+
701
+ return
702
+ }
703
+
704
+ const ormPaths = globSync([path.builtUserActionsPath('**/*.ts')], { absolute: true })
705
+
706
+ for (const ormPath of ormPaths) {
707
+ if (fs.existsSync(ormPath))
708
+ await fs.promises.unlink(ormPath)
709
+ }
710
+ }
711
+
712
+ export async function deleteExistingModelNameTypes(): Promise<void> {
713
+ const typeFile = path.corePath('types/src/model-names.ts')
714
+ if (fs.existsSync(typeFile))
715
+ await fs.promises.unlink(typeFile)
716
+ }
717
+
718
+ export async function deleteExistingModelRequest(modelStringFile?: string): Promise<void> {
719
+ const requestD = path.frameworkPath('types/requests.d.ts')
720
+ if (fs.existsSync(requestD))
721
+ await fs.promises.unlink(requestD)
722
+
723
+ if (modelStringFile) {
724
+ const requestFile = path.frameworkPath(`requests/${modelStringFile}.ts`)
725
+ if (fs.existsSync(requestFile))
726
+ await fs.promises.unlink(requestFile)
727
+
728
+ return
729
+ }
730
+
731
+ const requestFiles = globSync([path.frameworkPath('requests/*.ts')], { absolute: true })
732
+ for (const requestFile of requestFiles) {
733
+ if (fs.existsSync(requestFile))
734
+ await fs.promises.unlink(requestFile)
735
+ }
736
+ }
737
+
738
+ export async function deleteExistingOrmRoute(): Promise<void> {
739
+ const ormRoute = path.frameworkPath('orm/routes.ts')
740
+ if (fs.existsSync(ormRoute))
741
+ await fs.promises.unlink(ormRoute)
742
+ }
743
+
744
+ export async function generateKyselyTypes(): Promise<void> {
745
+ const modelFiles = globSync([path.userModelsPath('*.ts')], { absolute: true })
746
+ let text = ``
747
+
748
+ for (const modelFile of modelFiles) {
749
+ const model = (await import(modelFile)).default as Model
750
+ const tableName = getTableName(model, modelFile)
751
+ const modelName = getModelName(model, modelFile)
752
+ const words = tableName.split('_')
753
+ const pivotFormatted = `${words.map(word => word.charAt(0).toUpperCase() + word.slice(1)).join('')}`
754
+
755
+ text += `import type { ${pivotFormatted}Table } from '../src/models/${modelName}'\n`
756
+ }
757
+
758
+ text += `import type { Generated } from 'kysely'\n\n`
759
+
760
+ let pivotFormatted = ''
761
+ for (const modelFile of modelFiles) {
762
+ const model = (await import(modelFile)).default as Model
763
+ const modelName = getModelName(model, modelFile)
764
+ const pivotTables = await getPivotTables(model, modelName)
765
+
766
+ for (const pivotTable of pivotTables) {
767
+ const words = pivotTable.table.split('_')
768
+
769
+ pivotFormatted = `${words.map(word => word.charAt(0).toUpperCase() + word.slice(1)).join('')}Table`
770
+
771
+ text += `export interface ${pivotFormatted} {
772
+ id?: Generated<number>
773
+ ${pivotTable.firstForeignKey}: number
774
+ ${pivotTable.secondForeignKey}: number
775
+ }\n\n`
776
+ }
777
+ }
778
+
779
+ text += '\nexport interface MigrationsTable {\n'
780
+ text += 'name: string\n timestamp: string \n }'
781
+
782
+ text += '\nexport interface PasskeysTable {\n'
783
+ text += ' id: string\n'
784
+ text += ' cred_public_key: string\n'
785
+ text += ' user_id: number;\n'
786
+ text += ' webauthn_user_id: string\n'
787
+ text += ' counter: number\n'
788
+ text += ' credential_type: string\n'
789
+ text += ' device_type: string\n'
790
+ text += ' backup_eligible: boolean\n'
791
+ text += ' backup_status: boolean\n'
792
+ text += ' transports?: string\n'
793
+ text += ' created_at?: Date\n'
794
+ text += ' last_used_at: string \n'
795
+ text += '}\n\n'
796
+
797
+ text += '\nexport interface Database {\n'
798
+
799
+ for (const modelFile of modelFiles) {
800
+ const model = (await import(modelFile)).default as Model
801
+ const modelName = getModelName(model, modelFile)
802
+ const tableName = getTableName(model, modelFile)
803
+ const pivotTables = await getPivotTables(model, modelName)
804
+
805
+ for (const pivotTable of pivotTables) text += ` ${pivotTable.table}: ${pivotFormatted}\n`
806
+
807
+ const words = tableName.split('_')
808
+ const formattedTableName = `${words.map(word => word.charAt(0).toUpperCase() + word.slice(1)).join('')}Table`
809
+
810
+ text += ` ${tableName}: ${formattedTableName}\n`
811
+ }
812
+
813
+ text += 'passkeys: PasskeysTable\n'
814
+ text += 'migrations: MigrationsTable'
815
+
816
+ text += '}'
817
+
818
+ const file = Bun.file(path.frameworkPath('orm/src/types.ts'))
819
+ const writer = file.writer()
820
+
821
+ writer.write(text)
822
+
823
+ await writer.end()
824
+ }
825
+
826
+ export async function generateModelString(
827
+ tableName: string,
828
+ modelName: string,
829
+ model: Model,
830
+ attributes: ModelElement[],
831
+ ): Promise<string> {
832
+ const formattedTableName = pascalCase(tableName) // users -> Users
833
+ const formattedModelName = modelName.toLowerCase() // User -> user
834
+
835
+ let fieldString = ''
836
+ let constructorFields = ''
837
+ let jsonFields = '{\n'
838
+ let declareFields = ''
839
+ let whereStatements = ''
840
+ let whereFunctionStatements = ''
841
+ let relationMethods = ``
842
+ let relationImports = ``
843
+ let twoFactorStatements = ''
844
+ let mittCreateStatement = ``
845
+ let mittUpdateStatement = ``
846
+ let mittDeleteStatement = ``
847
+ let mittDeleteFindStatement = ``
848
+
849
+ const relations = await getRelations(model, modelName)
850
+
851
+ for (const relationInstance of relations) {
852
+ relationImports += `import ${relationInstance.model} from './${relationInstance.model}'\n\n`
853
+ }
854
+
855
+ const useTimestamps = model?.traits?.useTimestamps ?? model?.traits?.timestampable ?? true
856
+ const useSoftDeletes = model?.traits?.useSoftDeletes ?? model?.traits?.softDeletable ?? false
857
+ const observer = model?.traits?.observe
858
+
859
+ if (typeof observer === 'boolean') {
860
+ if (observer) {
861
+ mittCreateStatement += `if (model)\n dispatch('${formattedModelName}:created', model)`
862
+ mittUpdateStatement += `if (model)\n dispatch('${formattedModelName}:updated', model)`
863
+ mittDeleteStatement += `if (model)\n dispatch('${formattedModelName}:deleted', model)`
864
+
865
+ mittDeleteFindStatement += 'const model = await instance.find(id)'
866
+ }
867
+ }
868
+
869
+ if (Array.isArray(observer)) {
870
+ // Iterate through the array and append statements based on its contents
871
+ if (observer.includes('create')) {
872
+ mittCreateStatement += `if (model)\n dispatch('${formattedModelName}:created', model);`
873
+ }
874
+ if (observer.includes('update')) {
875
+ mittUpdateStatement += `if (model)\n dispatch('${formattedModelName}:updated', model);`
876
+ }
877
+ if (observer.includes('delete')) {
878
+ mittDeleteFindStatement += 'const model = await instance.find(id)'
879
+ mittDeleteStatement += `if (model)\n dispatch('${formattedModelName}:deleted', model);`
880
+ }
881
+ }
882
+
883
+ for (const relation of relations) {
884
+ const modelRelation = relation.model
885
+ const foreignKeyRelation = relation.foreignKey
886
+ const tableRelation = relation.table || ''
887
+ const pivotTableRelation = relation.pivotTable
888
+ const formattedModelRelation = modelRelation.toLowerCase()
889
+ const relationType = getRelationType(relation.relationship)
890
+ const relationCount = getRelationCount(relation.relationship)
891
+
892
+ if (relationType === 'throughType') {
893
+ const relationName = relation.relationName || formattedModelName + modelRelation
894
+ const throughRelation = relation.throughModel
895
+
896
+ if (relation.throughModel === undefined)
897
+ continue
898
+
899
+ const formattedThroughRelation = relation?.throughModel?.toLowerCase()
900
+ const throughTableRelation = throughRelation
901
+ const foreignKeyThroughRelation = relation.throughForeignKey || `${formattedThroughRelation}_id`
902
+
903
+ relationMethods += `
904
+ async ${relationName}() {
905
+ if (this.id === undefined)
906
+ throw new Error('Relation Error!')
907
+
908
+ const firstModel = await db.selectFrom('${throughTableRelation}')
909
+ .where('${foreignKeyRelation}', '=', this.id)
910
+ .selectAll()
911
+ .executeTakeFirst()
912
+
913
+ if (! firstModel)
914
+ throw new Error('Model Relation Not Found!')
915
+
916
+ const finalModel = ${modelRelation}
917
+ .where('${foreignKeyThroughRelation}', '=', firstModel.id)
918
+ .first()
919
+
920
+ return new ${modelRelation}.modelInstance(finalModel)
921
+ }\n\n`
922
+ }
923
+
924
+ if (relationType === 'hasType' && relationCount === 'many') {
925
+ const relationName = relation.relationName || tableRelation
926
+
927
+ relationMethods += `
928
+ async ${relationName}() {
929
+ if (this.id === undefined)
930
+ throw new Error('Relation Error!')
931
+
932
+ const results = await db.selectFrom('${tableRelation}')
933
+ .where('${foreignKeyRelation}', '=', this.id)
934
+ .selectAll()
935
+ .execute()
936
+
937
+ return results
938
+ }\n\n`
939
+ }
940
+
941
+ if (relationType === 'hasType' && relationCount === 'one') {
942
+ const relationName = relation.relationName || formattedModelRelation
943
+ relationMethods += `
944
+ async ${relationName}() {
945
+ if (this.id === undefined)
946
+ throw new Error('Relation Error!')
947
+
948
+ const model = ${modelRelation}
949
+ .where('${foreignKeyRelation}', '=', this.id).first()
950
+
951
+ if (! model)
952
+ throw new Error('Model Relation Not Found!')
953
+
954
+ return model
955
+ }\n\n`
956
+ }
957
+
958
+ if (relationType === 'belongsType' && !relationCount) {
959
+ const relationName = relation.relationName || formattedModelRelation
960
+
961
+ relationMethods += `
962
+ async ${relationName}() {
963
+ if (this.${foreignKeyRelation} === undefined)
964
+ throw new Error('Relation Error!')
965
+
966
+ const model = await ${modelRelation}
967
+ .where('id', '=', ${foreignKeyRelation})
968
+ .first()
969
+
970
+ if (! model)
971
+ throw new Error('Model Relation Not Found!')
972
+
973
+ return model
974
+ }\n\n`
975
+ }
976
+
977
+ if (relationType === 'belongsType' && relationCount === 'many') {
978
+ const pivotTable = pivotTableRelation || tableRelation
979
+ const relationName = relation.relationName || formattedModelName + plural(pascalCase(modelRelation))
980
+
981
+ relationMethods += `
982
+ async ${relationName}() {
983
+ if (this.id === undefined)
984
+ throw new Error('Relation Error!')
985
+
986
+ const results = await db.selectFrom('${pivotTable}')
987
+ .where('${foreignKeyRelation}', '=', this.id)
988
+ .selectAll()
989
+ .execute()
990
+
991
+ const tableRelationIds = results.map(result => result.${singular(tableRelation)}_id)
992
+
993
+ if (! tableRelationIds.length)
994
+ throw new Error('Relation Error!')
995
+
996
+ const relationResults = await ${modelRelation}.whereIn('id', tableRelationIds).get()
997
+
998
+ return relationResults
999
+ }\n\n`
1000
+ }
1001
+ }
1002
+
1003
+ declareFields += `public id: number | undefined \n `
1004
+
1005
+ constructorFields += `this.id = ${formattedModelName}?.id\n `
1006
+
1007
+ const useTwoFactor = typeof model.traits?.useAuth === 'object' && model.traits.useAuth.useTwoFactor
1008
+ const usePasskey = typeof model.traits?.useAuth === 'object' && model.traits.useAuth.usePasskey
1009
+
1010
+ if (useTwoFactor) {
1011
+ declareFields += `public two_factor_secret: string | undefined \n`
1012
+ constructorFields += `this.two_factor_secret = ${formattedModelName}?.two_factor_secret\n `
1013
+
1014
+ twoFactorStatements += `
1015
+ async generateTwoFactorForModel() {
1016
+ const secret = generateTwoFactorSecret()
1017
+
1018
+ await this.update({ 'two_factor_secret': secret })
1019
+ }
1020
+
1021
+ verifyTwoFactorCode(code: string): boolean {
1022
+ const modelTwoFactorSecret = this.two_factor_secret
1023
+ let isValid = false
1024
+
1025
+ if (typeof modelTwoFactorSecret === 'string') {
1026
+ isValid = verifyTwoFactorCode(code, modelTwoFactorSecret)
1027
+ }
1028
+
1029
+ return isValid
1030
+ }
1031
+ `
1032
+ }
1033
+
1034
+ if (usePasskey) {
1035
+ declareFields += 'public public_passkey: string | undefined \n'
1036
+ constructorFields += `this.public_passkey = ${formattedModelName}?.public_passkey\n `
1037
+
1038
+ twoFactorStatements += `
1039
+ async generateTwoFactorForModel() {
1040
+ const secret = generateTwoFactorSecret()
1041
+
1042
+ await this.update({ 'two_factor_secret': secret })
1043
+ }
1044
+
1045
+ verifyTwoFactorCode(code: string): boolean {
1046
+ const modelTwoFactorSecret = this.two_factor_secret
1047
+ let isValid = false
1048
+
1049
+ if (typeof modelTwoFactorSecret === 'string') {
1050
+ isValid = verifyTwoFactorCode(code, modelTwoFactorSecret)
1051
+ }
1052
+
1053
+ return isValid
1054
+ }
1055
+ `
1056
+ }
1057
+
1058
+ jsonFields += '\nid: this.id,\n'
1059
+ for (const attribute of attributes) {
1060
+ const entity = attribute.fieldArray?.entity === 'enum' ? 'string[]' : attribute.fieldArray?.entity
1061
+
1062
+ fieldString += ` ${snakeCase(attribute.field)}?: ${entity}\n `
1063
+ declareFields += `public ${snakeCase(attribute.field)}: ${entity} | undefined \n `
1064
+ constructorFields += `this.${snakeCase(attribute.field)} = ${formattedModelName}?.${snakeCase(attribute.field)}\n `
1065
+ jsonFields += `${snakeCase(attribute.field)}: this.${snakeCase(attribute.field)},\n `
1066
+
1067
+ whereStatements += `static where${pascalCase(attribute.field)}(value: string): ${modelName}Model {
1068
+ const instance = new ${modelName}Model(null)
1069
+
1070
+ instance.query = instance.query.where('${attribute.field}', '=', value)
1071
+
1072
+ return instance
1073
+ } \n\n`
1074
+
1075
+ whereFunctionStatements += `export async function where${pascalCase(attribute.field)}(value: ${entity}): Promise<${modelName}Model[]> {
1076
+ const query = db.selectFrom('${tableName}').where('${snakeCase(attribute.field)}', '=', value)
1077
+ const results = await query.execute()
1078
+
1079
+ return results.map(modelItem => new ${modelName}Model(modelItem))
1080
+ } \n\n`
1081
+ }
1082
+
1083
+ if (useTimestamps) {
1084
+ declareFields += `
1085
+ public created_at: Date | undefined
1086
+ public updated_at: Date | undefined
1087
+ `
1088
+
1089
+ constructorFields += `
1090
+ this.created_at = ${formattedModelName}?.created_at\n
1091
+ this.updated_at = ${formattedModelName}?.updated_at\n
1092
+ `
1093
+
1094
+ jsonFields += `
1095
+ created_at: this.created_at,\n
1096
+ updated_at: this.updated_at,\n
1097
+ `
1098
+ }
1099
+
1100
+ if (useSoftDeletes) {
1101
+ declareFields += `
1102
+ public deleted_at: string | undefined
1103
+ `
1104
+
1105
+ constructorFields += `
1106
+ this.deleted_at = ${formattedModelName}?.deleted_at\n
1107
+ `
1108
+
1109
+ jsonFields += `
1110
+ deleted_at: this.deleted_at,\n
1111
+ `
1112
+ }
1113
+
1114
+ jsonFields += '}'
1115
+
1116
+ const otherModelRelations = await fetchOtherModelRelations(model, modelName)
1117
+
1118
+ for (const otherModelRelation of otherModelRelations) {
1119
+ fieldString += ` ${otherModelRelation.foreignKey}?: number \n`
1120
+ declareFields += `public ${otherModelRelation.foreignKey}: number | undefined \n `
1121
+ constructorFields += `this.${otherModelRelation.foreignKey} = ${formattedModelName}?.${otherModelRelation.foreignKey}\n `
1122
+ }
1123
+
1124
+ if (useTwoFactor)
1125
+ fieldString += 'two_factor_secret?: string \n'
1126
+ if (usePasskey)
1127
+ fieldString += 'public_passkey?: string \n'
1128
+
1129
+ if (useTimestamps) {
1130
+ fieldString += `
1131
+ created_at?: Date\n
1132
+ updated_at?: Date
1133
+ `
1134
+ }
1135
+
1136
+ fieldString += `
1137
+ deleted_at?: Date
1138
+ `
1139
+
1140
+ const hidden = JSON.stringify(getHiddenAttributes(model.attributes))
1141
+ const fillable = JSON.stringify(getFillableAttributes(model.attributes))
1142
+
1143
+ return `import type { Generated, Insertable, Selectable, Updateable } from 'kysely'
1144
+ import { db } from '@stacksjs/database'
1145
+ import { sql } from '@stacksjs/database'
1146
+ import { dispatch } from '@stacksjs/events'
1147
+ import { generateTwoFactorSecret } from '@stacksjs/auth'
1148
+ import { verifyTwoFactorCode } from '@stacksjs/auth'
1149
+ import { cache } from '@stacksjs/cache'
1150
+ ${relationImports}
1151
+ // import { Kysely, MysqlDialect, PostgresDialect } from 'kysely'
1152
+ // import { Pool } from 'pg'
1153
+
1154
+ // TODO: we need an action that auto-generates these table interfaces
1155
+ export interface ${formattedTableName}Table {
1156
+ id: Generated<number>
1157
+ ${fieldString}
1158
+ }
1159
+
1160
+ interface ${modelName}Response {
1161
+ data: ${formattedTableName}
1162
+ paging: {
1163
+ total_records: number
1164
+ page: number
1165
+ total_pages: number
1166
+ }
1167
+ next_cursor: number | null
1168
+ }
1169
+
1170
+ export type ${modelName}Type = Selectable<${formattedTableName}Table>
1171
+ export type New${modelName} = Insertable<${formattedTableName}Table>
1172
+ export type ${modelName}Update = Updateable<${formattedTableName}Table>
1173
+ export type ${formattedTableName} = ${modelName}Type[]
1174
+
1175
+ export type ${modelName}Column = ${formattedTableName}
1176
+ export type ${modelName}Columns = Array<keyof ${formattedTableName}>
1177
+
1178
+ type SortDirection = 'asc' | 'desc'
1179
+ interface SortOptions { column: ${modelName}Type, order: SortDirection }
1180
+ // Define a type for the options parameter
1181
+ interface QueryOptions {
1182
+ sort?: SortOptions
1183
+ limit?: number
1184
+ offset?: number
1185
+ page?: number
1186
+ }
1187
+
1188
+ export class ${modelName}Model {
1189
+ private hidden = ${hidden}
1190
+ private fillable = ${fillable}
1191
+ private softDeletes = ${useSoftDeletes}
1192
+ protected query: any
1193
+ protected hasSelect: boolean
1194
+ ${declareFields}
1195
+ constructor(${formattedModelName}: Partial<${modelName}Type> | null) {
1196
+ ${constructorFields}
1197
+
1198
+ this.query = db.selectFrom('${tableName}')
1199
+ this.hasSelect = false
1200
+ }
1201
+
1202
+ // Method to find a ${modelName} by ID
1203
+ async find(id: number): Promise<${modelName}Model | undefined> {
1204
+ let query = db.selectFrom('${tableName}').where('id', '=', id).selectAll()
1205
+
1206
+ const model = await query.executeTakeFirst()
1207
+
1208
+ if (!model)
1209
+ return undefined
1210
+
1211
+ cache.getOrSet(\`${formattedModelName}:\${id}\`, JSON.stringify(model))
1212
+
1213
+ return this.parseResult(new ${modelName}Model(model))
1214
+ }
1215
+
1216
+ // Method to find a ${modelName} by ID
1217
+ static async find(id: number): Promise<${modelName}Model | undefined> {
1218
+ let query = db.selectFrom('${tableName}').where('id', '=', id).selectAll()
1219
+
1220
+ const instance = new ${modelName}Model(null)
1221
+
1222
+ const model = await query.executeTakeFirst()
1223
+
1224
+ if (!model)
1225
+ return undefined
1226
+
1227
+ cache.getOrSet(\`${formattedModelName}:\${id}\`, JSON.stringify(model))
1228
+
1229
+ return instance.parseResult(new ${modelName}Model(model))
1230
+ }
1231
+
1232
+ static async all(): Promise<${modelName}Model[]> {
1233
+ let query = db.selectFrom('${tableName}').selectAll()
1234
+
1235
+ const instance = new ${modelName}Model(null)
1236
+
1237
+ if (instance.softDeletes) {
1238
+ query = query.where('deleted_at', 'is', null)
1239
+ }
1240
+
1241
+ const results = await query.execute();
1242
+
1243
+ return results.map(modelItem => instance.parseResult(new ${modelName}Model(modelItem)));
1244
+ }
1245
+
1246
+
1247
+ static async findOrFail(id: number): Promise<${modelName}Model> {
1248
+ let query = db.selectFrom('${tableName}').where('id', '=', id)
1249
+
1250
+ const instance = new ${modelName}Model(null)
1251
+
1252
+ if (instance.softDeletes) {
1253
+ query = query.where('deleted_at', 'is', null);
1254
+ }
1255
+
1256
+ query = query.selectAll()
1257
+
1258
+ const model = await query.executeTakeFirst()
1259
+
1260
+ if (model === undefined)
1261
+ throw new Error(JSON.stringify({ status: 404, message: 'No model results found for query' }))
1262
+
1263
+ cache.getOrSet(\`${formattedModelName}:\${id}\`, JSON.stringify(model))
1264
+
1265
+ return instance.parseResult(new ${modelName}Model(model))
1266
+ }
1267
+
1268
+ static async findMany(ids: number[]): Promise<${modelName}Model[]> {
1269
+ let query = db.selectFrom('${tableName}').where('id', 'in', ids)
1270
+
1271
+ const instance = new ${modelName}Model(null)
1272
+
1273
+ if (instance.softDeletes) {
1274
+ query = query.where('deleted_at', 'is', null);
1275
+ }
1276
+
1277
+ query = query.selectAll()
1278
+
1279
+ const model = await query.execute()
1280
+
1281
+ return model.map(modelItem => instance.parseResult(new ${modelName}Model(modelItem)))
1282
+ }
1283
+
1284
+ // Method to get a User by criteria
1285
+ static async get(): Promise<UserModel[]> {
1286
+ const instance = new ${modelName}Model(null)
1287
+
1288
+ if (instance.hasSelect) {
1289
+ if (instance.softDeletes) {
1290
+ instance.query = instance.query.where('deleted_at', 'is', null)
1291
+ }
1292
+
1293
+ const model = await instance.query.execute()
1294
+
1295
+ return model.map((modelItem: ${modelName}Model) => new ${modelName}Model(modelItem))
1296
+ }
1297
+
1298
+ if (instance.softDeletes) {
1299
+ instance.query = instance.query.where('deleted_at', 'is', null)
1300
+ }
1301
+
1302
+ const model = await instance.query.selectAll().execute()
1303
+
1304
+ return model.map((modelItem: ${modelName}Model) => new ${modelName}Model(modelItem))
1305
+ }
1306
+
1307
+
1308
+ // Method to get a ${modelName} by criteria
1309
+ async get(): Promise<${modelName}Model[]> {
1310
+ if (this.hasSelect) {
1311
+
1312
+ if (this.softDeletes) {
1313
+ this.query = this.query.where('deleted_at', 'is', null);
1314
+ }
1315
+
1316
+ const model = await this.query.execute()
1317
+
1318
+ return model.map((modelItem: ${modelName}Model) => new ${modelName}Model(modelItem))
1319
+ }
1320
+
1321
+ if (this.softDeletes) {
1322
+ this.query = this.query.where('deleted_at', 'is', null);
1323
+ }
1324
+
1325
+ const model = await this.query.selectAll().execute()
1326
+
1327
+ return model.map((modelItem: ${modelName}Model) => new ${modelName}Model(modelItem))
1328
+ }
1329
+
1330
+ static async count(): Promise<number> {
1331
+ const instance = new ${modelName}Model(null)
1332
+
1333
+ if (instance.softDeletes) {
1334
+ instance.query = instance.query.where('deleted_at', 'is', null);
1335
+ }
1336
+
1337
+ const results = await instance.query.selectAll().execute()
1338
+
1339
+ return results.length
1340
+ }
1341
+
1342
+ async count(): Promise<number> {
1343
+ if (this.hasSelect) {
1344
+
1345
+ if (this.softDeletes) {
1346
+ this.query = this.query.where('deleted_at', 'is', null);
1347
+ }
1348
+
1349
+ const results = await this.query.execute()
1350
+
1351
+ return results.length
1352
+ }
1353
+
1354
+ const results = await this.query.selectAll().execute()
1355
+
1356
+ return results.length
1357
+ }
1358
+
1359
+ // Method to get all ${tableName}
1360
+ static async paginate(options: QueryOptions = { limit: 10, offset: 0, page: 1 }): Promise<${modelName}Response> {
1361
+ const totalRecordsResult = await db.selectFrom('${tableName}')
1362
+ .select(db.fn.count('id').as('total')) // Use 'id' or another actual column name
1363
+ .executeTakeFirst()
1364
+
1365
+ const totalRecords = Number(totalRecordsResult?.total) || 0
1366
+ const totalPages = Math.ceil(totalRecords / (options.limit ?? 10))
1367
+
1368
+ const ${tableName}WithExtra = await db.selectFrom('${tableName}')
1369
+ .selectAll()
1370
+ .orderBy('id', 'asc') // Assuming 'id' is used for cursor-based pagination
1371
+ .limit((options.limit ?? 10) + 1) // Fetch one extra record
1372
+ .offset(((options.page ?? 1) - 1) * (options.limit ?? 10)) // Ensure options.page is not undefined
1373
+ .execute()
1374
+
1375
+
1376
+ let nextCursor = null
1377
+ if (${tableName}WithExtra.length > (options.limit ?? 10)) nextCursor = ${tableName}WithExtra.pop()?.id ?? null
1378
+
1379
+ return {
1380
+ data: ${tableName}WithExtra,
1381
+ paging: {
1382
+ total_records: totalRecords,
1383
+ page: options.page || 1,
1384
+ total_pages: totalPages,
1385
+ },
1386
+ next_cursor: nextCursor,
1387
+ }
1388
+ }
1389
+
1390
+ // Method to create a new ${formattedModelName}
1391
+ static async create(new${modelName}: New${modelName}): Promise<${modelName}Model> {
1392
+ const instance = new ${modelName}Model(null)
1393
+
1394
+ const filteredValues = Object.fromEntries(
1395
+ Object.entries(new${modelName}).filter(([key]) => instance.fillable.includes(key)),
1396
+ ) as New${modelName}
1397
+
1398
+ const result = await db.insertInto('${tableName}')
1399
+ .values(filteredValues)
1400
+ .executeTakeFirstOrThrow()
1401
+
1402
+ const model = await find(Number(result.insertId)) as ${modelName}Model
1403
+
1404
+ ${mittCreateStatement}
1405
+
1406
+ return model
1407
+ }
1408
+
1409
+ static async forceCreate(new${modelName}: New${modelName}): Promise<${modelName}Model> {
1410
+ const result = await db.insertInto('${tableName}')
1411
+ .values(new${modelName})
1412
+ .executeTakeFirstOrThrow()
1413
+
1414
+ const model = await find(Number(result.insertId)) as ${modelName}Model
1415
+
1416
+ ${mittCreateStatement}
1417
+
1418
+ return model
1419
+ }
1420
+
1421
+ // Method to remove a ${modelName}
1422
+ static async remove(id: number): Promise<void> {
1423
+ const instance = new ${modelName}Model(null)
1424
+ ${mittDeleteFindStatement}
1425
+
1426
+ if (instance.softDeletes) {
1427
+ await db.updateTable('${tableName}')
1428
+ .set({
1429
+ deleted_at: sql.raw('CURRENT_TIMESTAMP')
1430
+ })
1431
+ .where('id', '=', id)
1432
+ .execute();
1433
+ } else {
1434
+ await db.deleteFrom('${tableName}')
1435
+ .where('id', '=', id)
1436
+ .execute();
1437
+ }
1438
+
1439
+
1440
+ ${mittDeleteStatement}
1441
+ }
1442
+
1443
+ where(...args: (string | number | boolean | undefined | null)[]): ${modelName}Model {
1444
+ let column: any
1445
+ let operator: any
1446
+ let value: any
1447
+
1448
+ if (args.length === 2) {
1449
+ [column, value] = args
1450
+ operator = '='
1451
+ } else if (args.length === 3) {
1452
+ [column, operator, value] = args
1453
+ } else {
1454
+ throw new Error("Invalid number of arguments")
1455
+ }
1456
+
1457
+ this.query = this.query.where(column, operator, value)
1458
+
1459
+ return this
1460
+ }
1461
+
1462
+ static where(...args: (string | number | boolean | undefined | null)[]): ${modelName}Model {
1463
+ let column: any
1464
+ let operator: any
1465
+ let value: any
1466
+
1467
+ const instance = new ${modelName}Model(null)
1468
+
1469
+ if (args.length === 2) {
1470
+ [column, value] = args
1471
+ operator = '='
1472
+ } else if (args.length === 3) {
1473
+ [column, operator, value] = args
1474
+ } else {
1475
+ throw new Error("Invalid number of arguments")
1476
+ }
1477
+
1478
+ instance.query = instance.query.where(column, operator, value)
1479
+
1480
+ return instance
1481
+ }
1482
+
1483
+ ${whereStatements}
1484
+
1485
+ static whereIn(column: keyof ${modelName}Type, values: any[]): ${modelName}Model {
1486
+ const instance = new ${modelName}Model(null)
1487
+
1488
+ instance.query = instance.query.where(column, 'in', values)
1489
+
1490
+ return instance
1491
+ }
1492
+
1493
+ async first(): Promise<${modelName}Model | undefined> {
1494
+ const model = await this.query.selectAll().executeTakeFirst()
1495
+
1496
+ if (! model) {
1497
+ return undefined
1498
+ }
1499
+
1500
+ return this.parseResult(new ${modelName}Model(model))
1501
+ }
1502
+
1503
+ async firstOrFail(): Promise<${modelName}Model | undefined> {
1504
+ const model = await this.query.selectAll().executeTakeFirst()
1505
+
1506
+ if (model === undefined)
1507
+ throw { status: 404, message: 'No ${modelName}Model results found for query' }
1508
+
1509
+ return this.parseResult(new ${modelName}Model(model))
1510
+ }
1511
+
1512
+ async exists(): Promise<boolean> {
1513
+ const model = await this.query.selectAll().executeTakeFirst()
1514
+
1515
+ return model !== null || model !== undefined
1516
+ }
1517
+
1518
+ static async first(): Promise<${modelName}Type | undefined> {
1519
+ return await db.selectFrom('${tableName}')
1520
+ .selectAll()
1521
+ .executeTakeFirst()
1522
+ }
1523
+
1524
+ async last(): Promise<${modelName}Type | undefined> {
1525
+ return await db.selectFrom('${tableName}')
1526
+ .selectAll()
1527
+ .orderBy('id', 'desc')
1528
+ .executeTakeFirst()
1529
+ }
1530
+
1531
+ static async last(): Promise<${modelName}Type | undefined> {
1532
+ return await db.selectFrom('${tableName}').selectAll().orderBy('id', 'desc').executeTakeFirst()
1533
+ }
1534
+
1535
+ static orderBy(column: keyof ${modelName}Type, order: 'asc' | 'desc'): ${modelName}Model {
1536
+ const instance = new ${modelName}Model(null)
1537
+
1538
+ instance.query = instance.query.orderBy(column, order)
1539
+
1540
+ return instance
1541
+ }
1542
+
1543
+ orderBy(column: keyof ${modelName}Type, order: 'asc' | 'desc'): ${modelName}Model {
1544
+ this.query = this.query.orderBy(column, order)
1545
+
1546
+ return this
1547
+ }
1548
+
1549
+ static orderByDesc(column: keyof ${modelName}Type): ${modelName}Model {
1550
+ const instance = new ${modelName}Model(null)
1551
+
1552
+ instance.query = instance.query.orderBy(column, 'desc')
1553
+
1554
+ return instance
1555
+ }
1556
+
1557
+ orderByDesc(column: keyof ${modelName}Type): ${modelName}Model {
1558
+ this.query = this.orderBy(column, 'desc')
1559
+
1560
+ return this
1561
+ }
1562
+
1563
+ static orderByAsc(column: keyof ${modelName}Type): ${modelName}Model {
1564
+ const instance = new ${modelName}Model(null)
1565
+
1566
+ instance.query = instance.query.orderBy(column, 'asc')
1567
+
1568
+ return instance
1569
+ }
1570
+
1571
+ orderByAsc(column: keyof ${modelName}Type): ${modelName}Model {
1572
+ this.query = this.query.orderBy(column, 'desc')
1573
+
1574
+ return this
1575
+ }
1576
+
1577
+ async update(${formattedModelName}: ${modelName}Update): Promise<${modelName}Model | undefined> {
1578
+ if (this.id === undefined)
1579
+ throw new Error('${modelName} ID is undefined')
1580
+
1581
+ const filteredValues = Object.fromEntries(
1582
+ Object.entries(${formattedModelName}).filter(([key]) => this.fillable.includes(key)),
1583
+ ) as New${modelName}
1584
+
1585
+ await db.updateTable('${tableName}')
1586
+ .set(filteredValues)
1587
+ .where('id', '=', this.id)
1588
+ .executeTakeFirst()
1589
+
1590
+ const model = await this.find(Number(this.id))
1591
+
1592
+
1593
+ ${mittUpdateStatement}
1594
+
1595
+ return model
1596
+ }
1597
+
1598
+ async forceUpdate(${formattedModelName}: ${modelName}Update): Promise<${modelName}Model | undefined> {
1599
+ if (this.id === undefined)
1600
+ throw new Error('${modelName} ID is undefined')
1601
+
1602
+ await db.updateTable('${tableName}')
1603
+ .set(${formattedModelName})
1604
+ .where('id', '=', this.id)
1605
+ .executeTakeFirst()
1606
+
1607
+ const model = await this.find(Number(this.id))
1608
+
1609
+
1610
+ ${mittUpdateStatement}
1611
+
1612
+ return model
1613
+ }
1614
+
1615
+ async save(): Promise<void> {
1616
+ if (!this)
1617
+ throw new Error('${modelName} data is undefined')
1618
+
1619
+ if (this.id === undefined) {
1620
+ await db.insertInto('${tableName}')
1621
+ .values(this as New${modelName})
1622
+ .executeTakeFirstOrThrow()
1623
+ }
1624
+ else {
1625
+ await this.update(this)
1626
+ }
1627
+ }
1628
+
1629
+ // Method to delete (soft delete) the ${formattedModelName} instance
1630
+ async delete(): Promise<void> {
1631
+ if (this.id === undefined)
1632
+ throw new Error('${modelName} ID is undefined')
1633
+
1634
+ ${mittDeleteFindStatement}
1635
+
1636
+ // Check if soft deletes are enabled
1637
+ if (this.softDeletes) {
1638
+ // Update the deleted_at column with the current timestamp
1639
+ await db.updateTable('${tableName}')
1640
+ .set({
1641
+ deleted_at: sql.raw('CURRENT_TIMESTAMP')
1642
+ })
1643
+ .where('id', '=', this.id)
1644
+ .execute();
1645
+ } else {
1646
+ // Perform a hard delete
1647
+ await db.deleteFrom('${tableName}')
1648
+ .where('id', '=', this.id)
1649
+ .execute();
1650
+ }
1651
+
1652
+
1653
+ ${mittDeleteStatement}
1654
+ }
1655
+
1656
+ ${relationMethods}
1657
+
1658
+ distinct(column: keyof ${modelName}Type): ${modelName}Model {
1659
+ this.query = this.query.select(column).distinct()
1660
+
1661
+ this.hasSelect = true
1662
+
1663
+ return this
1664
+ }
1665
+
1666
+ static distinct(column: keyof ${modelName}Type): ${modelName}Model {
1667
+ const instance = new ${modelName}Model(null)
1668
+
1669
+ instance.query = instance.query.select(column).distinct()
1670
+
1671
+ instance.hasSelect = true
1672
+
1673
+ return instance
1674
+ }
1675
+
1676
+ join(table: string, firstCol: string, secondCol: string): ${modelName}Model {
1677
+ this.query = this.query.innerJoin(table, firstCol, secondCol)
1678
+
1679
+ return this
1680
+ }
1681
+
1682
+ static join(table: string, firstCol: string, secondCol: string): ${modelName}Model {
1683
+ const instance = new ${modelName}Model(null)
1684
+
1685
+ instance.query = instance.query.innerJoin(table, firstCol, secondCol)
1686
+
1687
+ return instance
1688
+ }
1689
+
1690
+ static async rawQuery(rawQuery: string): Promise<any> {
1691
+ return await sql\`\${rawQuery}\`\.execute(db)
1692
+ }
1693
+
1694
+ toJSON() {
1695
+ const output: Partial<${modelName}Type> = ${jsonFields}
1696
+
1697
+
1698
+ type ${modelName} = Omit<${modelName}Type, 'password'>
1699
+
1700
+ return output as ${modelName}
1701
+ }
1702
+
1703
+ parseResult(model: ${modelName}Model): ${modelName}Model {
1704
+ for (const hiddenAttribute of this.hidden) {
1705
+ delete model[hiddenAttribute as keyof ${modelName}Model]
1706
+ }
1707
+
1708
+ return model
1709
+ }
1710
+
1711
+ ${twoFactorStatements}
1712
+ }
1713
+
1714
+ async function find(id: number): Promise<${modelName}Model | undefined> {
1715
+ let query = db.selectFrom('${tableName}').where('id', '=', id).selectAll()
1716
+
1717
+ const model = await query.executeTakeFirst()
1718
+
1719
+ if (!model) return undefined
1720
+
1721
+ return new ${modelName}Model(model)
1722
+ }
1723
+
1724
+ export async function count(): Promise<number> {
1725
+ const results = await ${modelName}Model.count()
1726
+
1727
+ return results
1728
+ }
1729
+
1730
+ export async function create(new${modelName}: New${modelName}): Promise<${modelName}Model> {
1731
+
1732
+ const result = await db.insertInto('${tableName}')
1733
+ .values(new${modelName})
1734
+ .executeTakeFirstOrThrow()
1735
+
1736
+ return await find(Number(result.insertId)) as ${modelName}Model
1737
+ }
1738
+
1739
+ export async function rawQuery(rawQuery: string): Promise<any> {
1740
+ return await sql\`\${rawQuery}\`\.execute(db)
1741
+ }
1742
+
1743
+ export async function remove(id: number): Promise<void> {
1744
+ await db.deleteFrom('${tableName}')
1745
+ .where('id', '=', id)
1746
+ .execute()
1747
+ }
1748
+
1749
+ ${whereFunctionStatements}
1750
+
1751
+ export const ${modelName} = ${modelName}Model
1752
+
1753
+ export default ${modelName}
1754
+ `
1755
+ }
1756
+
1757
+ export async function generateModelFiles(modelStringFile?: string): Promise<void> {
1758
+ try {
1759
+ log.info('Cleanup of older Models...')
1760
+ await deleteExistingModels(modelStringFile)
1761
+ log.success('Deleted Models')
1762
+
1763
+ log.info('Deleting old Model Name types...')
1764
+ await deleteExistingModelNameTypes()
1765
+ log.success('Deleted Model Name types')
1766
+
1767
+ log.info('Deleting old Model Requests...')
1768
+ await deleteExistingModelRequest(modelStringFile)
1769
+ log.success('Deleted Model Requests')
1770
+
1771
+ log.info('Deleting old Model Routes...')
1772
+ await deleteExistingOrmRoute()
1773
+ log.success('Deleted Model Routes')
1774
+
1775
+ log.info('Writing Model Names...')
1776
+ try {
1777
+ await writeModelNames()
1778
+ }
1779
+ catch (error) {
1780
+ /* eslint-disable-next-line no-console */
1781
+ console.log('error', error)
1782
+ process.exit(ExitCode.FatalError)
1783
+ }
1784
+ log.success('Wrote Model Names')
1785
+
1786
+ log.info('Writing Model Requests...')
1787
+ try {
1788
+ await writeModelRequest()
1789
+ }
1790
+ catch (error) {
1791
+ /* eslint-disable-next-line no-console */
1792
+ console.log('error', error)
1793
+ process.exit(ExitCode.FatalError)
1794
+ }
1795
+ log.success('Wrote Model Requests')
1796
+
1797
+ log.info('Generating API Routes...')
1798
+ const modelFiles = globSync([path.userModelsPath('**/*.ts')], { absolute: true })
1799
+ await generateApiRoutes(modelFiles)
1800
+ log.success('Generated API Routes')
1801
+
1802
+ for (const modelFile of modelFiles) {
1803
+ if (modelStringFile && modelStringFile !== modelFile)
1804
+ continue
1805
+ log.info(`Processing Model: ${italic(modelFile)}`)
1806
+
1807
+ const model = (await import(modelFile)).default as Model
1808
+ const tableName = getTableName(model, modelFile)
1809
+ const modelName = getModelName(model, modelFile)
1810
+ const file = Bun.file(path.frameworkPath(`orm/src/models/${modelName}.ts`))
1811
+ const fields = await extractFields(model, modelFile)
1812
+ const classString = await generateModelString(tableName, modelName, model, fields)
1813
+
1814
+ const writer = file.writer()
1815
+ log.info(`Writing API Endpoints for: ${italic(modelName)}`)
1816
+ writer.write(classString)
1817
+ log.success(`Wrote API endpoints for: ${italic(modelName)}`)
1818
+ await writer.end()
1819
+ }
1820
+
1821
+ log.info('Generating Query Builder types...')
1822
+ await generateKyselyTypes()
1823
+ log.success('Generated Query Builder types')
1824
+
1825
+ log.info('Ensuring Code Style...')
1826
+ try {
1827
+ // we run this in background in background, because we simply need to lint:fix the auto-generated code
1828
+ // the reason we run it in background is because we don't care whether it fails or not, given there
1829
+ // is a chance that the codebase has lint issues unrelating to our auto-generated code
1830
+ const process = Bun.spawn(['bunx', '--bun', 'eslint', '.', '--fix'], {
1831
+ stdio: ['ignore', 'pipe', 'pipe'],
1832
+ cwd: path.projectPath(),
1833
+ detached: true,
1834
+ })
1835
+
1836
+ const reader = process.stdout.getReader()
1837
+ // let output = ''
1838
+
1839
+ while (true) {
1840
+ const { done } = await reader.read()
1841
+ if (done)
1842
+ break
1843
+ // output += new TextDecoder().decode(value)
1844
+ }
1845
+
1846
+ const stderrReader = process.stderr.getReader()
1847
+ while (true) {
1848
+ const { done } = await stderrReader.read()
1849
+ if (done)
1850
+ break
1851
+ // Collect stderr output but do not log it
1852
+ // output += new TextDecoder().decode(value)
1853
+ }
1854
+
1855
+ const exitCode = await process.exited
1856
+
1857
+ if (exitCode !== 0) {
1858
+ log.debug(
1859
+ 'There was an error fixing your code style but we are ignoring it because we fixed the auto-generated code already.',
1860
+ )
1861
+ }
1862
+ else {
1863
+ log.success('Code style fixed successfully.')
1864
+ }
1865
+ }
1866
+ catch (error) {
1867
+ log.error('There was an error fixing your code style.')
1868
+ log.error(error)
1869
+ process.exit(ExitCode.FatalError)
1870
+ }
1871
+
1872
+ log.success('Linted')
1873
+ }
1874
+ catch (error) {
1875
+ log.error('There was an error generating your model files', error)
1876
+ process.exit(ExitCode.FatalError)
1877
+ }
1878
+ }
1879
+
1880
+ export async function extractAttributesFromModel(filePath: string): Promise<Attributes> {
33
1881
  // Read the TypeScript file
34
1882
  const content = fs.readFileSync(filePath, 'utf8')
35
1883
 
@@ -40,25 +1888,29 @@ export async function extractFieldsFromModel(filePath: string) {
40
1888
  })
41
1889
 
42
1890
  let fields: Attributes | undefined
43
-
44
- // Traverse the AST to find the `fields` object
45
1891
  traverse(ast, {
46
1892
  ObjectExpression(path) {
47
1893
  // Look for the `fields` key in the object
48
- const fieldsProperty = path.node.properties.find((property) => property.key?.name === 'attributes')
1894
+ const fieldsProperty = path.node.properties.find(
1895
+ property =>
1896
+ property.type === 'ObjectProperty'
1897
+ && property.key.type === 'Identifier'
1898
+ && property.key.name === 'attributes',
1899
+ )
49
1900
 
50
- if (fieldsProperty?.value) {
1901
+ if (fieldsProperty && fieldsProperty.type === 'ObjectProperty' && fieldsProperty.value) {
51
1902
  // Convert the AST back to code (stringify)
52
1903
  const generated = generator(fieldsProperty.value, {}, content)
53
- fields = generated.code
1904
+ fields = generated.code as unknown as Attributes
54
1905
  path.stop() // Stop traversing further once we found the fields
55
1906
  }
56
1907
  },
57
1908
  })
58
1909
 
59
- return fields
1910
+ return fields as Attributes
60
1911
  }
61
1912
 
62
- export function userModels() {
63
- return import.meta.glob<{ default: Model }>(path.userModelsPath('*.ts'))
64
- }
1913
+ // TODO: https://github.com/oven-sh/bun/issues/6060
1914
+ // export function userModels() {
1915
+ // return import.meta.glob<{ default: Model }>(path.userModelsPath('*.ts'))
1916
+ // }