@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/dist/index.d.ts +2 -0
- package/dist/index.js +993 -196
- package/dist/index.js.map +252 -352
- package/dist/types.d.ts +8 -0
- package/dist/utils.d.ts +33 -0
- package/package.json +11 -19
- package/src/generated/types.ts +0 -38
- package/src/index.ts +1 -1
- package/src/utils.ts +1868 -16
- package/src/drivers/dynamodb.ts +0 -16
- package/src/drivers/postgres.ts +0 -0
- package/src/drivers/sqlite.ts +0 -0
- /package/{src/drivers/mysql.ts → dist/generated/types.d.ts} +0 -0
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
|
|
6
|
-
import
|
|
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)
|
|
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)
|
|
37
|
+
if (model.table)
|
|
38
|
+
return model.table
|
|
28
39
|
|
|
29
40
|
return snakeCase(plural(getModelName(model, modelPath)))
|
|
30
41
|
}
|
|
31
42
|
|
|
32
|
-
export
|
|
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(
|
|
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
|
|
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
|
-
|
|
63
|
-
|
|
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
|
+
// }
|