@kava/kava-api-core 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/.github/workflows/publish.yml +40 -0
  2. package/dist/{auth.util.d.ts → auth/auth.d.ts} +1 -5
  3. package/dist/{auth.util.js → auth/auth.js} +38 -30
  4. package/dist/auth/auth.js.map +1 -0
  5. package/dist/auth/index.d.ts +1 -0
  6. package/dist/auth/index.js +2 -0
  7. package/dist/auth/index.js.map +1 -0
  8. package/dist/{context.util.js → context/context.js} +1 -1
  9. package/dist/context/context.js.map +1 -0
  10. package/dist/context/index.d.ts +1 -0
  11. package/dist/context/index.js +2 -0
  12. package/dist/context/index.js.map +1 -0
  13. package/dist/{controller.util.js → controller/controller.js} +1 -1
  14. package/dist/controller/controller.js.map +1 -0
  15. package/dist/controller/index.d.ts +1 -0
  16. package/dist/controller/index.js +2 -0
  17. package/dist/controller/index.js.map +1 -0
  18. package/dist/{conversion.util.js → conversion/conversion.js} +1 -1
  19. package/dist/conversion/conversion.js.map +1 -0
  20. package/dist/conversion/index.d.ts +1 -0
  21. package/dist/conversion/index.js +2 -0
  22. package/dist/conversion/index.js.map +1 -0
  23. package/dist/{db.util.d.ts → db/db.d.ts} +9 -5
  24. package/dist/{db.util.js → db/db.js} +40 -29
  25. package/dist/db/db.js.map +1 -0
  26. package/dist/db/index.d.ts +1 -0
  27. package/dist/db/index.js +2 -0
  28. package/dist/db/index.js.map +1 -0
  29. package/dist/index.d.ts +14 -13
  30. package/dist/index.js +14 -13
  31. package/dist/index.js.map +1 -1
  32. package/dist/logger/index.d.ts +1 -0
  33. package/dist/logger/index.js +2 -0
  34. package/dist/logger/index.js.map +1 -0
  35. package/dist/{logger.util.js → logger/logger.js} +16 -7
  36. package/dist/logger/logger.js.map +1 -0
  37. package/dist/mail/index.d.ts +1 -0
  38. package/dist/mail/index.js +2 -0
  39. package/dist/mail/index.js.map +1 -0
  40. package/dist/{mail.util.js → mail/mail.js} +1 -1
  41. package/dist/mail/mail.js.map +1 -0
  42. package/dist/middleware/index.d.ts +1 -0
  43. package/dist/middleware/index.js +2 -0
  44. package/dist/middleware/index.js.map +1 -0
  45. package/dist/{middleware.util.js → middleware/middleware.js} +1 -1
  46. package/dist/middleware/middleware.js.map +1 -0
  47. package/dist/model/index.d.ts +1 -0
  48. package/dist/model/index.js +2 -0
  49. package/dist/model/index.js.map +1 -0
  50. package/dist/{model.util.js → model/model.js} +1 -1
  51. package/dist/model/model.js.map +1 -0
  52. package/dist/permission/index.d.ts +1 -0
  53. package/dist/permission/index.js +2 -0
  54. package/dist/permission/index.js.map +1 -0
  55. package/dist/{permission.util.js → permission/permission.js} +1 -1
  56. package/dist/permission/permission.js.map +1 -0
  57. package/dist/registry/index.d.ts +1 -0
  58. package/dist/registry/index.js +2 -0
  59. package/dist/registry/index.js.map +1 -0
  60. package/dist/registry/registry.d.ts +28 -0
  61. package/dist/registry/registry.js +19 -0
  62. package/dist/registry/registry.js.map +1 -0
  63. package/dist/route/index.d.ts +1 -0
  64. package/dist/route/index.js +2 -0
  65. package/dist/route/index.js.map +1 -0
  66. package/dist/{route.util.js → route/route.js} +1 -1
  67. package/dist/route/route.js.map +1 -0
  68. package/dist/storage/index.d.ts +1 -0
  69. package/dist/storage/index.js +2 -0
  70. package/dist/storage/index.js.map +1 -0
  71. package/dist/{storage.util.js → storage/storage.js} +2 -2
  72. package/dist/storage/storage.js.map +1 -0
  73. package/dist/validation/index.d.ts +1 -0
  74. package/dist/validation/index.js +2 -0
  75. package/dist/validation/index.js.map +1 -0
  76. package/dist/{validation.util.js → validation/validation.js} +1 -1
  77. package/dist/validation/validation.js.map +1 -0
  78. package/package.json +2 -2
  79. package/src/{auth.util.ts → auth/auth.ts} +255 -241
  80. package/src/auth/index.ts +1 -0
  81. package/src/{context.util.ts → context/context.ts} +17 -17
  82. package/src/context/index.ts +1 -0
  83. package/src/{controller.util.ts → controller/controller.ts} +236 -236
  84. package/src/controller/index.ts +1 -0
  85. package/src/{conversion.util.ts → conversion/conversion.ts} +64 -64
  86. package/src/conversion/index.ts +1 -0
  87. package/src/{db.util.ts → db/db.ts} +420 -405
  88. package/src/db/index.ts +1 -0
  89. package/src/index.ts +14 -13
  90. package/src/logger/index.ts +1 -0
  91. package/src/{logger.util.ts → logger/logger.ts} +176 -169
  92. package/src/mail/index.ts +1 -0
  93. package/src/{mail.util.ts → mail/mail.ts} +85 -85
  94. package/src/middleware/index.ts +1 -0
  95. package/src/{middleware.util.ts → middleware/middleware.ts} +288 -288
  96. package/src/model/index.ts +1 -0
  97. package/src/{model.util.ts → model/model.ts} +2210 -2210
  98. package/src/permission/index.ts +1 -0
  99. package/src/{permission.util.ts → permission/permission.ts} +136 -136
  100. package/src/registry/index.ts +1 -0
  101. package/src/registry/registry.ts +37 -0
  102. package/src/route/index.ts +1 -0
  103. package/src/{route.util.ts → route/route.ts} +11 -11
  104. package/src/storage/index.ts +1 -0
  105. package/src/{storage.util.ts → storage/storage.ts} +101 -101
  106. package/src/validation/index.ts +1 -0
  107. package/src/{validation.util.ts → validation/validation.ts} +338 -338
  108. package/tsconfig.json +1 -1
  109. package/bun.lock +0 -160
  110. package/dist/auth.util.js.map +0 -1
  111. package/dist/context.util.js.map +0 -1
  112. package/dist/controller.util.js.map +0 -1
  113. package/dist/conversion.util.js.map +0 -1
  114. package/dist/db.util.js.map +0 -1
  115. package/dist/logger.util.js.map +0 -1
  116. package/dist/mail.util.js.map +0 -1
  117. package/dist/middleware.util.js.map +0 -1
  118. package/dist/model.util.js.map +0 -1
  119. package/dist/permission.util.js.map +0 -1
  120. package/dist/route.util.js.map +0 -1
  121. package/dist/storage.util.js.map +0 -1
  122. package/dist/validation.util.js.map +0 -1
  123. /package/dist/{context.util.d.ts → context/context.d.ts} +0 -0
  124. /package/dist/{controller.util.d.ts → controller/controller.d.ts} +0 -0
  125. /package/dist/{conversion.util.d.ts → conversion/conversion.d.ts} +0 -0
  126. /package/dist/{logger.util.d.ts → logger/logger.d.ts} +0 -0
  127. /package/dist/{mail.util.d.ts → mail/mail.d.ts} +0 -0
  128. /package/dist/{middleware.util.d.ts → middleware/middleware.d.ts} +0 -0
  129. /package/dist/{model.util.d.ts → model/model.d.ts} +0 -0
  130. /package/dist/{permission.util.d.ts → permission/permission.d.ts} +0 -0
  131. /package/dist/{route.util.d.ts → route/route.d.ts} +0 -0
  132. /package/dist/{storage.util.d.ts → storage/storage.d.ts} +0 -0
  133. /package/dist/{validation.util.d.ts → validation/validation.d.ts} +0 -0
@@ -1,2211 +1,2211 @@
1
- import { status } from 'elysia'
2
- import type { Knex } from 'knex'
3
- import { conversion, db } from '@utils'
4
-
5
-
6
-
7
- // ==========================
8
- // ## Decorator Type
9
- // ==========================
10
- export const PRIMARY_KEY_META = Symbol('primary-key-meta')
11
- export const FIELD_META = Symbol('field-meta')
12
- export const RELATION_META = Symbol('relation-meta')
13
- export const SOFT_DELETE_META = Symbol('soft-delete')
14
- export const ATTRIBUTE_META = Symbol('attribute-meta')
15
- export const FORMATTER_META = Symbol('formatter-meta')
16
- export const SCOPE_META = Symbol('scope-meta')
17
-
18
-
19
- // ==========================
20
- // ## Field Decorator Type
21
- // ==========================
22
- export type FieldFlag = 'fillable' | 'selectable' | 'searchable' | 'hidden'
23
- export type FieldMeta = {
24
- cast ?: ModelCastType
25
- fillable ?: boolean
26
- selectable ?: boolean
27
- searchable ?: boolean
28
- hidden ?: boolean
29
- }
30
-
31
-
32
- // ==========================
33
- // ## Payload Type
34
- // ==========================
35
- type NonFunctionKeys<T> = {
36
- [K in keyof T]: T[K] extends Function ? never : K
37
- }[keyof T]
38
-
39
- type DataShape<T> = Pick<T, NonFunctionKeys<T>>
40
-
41
- type ModelPayload<T> = Partial<DataShape<T>>
42
-
43
-
44
- // ==========================
45
- // ## Cast Type
46
- // ==========================
47
- export type ModelCastType = 'string' | 'number' | 'boolean' | 'date' | 'json'
48
- const Casts = {
49
- string : {
50
- fromDB : (v: any) => v == null ? v : String(v),
51
- toDB : (v: any) => v,
52
- },
53
- number : {
54
- fromDB : (v: any) => v == null ? v : Number(v),
55
- toDB : (v: any) => v,
56
- },
57
- boolean : {
58
- fromDB : (v: any) => Boolean(v),
59
- toDB : (v: any) => v ? 1 : 0,
60
- },
61
- date : {
62
- fromDB : (v: any) => v ? new Date(v) : null,
63
- toDB : (v: any) => v instanceof Date ? v.toISOString() : v,
64
- },
65
- json : {
66
- fromDB: (v: any) => {
67
- if (typeof v !== 'string') return v
68
-
69
- try {
70
- return JSON.parse(v)
71
- } catch {
72
- return null
73
- }
74
- },
75
- toDB : (v: any) => JSON.stringify(v),
76
- },
77
- }
78
-
79
-
80
- // ==========================
81
- // ## Relation Type
82
- // ==========================
83
- export type ModelRelationType = 'hasMany' | 'hasOne' | 'belongsTo' | 'belongsToMany'
84
- export type ModelRelationDescriptor = {
85
- type : ModelRelationType
86
- model : () => typeof Model
87
- foreignKey : string
88
- localKey : string
89
- pivotTable ?: string
90
- pivotLocal ?: string
91
- pivotForeign ?: string
92
- callback ?: (q: any) => void
93
- }
94
-
95
-
96
- // ==========================
97
- // ## Hook type
98
- // ==========================
99
- export type ModelHookEventType = "before-create" | "after-create" | "before-update" | "after-update" | "before-delete" | "after-delete"
100
- export type ModelHookFn = (ctx: ModelHookContextType) => any
101
- export type ModelHookContextType<T extends Model = Model> = {
102
- model : T
103
- trx ?: any
104
- snapshot ?: any
105
- }
106
-
107
-
108
- // ==========================
109
- // ## Aggregate type
110
- // ==========================
111
- type AggregateType = 'count' | 'sum' | 'avg' | 'min' | 'max'
112
-
113
-
114
- // ==========================
115
- // ## Scope type
116
- // ==========================
117
- export type ScopeType = {
118
- fn: Function
119
- mode: 'global' | 'internal'
120
- }
121
-
122
-
123
- // ==========================
124
- // ## Override knex Query builder interface
125
- // ==========================
126
- declare module 'knex' {
127
- namespace Knex {
128
- interface QueryBuilder<TRecord = any, TResult = any> {
129
- $model?: any
130
- _withTree?: Record<string, any>
131
- _formatter?: ((item: any) => any) | null
132
-
133
- _softDeleteScope?: 'default' | 'with' | 'only'
134
-
135
- _withAggregates?: Array<{
136
- relation: string
137
- alias: string
138
- fn: 'count' | 'sum' | 'avg' | 'min' | 'max'
139
- column: string
140
- callback?: (q: any) => void
141
- }>
142
-
143
- _orderByAggregates?: Array<{
144
- relation: string
145
- alias?: string
146
- fn: 'count' | 'sum' | 'avg' | 'min' | 'max'
147
- column: string
148
- direction: 'asc' | 'desc'
149
- callback?: (q: any) => void
150
- }>
151
- }
152
- }
153
- }
154
-
155
- export interface ModelQueryBuilder<T extends Record<string, any> = Record<string, any> > extends Knex.QueryBuilder<T, T[]> {
156
- _withTree?: Record<string, any>
157
- _formatter?: ((item: T) => any) | null
158
- _softDeleteScope?: 'default' | 'with' | 'only'
159
-
160
- findOrNotFound(id: string | number): Promise<T>
161
- firstOrNotFound(): Promise<T>
162
-
163
- search(
164
- keyword?: string,
165
- options?: {
166
- includes?: string[]
167
- searchable?: string[]
168
- }
169
- ): this
170
- filter(filters?: Record<string, string>): this
171
- selects(options?: {
172
- includes?: string[]
173
- selectable?: string[]
174
- }): this
175
- sorts(sorts?: string[]): this
176
-
177
- with(relation: string, callback?: any): this
178
- expand(relations?: Array<string | Record<string, (q: any) => void>>): this
179
-
180
- whereHas(
181
- relation: string,
182
- callback?: (q: ModelQueryBuilder<any>) => void
183
- ): this
184
- orWhereHas(
185
- relation: string,
186
- callback?: (q: ModelQueryBuilder<any>) => void
187
- ): this
188
- whereDoesntHave(
189
- relation: string,
190
- callback?: (q: ModelQueryBuilder<any>) => void
191
- ): this
192
- orWhereDoesntHave(
193
- relation: string,
194
- callback?: (q: ModelQueryBuilder<any>) => void
195
- ): this
196
-
197
- withAggregate(
198
- expr: string,
199
- fn: 'count' | 'sum' | 'avg' | 'min' | 'max',
200
- column?: string,
201
- callback?: (q: ModelQueryBuilder<any>) => void
202
- ): this
203
- orderByAggregate(
204
- expr: string,
205
- fn: 'count' | 'sum' | 'avg' | 'min' | 'max',
206
- column?: string,
207
- direction?: 'asc' | 'desc',
208
- callback?: (q: ModelQueryBuilder<any>) => void
209
- ): this
210
-
211
-
212
- get(): Promise<T[]>
213
- getFirst(): Promise<T>
214
- paginate(
215
- page?: number,
216
- limit?: number
217
- ): Promise<{ data: T[]; total: number }>
218
- option(selectableOption?: string[]): Promise<Array<{ value: any; label: any }>>
219
- paginateOrOption(
220
- page?: number,
221
- limit?: number,
222
- option?: string | boolean,
223
- selectableOption?: string[]
224
- ): Promise<{ data: any[]; total: number }>
225
- resolve(input?: any): Promise<{ data: T[]; total: number }>
226
-
227
- format(formatter: string | ((item: T) => any)): this
228
-
229
- withTrashed(): this
230
- onlyTrashed(): this
231
- }
232
-
233
-
234
-
235
- export abstract class Model {
236
- id!: number;
237
- created_at!: Date;
238
- updated_at!: Date;
239
- deleted_at!: Date;
240
-
241
- static table : string = ""
242
- static primaryKey : string = 'id'
243
- static softDelete : boolean = false
244
- static deletedAtColumn : string = 'deleted_at'
245
-
246
- protected _original : Record<string, any> = {}
247
- protected _exists : boolean = false
248
- protected _trx ?: Knex.Transaction = undefined
249
-
250
- private static _hooks : Record<string, ModelHookFn[]> = {}
251
- private _instanceHooks : Record<string, ModelHookFn[]> = {}
252
- private _disabledHooks = new Set<string>()
253
-
254
-
255
- // ==========================
256
- // ## Constructor model
257
- // ==========================
258
- constructor(data: Record<string, any> = {}) {
259
- Object.assign(this, data)
260
-
261
- if ((this as any)[(this.constructor as typeof Model).primaryKey]) this._exists = true
262
- }
263
-
264
- protected static newInstance<T extends typeof Model>(this: T): InstanceType<T> & Model {
265
- return new (this as any)()
266
- }
267
-
268
-
269
- // ==========================
270
- // ## Field
271
- // ==========================
272
- static [FIELD_META] ?: Record<string, any>
273
-
274
- static getDefaultFields(): Record<string, FieldMeta> {
275
- const pk = (this as any)[PRIMARY_KEY_META] ?? this.primaryKey ?? 'id'
276
-
277
- return {
278
- [pk]: {
279
- cast: 'number',
280
- selectable: true,
281
- },
282
- created_at: { cast: 'date' },
283
- updated_at: { cast: 'date' },
284
- deleted_at: { cast: 'date' },
285
- }
286
- }
287
-
288
- static get fields(): Record<string, FieldMeta> {
289
- const defaults = this.getDefaultFields?.() ?? {};
290
-
291
- return {...defaults,...(this[FIELD_META] ?? {})}
292
- }
293
-
294
- static get fillable() {
295
- return Object.entries(this.fields).filter(([, v]) => v.fillable).map(([k]) => k)
296
- }
297
-
298
- static get selectable() {
299
- return Object.entries(this.fields).filter(([, v]) => v.selectable).map(([k]) => k)
300
- }
301
-
302
- static get searchable() {
303
- return Object.entries(this.fields).filter(([, v]) => v.searchable).map(([k]) => k)
304
- }
305
-
306
-
307
- // ==========================
308
- // ## Table
309
- // ==========================
310
- static getTable(): string {
311
- if (this.table) return this.table
312
-
313
- return conversion.strPlural(conversion.strSnake(this.name))
314
- }
315
-
316
-
317
- // ==========================
318
- // ## Primary key
319
- // ==========================
320
- static getPrimaryKey(): string {
321
- return (this as any)[PRIMARY_KEY_META] ?? this.primaryKey ?? 'id'
322
- }
323
-
324
-
325
- // ==========================
326
- // ## Relation
327
- // ==========================
328
- static [RELATION_META] ?: Record<string, any>
329
-
330
- static get relations() {
331
- return this[RELATION_META] ?? {}
332
- }
333
-
334
-
335
- // ==========================
336
- // ## Attribute
337
- // ==========================
338
- static get attributes(): Record<string, Function> {
339
- return (this as any)[ATTRIBUTE_META] ?? {}
340
- }
341
-
342
-
343
- // ==========================
344
- // ## Formatter
345
- // ==========================
346
- static get formatters(): Record<string, Function> {
347
- return (this as any)[FORMATTER_META] ?? {}
348
- }
349
-
350
-
351
- // ==========================
352
- // ## Scope
353
- // ==========================
354
- static get scopes(): Record<string, ScopeType> {
355
- return (this as any)[SCOPE_META] ?? {}
356
- }
357
-
358
-
359
- // ==========================
360
- // ## Getter attribute
361
- // ==========================
362
- getOriginal(key?: string) {
363
- if (!key) return { ...this._original }
364
-
365
- return this._original[key]
366
- }
367
-
368
- getChanges() {
369
- const changes: Record<string, any> = {}
370
- for (const key in this._original) {
371
- if ((this as any)[key] != this._original[key]) {
372
- changes[key] = (this as any)[key]
373
- }
374
- }
375
-
376
- return changes
377
- }
378
-
379
- getPrevious() {
380
- const prev: Record<string, any> = {}
381
- for (const key in this._original) {
382
- if ((this as any)[key] != this._original[key]) {
383
- prev[key] = this._original[key]
384
- }
385
- }
386
-
387
- return prev
388
- }
389
-
390
-
391
- // ==========================
392
- // ## Query builder
393
- // ==========================
394
- static query<T extends Record<string, any> = Record<string, any>>(trx?: Knex | Knex.Transaction): ModelQueryBuilder<T> {
395
- const qb = (trx ?? db)(this.getTable())
396
-
397
- return extendModelQuery(qb, this) as ModelQueryBuilder<T>
398
- }
399
-
400
-
401
- // ==========================
402
- // ## Casting
403
- // ==========================
404
- castFromDB(row: Record<string, any>): this {
405
- const ctor = this.constructor as typeof Model
406
- const fields = ctor.fields
407
-
408
- for (const [key, value] of Object.entries(row)) {
409
- if (!fields[key]) continue
410
- const meta = fields[key]
411
- ;(this as any)[key] = meta.cast ? Casts[meta.cast].fromDB(value) : value
412
- }
413
-
414
- this._original = {}
415
- for (const key of Object.keys(fields)) {
416
- this._original[key] = (this as any)[key]
417
- }
418
-
419
- const attrs = ctor.attributes
420
- for (const [name, fn] of Object.entries(attrs)) {
421
- Object.defineProperty(this, name, {
422
- get: () => fn.call(this),
423
- enumerable: true,
424
- })
425
- }
426
-
427
- this._exists = true
428
- return this
429
- }
430
-
431
-
432
- castToDB() {
433
- const fields = (this.constructor as typeof Model).fields
434
- const data: Record<string, any> = {}
435
-
436
- for (const [key, meta] of Object.entries(fields)) {
437
- if (!meta.fillable) continue
438
-
439
- const val = (this as any)[key]
440
-
441
- if (val === undefined) continue
442
-
443
- data [key] = meta.cast ? Casts[meta.cast].toDB(val): val
444
- }
445
-
446
- return data
447
- }
448
-
449
-
450
- // ==========================
451
- // ## Dirty Field
452
- // ==========================
453
- getDirty(): Record<string, any> {
454
- const dirty: any = {}
455
- for (const key in this._original) {
456
- if (Object.is((this as any)[key], this._original[key])) continue
457
- dirty[key] = (this as any)[key]
458
- }
459
-
460
- return dirty
461
- }
462
-
463
-
464
- // ==========================
465
- // ## Hydration
466
- // ==========================
467
- static hydrate<T extends typeof Model>(
468
- this: T,
469
- rows: any[] | null | undefined
470
- ): InstanceType<T>[] {
471
- if (!rows || !Array.isArray(rows)) return []
472
-
473
- return rows.map(row => {
474
- if (row instanceof this) return row as InstanceType<T>
475
-
476
- const instance = this.newInstance()
477
-
478
- return instance.castFromDB(row) as InstanceType<T>
479
- })
480
- }
481
-
482
-
483
- // ==========================
484
- // ## Fill model from payload
485
- // ==========================
486
- fill<T extends this>(this: T, payload: ModelPayload<T>): T {
487
- const ctor = this.constructor as typeof Model
488
- const fields = ctor.fields
489
-
490
- for (const [key, value] of Object.entries(payload)) {
491
- const meta = fields[key]
492
- if (!meta || !meta.fillable) continue
493
- ;(this as any)[key] = value
494
- }
495
-
496
- return this
497
- }
498
-
499
- // ==========================
500
- // ## Create from fillable
501
- // ==========================
502
- static async create<T extends typeof Model>(
503
- this: T,
504
- payload: ModelPayload<InstanceType<T>>,
505
- trx?: Knex.Transaction
506
- ): Promise<InstanceType<T>> {
507
-
508
- const instance = this.newInstance() as InstanceType<T>
509
-
510
- instance.fill(payload)
511
-
512
- const data = instance.castToDB()
513
-
514
- await instance.runHook('before-create', { model: instance, trx })
515
-
516
- const conn = trx ?? db
517
- const [row] = await conn(this.getTable()).insert(data).returning('*')
518
-
519
- await instance.runHook('after-create', { model: instance, trx })
520
-
521
- return instance.castFromDB(row)
522
- }
523
-
524
-
525
-
526
-
527
- // ==========================
528
- // ## update from fillable
529
- // ==========================
530
- static async update<T extends typeof Model>(
531
- this: T,
532
- payload: ModelPayload<InstanceType<T>>,
533
- uniqueKeys: (keyof InstanceType<T> & string)[],
534
- trx?: Knex.Transaction
535
- ): Promise<InstanceType<T>> {
536
-
537
- if (!uniqueKeys.length) {
538
- throw new Error('updateByUnique requires uniqueKeys')
539
- }
540
-
541
- const instance = this.newInstance() as InstanceType<T>
542
- instance.fill(payload)
543
-
544
- const data = instance.castToDB()
545
- const where: Record<string, any> = {}
546
-
547
- for (const key of uniqueKeys) {
548
- if ((payload as any)[key] === undefined) {
549
- throw new Error(`Missing unique key: ${key}`)
550
- }
551
- where[key] = (payload as any)[key]
552
- }
553
-
554
- const conn = trx ?? db
555
- const [row] = await conn(this.getTable()).where(where).update(data).returning('*')
556
-
557
- if (!row) throw status(404, { message: 'Record not found' })
558
-
559
- return instance.castFromDB(row)
560
- }
561
-
562
-
563
-
564
-
565
- // ==========================
566
- // ## Update or insert from fillable
567
- // ==========================
568
- static async upsert<T extends typeof Model>(
569
- this: T,
570
- payload: ModelPayload<InstanceType<T>>,
571
- uniqueKeys: (keyof InstanceType<T> & string)[],
572
- trx?: Knex.Transaction
573
- ): Promise<InstanceType<T>> {
574
- if(uniqueKeys.length === 0) {
575
- throw new Error('Upsert requires uniqueKeys')
576
- }
577
-
578
- const constraints: Record<string, any> = {}
579
-
580
- for (const key of uniqueKeys) {
581
- if ((payload as any)[key] === undefined) {
582
- throw new Error(`Missing unique key: ${key}`)
583
- }
584
- constraints[key] = (payload as any)[key]
585
- }
586
-
587
- let row = await this.query().where(constraints).first();
588
-
589
- if (!row) {
590
- const instance = this.newInstance() as InstanceType<T>
591
- instance.fill(payload)
592
-
593
- const data = instance.castToDB()
594
- const conn = trx ?? db
595
- const [row] = await conn(this.getTable()).insert(data).returning('*')
596
-
597
- return instance.castFromDB(row)
598
- } else {
599
- const instance = this.newInstance() as InstanceType<T>
600
- instance.fill(payload)
601
-
602
- const data = instance.castToDB()
603
- const conn = trx ?? db
604
- const [row] = await conn(this.getTable()).where(constraints).update(data).returning('*')
605
-
606
- return instance.castFromDB(row)
607
- }
608
- }
609
-
610
-
611
-
612
-
613
- // ==========================
614
- // ## Save (insert / update)
615
- // ==========================
616
- async save() {
617
- const model = this.constructor as typeof Model
618
- const table = model.getTable()
619
- const pk = model.primaryKey
620
- const conn = this._trx ?? db
621
- const hookCtx = { model: this, trx: this._trx }
622
-
623
- if (!this._exists) {
624
- const data = this.castToDB()
625
-
626
- await this.runHook(`before-create` as ModelHookEventType, hookCtx)
627
-
628
- const [row] = await conn(table).insert(data).returning('*')
629
-
630
- this.castFromDB(row)
631
-
632
- await this.runHook(`after-create` as ModelHookEventType, hookCtx)
633
-
634
- return this
635
- }
636
-
637
- const dirty = this.getDirty()
638
- if (Object.keys(dirty).length === 0) return this
639
-
640
- await this.runHook(`before-update` as ModelHookEventType, hookCtx)
641
-
642
- const fields = model.fields
643
- const updateData: Record<string, any> = {}
644
-
645
- for (const [key, meta] of Object.entries(fields)) {
646
- if (!meta.fillable) continue
647
- if (!(key in dirty)) continue
648
-
649
- const val = dirty[key]
650
- updateData[key] = meta.cast ? Casts[meta.cast].toDB(val) : val
651
- }
652
-
653
- if (Object.keys(updateData).length === 0) return this
654
-
655
- await conn(table).where(pk, (this as any)[pk]).update(updateData)
656
-
657
- Object.assign(this._original, dirty)
658
-
659
- await this.runHook(`after-update` as ModelHookEventType, hookCtx)
660
-
661
- return this
662
- }
663
-
664
-
665
-
666
- // ==========================
667
- // ## Save with relation
668
- // ==========================
669
- async pump<T extends this>(
670
- this : T,
671
- payload : ModelPayload<T> | ModelPayload<T>[],
672
- options : { trx?: Knex.Transaction } = {}
673
- ) : Promise<T | T[]> {
674
-
675
- const isRoot = !options.trx
676
- const trx = options.trx ?? await db.transaction()
677
-
678
- try {
679
-
680
- // Handle Array (Bulk)
681
- if (Array.isArray(payload)) {
682
- const Ctor = this.constructor as typeof Model
683
- const pkName = Ctor.primaryKey ?? 'id'
684
- const results: T[] = []
685
-
686
- for (const item of payload) {
687
- let instance: T | null = null
688
- const pkValue = (item as any)[pkName]
689
-
690
- if (pkValue) {
691
- const row = await Ctor.query(trx).where(pkName, pkValue).first()
692
-
693
- if (row) {
694
- instance = Ctor.hydrate([row])[0] as T
695
- }
696
- }
697
-
698
- if (!instance) {
699
- instance = Ctor.newInstance() as T
700
- }
701
-
702
- await instance.pump(item, { trx })
703
- results.push(instance)
704
- }
705
-
706
- if (isRoot) await trx.commit()
707
- return results
708
- }
709
-
710
-
711
- const ctor = this.constructor as typeof Model
712
- const fields = ctor.fields
713
- const relations = ctor.relations ?? {}
714
-
715
- const flat: ModelPayload<T> = {}
716
- const nested: Record<string, any> = {}
717
-
718
- for (const key of Object.keys(payload) as Array<keyof DataShape<T>>) {
719
- const value = (payload as any)[key]
720
-
721
- if (fields[key as string]?.fillable) {
722
- flat[key] = value
723
- } else if (relations[key as string] && value !== null) {
724
- nested[key as string] = value
725
- }
726
- }
727
-
728
- this.fill(flat)
729
- await this.useTransaction(trx).save()
730
-
731
- for (const [name, value] of Object.entries(nested)) {
732
- const relDef = relations[name]
733
- if (!relDef) continue
734
-
735
- const desc = relDef()
736
- const Related = desc.model()
737
-
738
- // ===== hasMany / belongsToMany =====
739
- if (Array.isArray(value)) {
740
- const existing = await Related.query(trx).where(desc.foreignKey, (this as any)[desc.localKey]).get()
741
-
742
- const existingIds = existing.map((r: Model) => (r as any)[Related.primaryKey])
743
- const incomingIds: any[] = []
744
-
745
- for (const item of value) {
746
- if ((item as any)[Related.primaryKey]) {
747
- const child = await Related.query(trx).where(Related.primaryKey, (item as any)[Related.primaryKey]).getFirst()
748
-
749
- if (child) {
750
- await child.pump(item as any, { trx })
751
- incomingIds.push(child[Related.primaryKey])
752
- }
753
- } else {
754
- const child = Related.newInstance()
755
- ;(child as any)[desc.foreignKey] = (this as any)[desc.localKey]
756
- await child.pump(item as any, { trx })
757
- incomingIds.push(child[Related.primaryKey])
758
- }
759
- }
760
-
761
- const toDelete = existingIds.filter((id: number) => !incomingIds.includes(id))
762
- if (toDelete.length) {
763
- await Related.query(trx).whereIn(Related.primaryKey, toDelete).delete()
764
- }
765
-
766
- continue
767
- }
768
-
769
- const child = Related.newInstance()
770
- if (desc.type !== 'belongsTo') {
771
- ;(child as any)[desc.foreignKey] = (this as any)[desc.localKey]
772
- }
773
-
774
- await child.pump(value as any, { trx })
775
- }
776
-
777
- if (isRoot) await trx.commit()
778
- return this
779
-
780
- } catch (err) {
781
- if (isRoot) await trx.rollback()
782
- throw err
783
- }
784
- }
785
-
786
-
787
-
788
- // ==========================
789
- // ## Soft Delete
790
- // ==========================
791
- static getSoftDeleteConfig() {
792
- return (this as any)[SOFT_DELETE_META] ?? null
793
- }
794
-
795
- static isSoftDelete(): boolean {
796
- return !!this.getSoftDeleteConfig()
797
- }
798
-
799
- static getDeletedAtColumn(): string | null {
800
- return this.getSoftDeleteConfig()?.column ?? null
801
- }
802
-
803
-
804
- // ==========================
805
- // ## Delete (with or without soft delete)
806
- // ==========================
807
- async delete(): Promise<Record<string, any> | null> {
808
- const model = this.constructor as typeof Model
809
- const soft = model.getSoftDeleteConfig?.()
810
-
811
- if (!this._exists) return null
812
-
813
- if (!soft) return await this.forceDelete()
814
-
815
- const trx = this._trx ?? await db.transaction()
816
-
817
- try {
818
- await this.runHook(`before-delete` as ModelHookEventType, { model: this, trx })
819
-
820
- await trx(model.getTable()).where(model.primaryKey, (this as any)[model.primaryKey]).update({ [soft.column]: new Date() })
821
-
822
- await this.runHook(`after-delete` as ModelHookEventType, { model: this, trx })
823
-
824
- if (!this._trx) await trx.commit()
825
-
826
- const snapshot = await (this._trx ?? db)(model.getTable()).where(model.primaryKey, (this as any)[model.primaryKey]).first()
827
-
828
- this._exists = false
829
-
830
- return snapshot
831
- } catch (err) {
832
- if (!this._trx) await trx.rollback()
833
-
834
- throw err
835
- }
836
- }
837
-
838
-
839
- // ==========================
840
- // ## Delete without soft delete
841
- // ==========================
842
- async forceDelete() {
843
- const model = this.constructor as typeof Model
844
- const pk = model.primaryKey
845
-
846
- if (!this._exists) return
847
-
848
- const snapshot = await (this._trx ?? db)(model.getTable()).where(pk, (this as any)[pk]).first()
849
-
850
- if (!snapshot) return null
851
-
852
- await (this._trx ?? db)(model.getTable()).where(pk, (this as any)[pk]).delete()
853
-
854
- this._exists = false
855
-
856
- return snapshot
857
- }
858
-
859
- async restore(): Promise<this> {
860
- const model = this.constructor as typeof Model
861
- const soft = model.getSoftDeleteConfig?.()
862
-
863
- if (!soft) return this
864
-
865
- await this.runHook(`before-update` as ModelHookEventType, { model: this, trx: this._trx })
866
-
867
- await (this._trx ?? db)(model.getTable()).where(model.primaryKey, (this as any)[model.primaryKey]).update({ [soft.column]: null })
868
-
869
- await this.runHook(`after-update` as ModelHookEventType, { model: this, trx: this._trx })
870
-
871
- this._exists = true
872
-
873
- return this
874
- }
875
-
876
-
877
-
878
- // ==========================
879
- // ## Model Hook
880
- // ==========================
881
- on<T extends this>(event: ModelHookEventType, fn: (ctx: ModelHookContextType<T>) => any) {
882
- if (!this._instanceHooks[event]) this._instanceHooks[event] = []
883
-
884
- this._instanceHooks[event].push(fn as ModelHookFn)
885
-
886
- return this
887
- }
888
-
889
- off(event: ModelHookEventType) {
890
- this._disabledHooks.add(event)
891
-
892
- return this
893
- }
894
-
895
- protected async runHook(event: ModelHookEventType, ctx: ModelHookContextType) {
896
- if (this._disabledHooks.has(event)) return
897
-
898
- const ctor = this.constructor as typeof Model
899
-
900
- const globals = ctor._hooks?.[event] ?? []
901
- for (const fn of globals) await fn(ctx)
902
-
903
- const locals = this._instanceHooks[event] ?? []
904
- for (const fn of locals) await fn(ctx)
905
- }
906
-
907
-
908
-
909
- // ==========================
910
- // ## To response data
911
- // ==========================
912
- toJSON() {
913
- const data: Record<string, any> = {}
914
- const ctor = this.constructor as typeof Model
915
- const fields = ctor.fields ?? {}
916
- const relations = ctor.relations ?? {}
917
- const attributes = ctor.attributes ?? {}
918
-
919
- for (const key of Object.keys(fields)) {
920
- if ((fields[key] as any)?.hidden) continue
921
- data[key] = (this as any)[key]
922
- }
923
-
924
- const expanded = (this as any).__expandedAttributes ?? []
925
- for (const key of expanded) {
926
- if (attributes[key]) {
927
- data[key] = (this as any)[key]
928
- }
929
- }
930
-
931
- for (const key of Object.keys(relations)) {
932
- if ((this as any)[key] !== undefined) {
933
- data[key] = (this as any)[key]
934
- }
935
- }
936
-
937
- return data
938
- }
939
-
940
-
941
-
942
-
943
- // ==========================
944
- // ## Transaction binding
945
- // ==========================
946
- useTransaction(trx: Knex.Transaction) {
947
- this._trx = trx
948
-
949
- return this
950
- }
951
- }
952
-
953
-
954
-
955
- // ==========================
956
- // ## Model query builder (extend from knex query builder)
957
- // ==========================
958
- export function extendModelQuery(
959
- query: Knex.QueryBuilder,
960
- Model: any
961
- ) {
962
- ;(query as any).$model = Model
963
- ;(query as any)._withTree = {}
964
- ;(query as any)._softDeleteScope = 'default' //? default | with | only
965
- ;(query as any)._formatter = null
966
- ;(query as any)._disabledScopes = new Set<string>()
967
-
968
-
969
- // =========================
970
- // ## Soft delete query
971
- // =========================
972
- ;(query as any).withTrashed = function () {
973
- this._softDeleteScope = 'with'
974
- return this
975
- }
976
-
977
- ;(query as any).onlyTrashed = function () {
978
- this._softDeleteScope = 'only'
979
- return this
980
- }
981
-
982
-
983
- // ==========================
984
- // ## find or not found
985
- // ==========================
986
- if (!(query as any).findOrNotFound) {
987
- ;(query as any).findOrNotFound = async function (id: any) {
988
- applyGlobalScopes(this)
989
-
990
- const pk = Model.primaryKey ?? 'id'
991
- const row = await this.where(pk, id).first()
992
-
993
- if (!row) throw status(404, { message: "Error: Record not found!" });
994
-
995
- return Model.hydrate([row])[0]
996
- }
997
- }
998
-
999
-
1000
- // ==========================
1001
- // ## first or not found
1002
- // ==========================
1003
- if (!(query as any).firstOrNotFound) {
1004
- ;(query as any).firstOrNotFound = async function () {
1005
- applyGlobalScopes(this)
1006
-
1007
- const row = await this.first()
1008
-
1009
- if (!row) throw status(404, { message: "Error: Record not found!" });
1010
-
1011
- return Model.hydrate([row])[0]
1012
- }
1013
- }
1014
-
1015
-
1016
- // ==========================
1017
- // ## find
1018
- // ==========================
1019
- if (!(query as any).find) {
1020
- ;(query as any).find = async function (id: any) {
1021
- applyGlobalScopes(this)
1022
-
1023
- const pk = Model.primaryKey ?? 'id'
1024
- const row = await this.where(pk, id).first()
1025
-
1026
- if (!row) return null;
1027
-
1028
- return Model.hydrate([row])[0]
1029
- }
1030
- }
1031
-
1032
-
1033
- // ==========================
1034
- // ## Search query
1035
- // ==========================
1036
- if (!(query as any).search) {
1037
- ;(query as any).search = function (
1038
- keyword : string,
1039
- { includes = [], searchable = [] } : { includes?: string[], searchable?: string[] } = {}
1040
- ) {
1041
- const model = (this as any).$model
1042
- if (!model) return this
1043
-
1044
- const defaultSearchable = Model.searchable || []
1045
- const mergedSearchable = searchable?.length ? searchable : [...defaultSearchable, ...includes]
1046
-
1047
- if (!keyword || !mergedSearchable.length) return this
1048
-
1049
-
1050
- this.where((q: any) => {
1051
- mergedSearchable.forEach((column) => {
1052
- if (column.includes(".")) {
1053
- const [relation, col] = column.split(".")
1054
- q.orWhereHas(relation, (rel: any) => rel.where(col, "ILIKE", `%${keyword}%`))
1055
- } else {
1056
- q.orWhere(column, "ILIKE", `%${keyword}%`)
1057
- }
1058
- })
1059
- })
1060
-
1061
- return this
1062
- }
1063
- }
1064
-
1065
-
1066
- // ==========================
1067
- // ## Filer query
1068
- // ==========================
1069
- if (!(query as any).filter) {
1070
- ;(query as any).filter = function (filters?: Record<string, string>) {
1071
- if (!filters) return this
1072
-
1073
- for (const [field, filter] of Object.entries(filters)) {
1074
- const [type, value] = filter.split(":")
1075
- if (!type || value === undefined) continue
1076
-
1077
- const applyWhere = (q: any, col: string) => {
1078
- switch (type) {
1079
- case "li": q.where(col, "ILIKE", `%${value}%`); break
1080
- case "eq": q.where(col, value); break
1081
- case "ne": q.where(col, "!=", value); break
1082
- case "in": q.whereIn(col, value.split(",")); break
1083
- case "ni": q.whereNotIn(col, value.split(",")); break
1084
- case "bw": {
1085
- const [min, max] = value.split(",")
1086
- q.whereBetween(col, [min, max])
1087
- break
1088
- }
1089
- }
1090
- }
1091
-
1092
- if (field.includes(".")) {
1093
- const [relation, col] = field.split(".")
1094
- this.whereHas(relation, (q: any) => applyWhere(q, col))
1095
- } else {
1096
- applyWhere(this, field)
1097
- }
1098
- }
1099
-
1100
- return this
1101
- }
1102
- }
1103
-
1104
-
1105
- // ==========================
1106
- // ## Select query
1107
- // ==========================
1108
- if (!(query as any).selects) {
1109
- ;(query as any).selects = function ({ includes = [], selectable = [] } : { includes?: string[], selectable?: string[] } = {}) {
1110
- const model = (this as any).$model
1111
- if (!model) return this
1112
-
1113
- const defaultSelectable = Model.selectable || ["*"]
1114
-
1115
- this.select(selectable?.length ? selectable : [...defaultSelectable, ...includes])
1116
-
1117
- return this
1118
- }
1119
- }
1120
-
1121
-
1122
- // ==========================
1123
- // ## Sort query
1124
- // ==========================
1125
- if (!(query as any).sorts) {
1126
- ;(query as any).sorts = function (sorts?: string[]) {
1127
- if (!Array.isArray(sorts) || sorts.length === 0) return this;
1128
-
1129
- sorts.forEach((sortExpr) => {
1130
- if (typeof sortExpr !== "string") return;
1131
-
1132
- const parts = sortExpr.trim().split(/\s+/);
1133
- const column = parts[0];
1134
- const direction = (parts[1] || "asc").toLowerCase();
1135
-
1136
- if (!["asc", "desc"].includes(direction)) {
1137
- this.orderBy(column, "asc");
1138
- } else {
1139
- this.orderBy(column, direction);
1140
- }
1141
- });
1142
-
1143
- return this;
1144
- };
1145
- }
1146
-
1147
-
1148
-
1149
- // ==========================
1150
- // ## Get query
1151
- // ==========================
1152
- ;(query as any).get = async function () {
1153
- applyGlobalScopes(this)
1154
- applyWithAggregates(this)
1155
- applyOrderByAggregates(this)
1156
-
1157
- const rows = await this
1158
- let result = this.$model.hydrate(rows)
1159
-
1160
- if (this._withTree && Object.keys(this._withTree).length) {
1161
- await loadRelations(result, this.$model, this._withTree)
1162
- }
1163
-
1164
- result.forEach((item: any) => {
1165
- item.__expandedAttributes = Object.keys(this._withTree || {})
1166
- .filter(k => this._withTree[k]?.__attribute)
1167
- })
1168
-
1169
- if (this._formatter) {
1170
- result = result.map(this._formatter)
1171
- }
1172
-
1173
- return result
1174
- }
1175
-
1176
-
1177
- // ==========================
1178
- // ## First query
1179
- // ==========================
1180
- ;(query as any).getFirst = async function () {
1181
- applyGlobalScopes(this)
1182
- applyWithAggregates(this)
1183
- applyOrderByAggregates(this)
1184
-
1185
- const rows = await this.limit(1)
1186
- let result = this.$model.hydrate(rows)
1187
-
1188
- if (this._withTree && Object.keys(this._withTree).length) {
1189
- await loadRelations(result, this.$model, this._withTree)
1190
- }
1191
-
1192
- if(!result.at(0)) return null
1193
-
1194
- result.at(0).__expandedAttributes = Object.keys(this._withTree || {}).filter(k => this._withTree[k]?.__attribute)
1195
-
1196
- if (this._formatter) {
1197
- result = result.map(this._formatter).at(0)
1198
- }
1199
-
1200
- return result.at(0)
1201
- }
1202
-
1203
-
1204
-
1205
- // ==========================
1206
- // ## Paginate query
1207
- // ==========================
1208
- if (!(query as any).paginate) {
1209
- ;(query as any).paginate = async function (page = 1, limit = 10) {
1210
- applyGlobalScopes(this)
1211
- applyWithAggregates(this)
1212
- applyOrderByAggregates(this)
1213
-
1214
- const offset = (page - 1) * limit
1215
-
1216
- const raw = await this.clone().limit(limit).offset(offset)
1217
- let data = Model.hydrate(raw)
1218
-
1219
- const [{ count }] = await this.clone().clearSelect().clearOrder().count('* as count')
1220
- const total = Number(count)
1221
-
1222
- if(!total) return { data: [], total: 0 }
1223
-
1224
- if (this._withTree && Object.keys(this._withTree).length) {
1225
- await loadRelations(data, this.$model, this._withTree)
1226
- }
1227
-
1228
- data.forEach((item: any) => {
1229
- item.__expandedAttributes = Object.keys(this._withTree || {})
1230
- .filter(k => this._withTree[k]?.__attribute)
1231
- })
1232
-
1233
- if (this._formatter) {
1234
- data = data.map(this._formatter)
1235
- }
1236
-
1237
- return { data, total }
1238
- }
1239
- }
1240
-
1241
- // =================================>
1242
- // ## Expand query (Eager loading)
1243
- // =================================>
1244
- ;(query as any).expand = function (entries: Array<string | Record<string, (q: any) => void>> = []) {
1245
- if (!Array.isArray(entries) || !entries.length) return this
1246
-
1247
- if (!this._withTree) this._withTree = {}
1248
-
1249
- const applyPath = (
1250
- path: string,
1251
- callback?: (q: any) => void
1252
- ) => {
1253
- const parts = path.split('.')
1254
- let cur = this._withTree
1255
- let node: any
1256
-
1257
- for (const part of parts) {
1258
- cur[part] ??= { __children: {} }
1259
- node = cur[part]
1260
- cur = node.__children
1261
- }
1262
-
1263
- if (callback && node) {
1264
- node.__callback = callback
1265
- }
1266
- }
1267
-
1268
- for (const entry of entries) {
1269
- if (typeof entry === 'string') {
1270
- applyPath(entry)
1271
- continue
1272
- }
1273
-
1274
- if (typeof entry === 'object') {
1275
- for (const [path, cb] of Object.entries(entry)) {
1276
- applyPath(path, cb)
1277
- }
1278
- }
1279
- }
1280
-
1281
- return this
1282
- }
1283
-
1284
-
1285
-
1286
-
1287
- // ==========================
1288
- // ## Where has query
1289
- // ==========================
1290
- if (!(query as any).whereHas) {
1291
- ;(query as any).whereHas = function (path: string, callback?: (q: any) => void) {
1292
- const Model = this.$model
1293
- if (!Model) return this
1294
-
1295
- const relations = path.split('.')
1296
-
1297
- whereHasSubquery(this, Model, relations, callback, false)
1298
-
1299
- return this
1300
- }
1301
- }
1302
-
1303
-
1304
- // ==========================
1305
- // ## Or where has query
1306
- // ==========================
1307
- if (!(query as any).orWhereHas) {
1308
- ;(query as any).orWhereHas = function (path: string, callback?: (q: any) => void) {
1309
- const Model = this.$model
1310
- if (!Model) return this
1311
-
1312
- this.orWhere(() => {
1313
- const relations = path.split('.')
1314
- whereHasSubquery(this, Model, relations, callback, false)
1315
- })
1316
-
1317
- return this
1318
- }
1319
- }
1320
-
1321
-
1322
- // ==========================
1323
- // ## Where doesn't have has query
1324
- // ==========================
1325
- if (!(query as any).whereDoesntHave) {
1326
- ;(query as any).whereDoesntHave = function (path: string, callback?: (q: any) => void) {
1327
- const Model = this.$model
1328
- if (!Model) return this
1329
-
1330
- const relations = path.split('.')
1331
-
1332
- whereHasSubquery(this, Model, relations, callback, true)
1333
-
1334
- return this
1335
- }
1336
- }
1337
-
1338
-
1339
- // ==========================
1340
- // ## Or where doesn't have has query
1341
- // ==========================
1342
- if (!(query as any).orWhereDoesntHave) {
1343
- ;(query as any).orWhereDoesntHave = function (path: string, callback?: (q: any) => void) {
1344
- const Model = this.$model
1345
- if (!Model) return this
1346
-
1347
- this.orWhere(() => {
1348
- const relations = path.split('.')
1349
- whereHasSubquery(this, Model, relations, callback, true)
1350
- })
1351
-
1352
- return this
1353
- }
1354
- }
1355
-
1356
-
1357
- // ==========================
1358
- // ## Scope query
1359
- // =========================
1360
- if (!(query as any).scope) {
1361
- ;(query as any).scope = function (name: string, ...args: any[]) {
1362
- const Model = this.$model
1363
- if (!Model) return this
1364
-
1365
- const meta = Model.scopes?.[name]
1366
- if (!meta) {
1367
- throw new Error(`Scope "${name}" not found`)
1368
- }
1369
-
1370
- if (meta.mode !== 'internal') {
1371
- throw new Error(`Scope "${name}" is global and cannot be called manually`)
1372
- }
1373
-
1374
- meta.fn.apply(this, args)
1375
-
1376
- return this
1377
- }
1378
- }
1379
-
1380
- if (!(query as any).withoutScope) {
1381
- ;(query as any).withoutScope = function (...names: string[]) {
1382
- for (const name of names) {
1383
- this._disabledScopes.add(name)
1384
- }
1385
- return this
1386
- }
1387
- }
1388
-
1389
-
1390
-
1391
- // ==========================
1392
- // ## with aggregate query
1393
- // ==========================
1394
- if (!(query as any).withAggregate) {
1395
- ;(query as any).withAggregate = function (expr: string, fn: 'count' | 'sum' | 'avg' | 'min' | 'max', column: string = '*', callback?: (q: any) => void) {
1396
- if (!this._withAggregates) this._withAggregates = []
1397
-
1398
- const [rel, aliasRaw] = expr.split(/\s+as\s+/i)
1399
-
1400
- this._withAggregates.push({
1401
- relation : rel.trim(),
1402
- alias : aliasRaw?.trim() || `${rel}_${fn}`,
1403
- fn,
1404
- column,
1405
- callback,
1406
- })
1407
-
1408
- return this
1409
- }
1410
- }
1411
-
1412
-
1413
- // ==========================
1414
- // ## Order by aggregate query
1415
- // ==========================
1416
- if (!(query as any).orderByAggregate) {
1417
- ;(query as any).orderByAggregate = function (expr: string, fn: 'count' | 'sum' | 'avg' | 'min' | 'max', column: string = '*', direction: 'asc' | 'desc' = 'asc', callback?: (q: any) => void) {
1418
- if (!this._orderByAggregates) this._orderByAggregates = []
1419
-
1420
- const [rel, aliasRaw] = expr.split(/\s+as\s+/i)
1421
-
1422
- this._orderByAggregates.push({
1423
- relation : rel.trim(),
1424
- alias : aliasRaw?.trim(),
1425
- fn,
1426
- column,
1427
- direction,
1428
- callback,
1429
- })
1430
-
1431
- return this
1432
- }
1433
- }
1434
-
1435
-
1436
-
1437
-
1438
- // =================================>
1439
- // ## Get option query
1440
- // =================================>
1441
- if (!(query as any).option) {
1442
- ;(query as any).option = async function (selectableOption?: string[]) {
1443
- applyGlobalScopes(this)
1444
-
1445
- const model = (this as any).$model;
1446
- const q = this.clone();
1447
- let defaultSelectable: string[] = [];
1448
-
1449
- if (model) {
1450
- defaultSelectable = model.selectable || [];
1451
- }
1452
-
1453
- let processedCols: string[] = [];
1454
-
1455
- if (!Array.isArray(selectableOption) || selectableOption.length === 0) {
1456
- const valueCol = defaultSelectable.length > 0 ? defaultSelectable[0] : model.primaryKey;
1457
- const labelCol = defaultSelectable.length > 0 ? defaultSelectable[1] : defaultSelectable[0] ?? model.primaryKey;
1458
-
1459
- processedCols = [`${valueCol} as value`, `${labelCol} as label`];
1460
- } else {
1461
- processedCols = selectableOption.map((col, index) => {
1462
- const hasAlias = /\s+as\s+/i.test(col);
1463
-
1464
- if (!hasAlias) {
1465
- if (index === 0) return `${col} as value`;
1466
- if (index === 1) return `${col} as label`;
1467
- }
1468
-
1469
- return col;
1470
- });
1471
- }
1472
-
1473
- q.clearSelect().select(processedCols);
1474
-
1475
- return await q;
1476
- };
1477
- }
1478
-
1479
-
1480
- // ==========================
1481
- // ## Paginate or option query
1482
- // ==========================
1483
- if (!(query as any).paginateOrOption) {
1484
- ;(query as any).paginateOrOption = async function (
1485
- page : number = 1,
1486
- limit : number = 10,
1487
- option ?: string | boolean,
1488
- selectableOption ?: string[],
1489
- ) {
1490
- const isOption = ["true", "1", "yes"].includes(String(option).toLowerCase());
1491
-
1492
- if (isOption) {
1493
- const data = await this.option(selectableOption);
1494
-
1495
- return { data, total: data.length };
1496
- }
1497
-
1498
- const result = await this.paginate(page, limit);
1499
-
1500
- return result;
1501
- };
1502
- }
1503
-
1504
-
1505
- // ==========================
1506
- // ## Resolve query
1507
- // ==========================
1508
- if (!(query as any).resolve) {
1509
- ;(query as any).resolve = async function (input: any = {}) {
1510
- const gq = input?.getQuery ? input.getQuery : input
1511
- const isOption = input?.headers?.["x-option"] || gq?.isOption || false
1512
-
1513
- this.
1514
- expand?.(gq.expand).
1515
- search?.(gq.search, {
1516
- includes: [],
1517
- searchable: gq.searchable
1518
- }).
1519
- filter?.(gq.filter).
1520
- selects?.({
1521
- includes: [],
1522
- selectable: gq.selectable
1523
- }).
1524
- sorts?.(gq.sort)
1525
-
1526
- if (isOption || gq.paginate) return await this.paginateOrOption?.(gq.page, gq.paginate, isOption, gq.selectableOption)
1527
-
1528
- const data = await this.query().get()
1529
-
1530
- return { data, total: data.length }
1531
- }
1532
- }
1533
-
1534
-
1535
- // ==========================
1536
- // ## format result query
1537
- // ==========================
1538
- if (!(query as any).format) {
1539
- ;(query as any).format = function (formatter: string | ((item: any) => any)) {
1540
- const Model = this.$model
1541
-
1542
- if (typeof formatter === 'string') {
1543
- const fn = Model?.formatters?.[formatter]
1544
- if (!fn) throw new Error(`Formatter "${formatter}" not found on model ${Model?.name}`)
1545
-
1546
- this._formatter = fn
1547
- return this
1548
- }
1549
-
1550
- if (typeof formatter === 'function') {
1551
- this._formatter = formatter
1552
- return this
1553
- }
1554
-
1555
- throw new Error('format() only accepts string or function')
1556
- }
1557
- }
1558
-
1559
-
1560
-
1561
- return query
1562
- }
1563
-
1564
-
1565
-
1566
-
1567
- // ?? Public model helpers
1568
-
1569
-
1570
- // =================================>
1571
- // ## Primary key decorator model helpers
1572
- // =================================>
1573
- export function PrimaryKey() {
1574
- return function (target: any, key: string) {
1575
- const ctor = target.constructor
1576
-
1577
- ctor.primaryKey = key
1578
-
1579
- if (!ctor[FIELD_META]) ctor[FIELD_META] = {}
1580
-
1581
- ctor[FIELD_META][key] = {
1582
- cast: 'number',
1583
- selectable: true,
1584
- }
1585
-
1586
- ctor[PRIMARY_KEY_META] = key
1587
- }
1588
- }
1589
-
1590
-
1591
- // =================================>
1592
- // ## Field decorator model helpers
1593
- // =================================>
1594
- export function Field(defs: string[]) {
1595
- return function (target: any, key: string) {
1596
- const ctor = target.constructor
1597
- if (!ctor[FIELD_META]) ctor[FIELD_META] = {}
1598
-
1599
- const meta: FieldMeta = {}
1600
-
1601
- const [first, ...rest] = defs
1602
- if (['string','number','boolean','date','json'].includes(first)) {
1603
- meta.cast = first as ModelCastType
1604
- } else {
1605
- rest.unshift(first)
1606
- }
1607
-
1608
- for (const flag of rest) {
1609
- if (flag === 'fillable') meta.fillable = true
1610
- if (flag === 'selectable') meta.selectable = true
1611
- if (flag === 'searchable') meta.searchable = true
1612
- }
1613
-
1614
- ctor[FIELD_META][key] = meta
1615
- }
1616
- }
1617
-
1618
-
1619
-
1620
- // =================================>
1621
- // ## Relation decorator model helpers
1622
- // =================================>
1623
- export function HasMany(
1624
- model : () => typeof Model,
1625
- options ?: {
1626
- foreignKey ?: string
1627
- localKey ?: string
1628
- callback ?: (q: any) => void }
1629
- ) {
1630
- return (target: any, key: string) => {
1631
- const parent = target.constructor
1632
- const parentKey = options?.localKey ?? 'id'
1633
- const foreignKey = options?.foreignKey ?? `${conversion.strSnake(parent.name)}_id`
1634
-
1635
- pushRelation(target, key, { type: 'hasMany', model, foreignKey, localKey: parentKey, callback: options?.callback })
1636
- }
1637
- }
1638
-
1639
-
1640
- export function HasOne(
1641
- model : () => typeof Model,
1642
- options ?: {
1643
- foreignKey ?: string
1644
- localKey ?: string
1645
- callback ?: (q: any) => void
1646
- }
1647
- ) {
1648
- return (target: any, key: string) => {
1649
- const parent = target.constructor
1650
- const parentKey = options?.localKey ?? 'id'
1651
- const foreignKey = options?.foreignKey ?? `${conversion.strSnake(parent.name)}_id`
1652
-
1653
- pushRelation(target, key, { type: 'hasOne', model, foreignKey, localKey: parentKey, callback: options?.callback })
1654
- }
1655
- }
1656
-
1657
-
1658
- export function BelongsTo(
1659
- model : () => typeof Model,
1660
- options ?: {
1661
- foreignKey ?: string
1662
- ownerKey ?: string
1663
- callback ?: (q: any) => void
1664
- }
1665
- ) {
1666
- return (target: any, key: string) => {
1667
- const ctor = target.constructor
1668
- const foreignKey = options?.foreignKey ?? `${conversion.strSnake(key)}_id`
1669
- const ownerKey = options?.ownerKey ?? 'id'
1670
-
1671
- pushRelation(target, key, { type: 'belongsTo', model, foreignKey, localKey: ownerKey, callback: options?.callback })
1672
-
1673
- if (!ctor[FIELD_META]) ctor[FIELD_META] = {}
1674
-
1675
- if (!ctor[FIELD_META][foreignKey]) {
1676
- ctor[FIELD_META][foreignKey] = {
1677
- cast: 'number',
1678
- fillable: true,
1679
- selectable: true,
1680
- }
1681
- }
1682
- }
1683
- }
1684
-
1685
-
1686
- // export function BelongsToMany(
1687
- // model : () => typeof Model,
1688
- // options ?: {
1689
- // pivotTable ?: string
1690
- // pivotLocal ?: string
1691
- // pivotForeign ?: string
1692
- // localKey ?: string
1693
- // callback ?: (q: any) => void
1694
- // }
1695
- // ) {
1696
- // return (target: any, key: string) => {
1697
- // const parent = target.constructor
1698
- // const parentName = conversion.strSnake(parent.name)
1699
- // const relatedName = conversion.strSnake(model().name)
1700
- // const pivotTable = options?.pivotTable ?? conversion.strPlural(`${parentName}_${relatedName}`)
1701
- // const localKey = options?.localKey ?? 'id'
1702
- // const pivotLocal = options?.pivotLocal ?? `${parentName}_id`
1703
- // const pivotForeign = options?.pivotForeign ?? `${relatedName}_id`
1704
-
1705
- // pushRelation(target, key, { type: 'belongsToMany', model, localKey, foreignKey: localKey, pivotTable, pivotLocal, pivotForeign, callback: options?.callback })
1706
- // }
1707
- // }
1708
- export function BelongsToMany(
1709
- model : () => typeof Model,
1710
- options ?: {
1711
- pivotTable ?: string
1712
- pivotLocal ?: string
1713
- pivotForeign ?: string
1714
- localKey ?: string
1715
- callback ?: (q: any) => void
1716
- }
1717
- ) {
1718
- return (target: any, key: string) => {
1719
- const localKey = options?.localKey ?? 'id'
1720
-
1721
- pushRelation(target, key, {
1722
- type: 'belongsToMany',
1723
- model,
1724
- localKey,
1725
- foreignKey: localKey,
1726
- pivotTable: options?.pivotTable,
1727
- pivotLocal: options?.pivotLocal,
1728
- pivotForeign: options?.pivotForeign,
1729
- callback: options?.callback,
1730
- })
1731
- }
1732
- }
1733
-
1734
-
1735
-
1736
- // =================================>
1737
- // ## Soft delete decorator model helpers
1738
- // =================================>
1739
- export function SoftDelete() {
1740
- return function (target: any, propertyKey: string) {
1741
- const ctor = target.constructor
1742
-
1743
- ctor[SOFT_DELETE_META] = { enabled: true, column: propertyKey }
1744
- }
1745
- }
1746
-
1747
-
1748
-
1749
- // =================================>
1750
- // ## Attribute decorator model helpers
1751
- // =================================>
1752
- export function Attribute() {
1753
- return function (
1754
- target: any,
1755
- key: string,
1756
- descriptor: PropertyDescriptor
1757
- ) {
1758
- const ctor = target.constructor
1759
- if (!ctor[ATTRIBUTE_META]) ctor[ATTRIBUTE_META] = {}
1760
- ctor[ATTRIBUTE_META][key] = descriptor.value
1761
- }
1762
- }
1763
-
1764
-
1765
-
1766
- // =================================>
1767
- // ## Formatter decorator model helpers
1768
- // =================================>
1769
- export function Formatter() {
1770
- return function (
1771
- target: any,
1772
- propertyKey: string,
1773
- descriptor: PropertyDescriptor
1774
- ) {
1775
- const ctor = target
1776
- if (!ctor[FORMATTER_META]) {
1777
- ctor[FORMATTER_META] = {}
1778
- }
1779
-
1780
- ctor[FORMATTER_META][propertyKey] = descriptor.value
1781
- }
1782
- }
1783
-
1784
-
1785
-
1786
- // =================================>
1787
- // ## Scope decorator model helpers
1788
- // =================================>
1789
- export function Scope(
1790
- mode: 'global' | 'internal' = 'internal'
1791
- ) {
1792
- return function (
1793
- target: any,
1794
- propertyKey: string,
1795
- descriptor: PropertyDescriptor
1796
- ) {
1797
- const ctor = target.constructor
1798
- if (!ctor[SCOPE_META]) ctor[SCOPE_META] = {}
1799
-
1800
- ctor[SCOPE_META][propertyKey] = {
1801
- fn: descriptor.value,
1802
- mode,
1803
- } satisfies ScopeType
1804
- }
1805
- }
1806
-
1807
-
1808
- // =================================>
1809
- // ## Hook
1810
- // =================================>
1811
- export function On(event: ModelHookEventType) {
1812
- return function (
1813
- target: any,
1814
- propertyKey: string,
1815
- descriptor: PropertyDescriptor
1816
- ) {
1817
- const ctor = target.constructor
1818
-
1819
- if (!ctor._hooks) {
1820
- ctor._hooks = {}
1821
- }
1822
-
1823
- if (!ctor._hooks[event]) {
1824
- ctor._hooks[event] = []
1825
- }
1826
-
1827
- ctor._hooks[event].push(
1828
- async ({ model, trx }: ModelHookContextType) => {
1829
- return descriptor.value.call(model, { trx })
1830
- }
1831
- )
1832
- }
1833
- }
1834
-
1835
-
1836
-
1837
-
1838
-
1839
- // ?? Private model helpers
1840
-
1841
-
1842
- // =================================>
1843
- // ## Global scope model helpers
1844
- // =================================>
1845
- function applyGlobalScopes(query: any) {
1846
- const Model = query.$model
1847
- if (!Model) return
1848
-
1849
- applyScopes(query)
1850
-
1851
- if (Model.isSoftDelete?.()) {
1852
- const col = Model.getDeletedAtColumn()
1853
- const mode = query._softDeleteScope ?? 'default'
1854
-
1855
- if (mode === 'with') return
1856
- if (mode === 'default') query.whereNull(col)
1857
- if (mode === 'only') query.whereNotNull(col)
1858
- }
1859
- }
1860
-
1861
-
1862
-
1863
- // =================================>
1864
- // ## Load relation model helpers
1865
- // =================================>
1866
- async function loadRelations(
1867
- rows : any[],
1868
- Model : any,
1869
- tree : Record<string, any>
1870
- ) {
1871
- if (!rows.length) return
1872
-
1873
- for (const [name, node] of Object.entries(tree)) {
1874
- const rel = Model.relations[name]
1875
- if (!rel) continue
1876
-
1877
- const desc = rel()
1878
- let related: any[] = []
1879
-
1880
- if (desc.type === 'belongsTo') {
1881
- related = await loadBelongsTo(rows, desc, name, node.__callback)
1882
- }
1883
-
1884
- if (desc.type === 'belongsToMany') {
1885
- related = await loadBelongsToMany(rows, desc, name, node.__callback)
1886
- }
1887
-
1888
- if (desc.type === 'hasMany') {
1889
- related = await loadHasMany(rows, desc, name, node.__callback)
1890
- }
1891
-
1892
- if (desc.type === 'hasOne') {
1893
- related = await loadHasOne(rows, desc, name, node.__callback)
1894
- }
1895
-
1896
- if (
1897
- node.__children &&
1898
- Object.keys(node.__children).length &&
1899
- related.length
1900
- ) {
1901
- await loadRelations(related, desc.model(), node.__children)
1902
- }
1903
- }
1904
- }
1905
-
1906
- async function loadBelongsTo(
1907
- rows: any[],
1908
- rel: any,
1909
- name: string,
1910
- callback?: (q: any) => void
1911
- ) {
1912
- const ids = [...new Set(rows.map(r => r[rel.foreignKey]).filter(Boolean))]
1913
- if (!ids.length) {
1914
- rows.forEach(r => (r[name] = null))
1915
-
1916
- return []
1917
- }
1918
-
1919
- const q = rel.model().query().whereIn(rel.localKey, ids)
1920
-
1921
- rel.callback?.(q)
1922
- callback?.(q)
1923
-
1924
- const related = rel.model().hydrate(await q)
1925
- const map = new Map(related.map((r: any) => [String(r[rel.localKey]), r]))
1926
-
1927
- rows.forEach(r => (r[name] = map.get(String(r[rel.foreignKey])) ?? null))
1928
-
1929
- return related
1930
- }
1931
-
1932
-
1933
- async function loadBelongsToMany(
1934
- rows: any[],
1935
- rel: any,
1936
- name: string,
1937
- callback?: (q: any) => void
1938
- ) {
1939
- const ids = rows.map(r => r[rel.localKey])
1940
- if (!ids.length) {
1941
- rows.forEach(r => (r[name] = []))
1942
- return []
1943
- }
1944
-
1945
- const Parent = rows[0].constructor
1946
- const Related = rel.model()
1947
- const parentName = conversion.strSnake(Parent.name)
1948
- const relatedName = conversion.strSnake(Related.name)
1949
-
1950
- const pivotTable = rel.pivotTable ?? conversion.strPlural(`${parentName}_${relatedName}`)
1951
- const pivotLocal = rel.pivotLocal ?? `${parentName}_id`
1952
- const pivotForeign = rel.pivotForeign ?? `${relatedName}_id`
1953
- const relatedTable = Related.getTable()
1954
-
1955
- const q = Related.query().join(pivotTable, `${relatedTable}.${Related.primaryKey}`, '=', `${pivotTable}.${pivotForeign}`).whereIn(`${pivotTable}.${pivotLocal}`, ids)
1956
-
1957
- rel.callback?.(q)
1958
- callback?.(q)
1959
-
1960
- const related = Related.hydrate(await q)
1961
-
1962
- const grouped: Record<string, any[]> = {}
1963
-
1964
- for (const r of related) {
1965
- const pivotValue = (r as any)[pivotLocal]
1966
- ;(grouped[pivotValue] ??= []).push(r)
1967
- }
1968
-
1969
- rows.forEach(r => { r[name] = grouped[r[rel.localKey]] ?? [] })
1970
-
1971
- return related
1972
- }
1973
-
1974
-
1975
-
1976
- async function loadHasMany(rows: any[], rel: any, name: string, callback?: (q: any) => void) {
1977
- const ids = rows.map(r => r[rel.localKey])
1978
- if (!ids.length) {
1979
- rows.forEach(r => (r[name] = []))
1980
-
1981
- return []
1982
- }
1983
-
1984
- const q = rel.model().query().whereIn(rel.foreignKey, ids)
1985
-
1986
- rel.callback?.(q)
1987
- callback?.(q)
1988
-
1989
- const related = rel.model().hydrate(await q)
1990
- const grouped: Record<string, any[]> = {}
1991
-
1992
- for (const r of related) {
1993
- ;(grouped[String(r[rel.foreignKey])] ??= []).push(r)
1994
- }
1995
-
1996
- rows.forEach(r => (r[name] = grouped[String(r[rel.localKey])] ?? []))
1997
-
1998
- return related
1999
- }
2000
-
2001
-
2002
- async function loadHasOne(
2003
- rows: any[],
2004
- rel: any,
2005
- name: string,
2006
- callback?: (q: any) => void
2007
- ) {
2008
- const ids = rows.map(r => r[rel.localKey])
2009
- if (!ids.length) {
2010
- rows.forEach(r => (r[name] = null))
2011
- return []
2012
- }
2013
-
2014
- const q = rel.model().query().whereIn(rel.foreignKey, ids)
2015
-
2016
- rel.callback?.(q)
2017
- callback?.(q)
2018
-
2019
- const related = rel.model().hydrate(await q)
2020
- const map = new Map(
2021
- related.map((r: any) => [String(r[rel.foreignKey]), r])
2022
- )
2023
-
2024
- rows.forEach(r => r[name] = map.get(String(r[rel.localKey])) ?? null)
2025
-
2026
- return related
2027
- }
2028
-
2029
-
2030
-
2031
- // =================================>
2032
- // ## Add relation model helpers
2033
- // =================================>
2034
- function pushRelation(
2035
- target : any,
2036
- key : string,
2037
- desc : ModelRelationDescriptor
2038
- ) {
2039
- const ctor = target.constructor
2040
- if (!ctor[RELATION_META]) ctor[RELATION_META] = {}
2041
-
2042
- ctor[RELATION_META][key] = () => desc
2043
- }
2044
-
2045
-
2046
-
2047
- // =================================>
2048
- // ## Where has model helpers
2049
- // =================================>
2050
- function whereHasSubquery(
2051
- parentQuery: any,
2052
- Model: any,
2053
- relations: string[],
2054
- callback?: (q: any) => void,
2055
- negate: boolean = false
2056
- ) {
2057
- const relation = relations[0]
2058
- const relDef = Model.relations?.[relation]
2059
- if (!relDef) return
2060
-
2061
- const desc = relDef()
2062
- const Related = desc.model()
2063
-
2064
- const parentTable = Model.getTable()
2065
- const relatedTable = Related.getTable()
2066
-
2067
- const method = negate ? 'whereNotExists' : 'whereExists'
2068
-
2069
- parentQuery[method](function (this: Knex.QueryBuilder) {
2070
- this.select(1).from(relatedTable)
2071
-
2072
- if (desc.type === 'hasMany' || desc.type === 'hasOne') {
2073
- this.whereRaw(`${relatedTable}.${desc.foreignKey} = ${parentTable}.${desc.localKey}`)
2074
- }
2075
-
2076
- if (desc.type === 'belongsTo') {
2077
- this.whereRaw(`${relatedTable}.${desc.localKey} = ${parentTable}.${desc.foreignKey}`)
2078
- }
2079
-
2080
- if (desc.type === 'belongsToMany') {
2081
- const pivot = desc.pivotTable
2082
-
2083
- this.join(pivot, `${pivot}.${desc.pivotForeign}`, '=', `${relatedTable}.${Related.primaryKey}`)
2084
-
2085
- this.whereRaw(`${pivot}.${desc.pivotLocal} = ${parentTable}.${desc.localKey}`)
2086
- }
2087
-
2088
- if (relations.length > 1) {
2089
- whereHasSubquery(this, Related, relations.slice(1), callback, negate)
2090
- return
2091
- }
2092
-
2093
- if (callback) {
2094
- const qb = Related.query().from(relatedTable)
2095
-
2096
- desc.callback?.(qb)
2097
- callback(qb)
2098
-
2099
- this.whereExists(qb)
2100
- }
2101
- })
2102
- }
2103
-
2104
-
2105
-
2106
- // =================================>
2107
- // ## Add aggregate model helpers
2108
- // =================================>
2109
- function applyWithAggregates(query: any) {
2110
- const Model = query.$model
2111
- if (!Model || !query._withAggregates?.length) return
2112
-
2113
- const parentTable = Model.getTable()
2114
-
2115
- for (const item of query._withAggregates) {
2116
- const relDef = Model.relations?.[item.relation]
2117
- if (!relDef) continue
2118
-
2119
- const desc = relDef()
2120
- const Related = desc.model()
2121
- const relatedTable = Related.getTable()
2122
-
2123
- const fn = item.fn as AggregateType
2124
- const sub = (db(Related.getTable()) as any)[fn](item.column)
2125
-
2126
- if (desc.type === 'hasMany' || desc.type === 'hasOne') {
2127
- sub.whereRaw(`${relatedTable}.${desc.foreignKey} = ${parentTable}.${desc.localKey}`)
2128
- }
2129
-
2130
- if (desc.type === 'belongsTo') {
2131
- sub.whereRaw(`${relatedTable}.${desc.localKey} = ${parentTable}.${desc.foreignKey}`)
2132
- }
2133
-
2134
- if (desc.type === 'belongsToMany') {
2135
- const pivot = desc.pivotTable
2136
-
2137
- sub.join(pivot, `${pivot}.${desc.pivotForeign}`, '=', `${relatedTable}.${Related.primaryKey}`)
2138
-
2139
- sub.whereRaw(`${pivot}.${desc.pivotLocal} = ${parentTable}.${desc.localKey}`)
2140
- }
2141
-
2142
- desc.callback?.(sub)
2143
- item.callback?.(sub)
2144
-
2145
- query.select(query.client.raw(`(${sub.toQuery()}) as ${item.alias}`))
2146
- }
2147
- }
2148
-
2149
-
2150
-
2151
- // =================================>
2152
- // ## Add order by aggregate model helpers
2153
- // =================================>
2154
- function applyOrderByAggregates(query: any) {
2155
- const Model = query.$model
2156
- if (!Model || !query._orderByAggregates?.length) return
2157
-
2158
- const parentTable = Model.getTable()
2159
-
2160
- for (const item of query._orderByAggregates) {
2161
- const relDef = Model.relations?.[item.relation]
2162
- if (!relDef) continue
2163
-
2164
- const desc = relDef()
2165
- const Related = desc.model()
2166
- const relatedTable = Related.getTable()
2167
-
2168
- const fn = item.fn as AggregateType
2169
- const sub = (db(Related.getTable()) as any)[fn](item.column)
2170
-
2171
- if (desc.type === 'hasMany') {
2172
- sub.whereRaw(
2173
- `${relatedTable}.${desc.foreignKey} = ${parentTable}.${desc.localKey}`
2174
- )
2175
- }
2176
-
2177
- if (desc.type === 'belongsTo') {
2178
- sub.whereRaw(
2179
- `${relatedTable}.${desc.localKey} = ${parentTable}.${desc.foreignKey}`
2180
- )
2181
- }
2182
-
2183
- if (Related.isSoftDelete?.()) {
2184
- sub.whereNull(Related.getDeletedAtColumn())
2185
- }
2186
-
2187
- desc.callback?.(sub)
2188
- item.callback?.(sub)
2189
-
2190
- query.orderByRaw(`(${sub.toQuery()}) ${item.direction}`)
2191
- }
2192
- }
2193
-
2194
-
2195
- // =================================>
2196
- // ## Add scope model helpers
2197
- // =================================>
2198
- function applyScopes(query: any) {
2199
- const Model = query.$model
2200
- if (!Model) return
2201
-
2202
- const scopes = Model.scopes ?? {}
2203
- const disabled = query._disabledScopes ?? new Set<string>()
2204
-
2205
- for (const [name, meta] of Object.entries(scopes) as [string, ScopeType][]) {
2206
- if (meta.mode !== 'global') continue
2207
- if (disabled.has(name)) continue
2208
-
2209
- meta.fn.call(Model, query)
2210
- }
1
+ import { status } from 'elysia'
2
+ import type { Knex } from 'knex'
3
+ import { conversion, db } from '@utils'
4
+
5
+
6
+
7
+ // ==========================
8
+ // ## Decorator Type
9
+ // ==========================
10
+ export const PRIMARY_KEY_META = Symbol('primary-key-meta')
11
+ export const FIELD_META = Symbol('field-meta')
12
+ export const RELATION_META = Symbol('relation-meta')
13
+ export const SOFT_DELETE_META = Symbol('soft-delete')
14
+ export const ATTRIBUTE_META = Symbol('attribute-meta')
15
+ export const FORMATTER_META = Symbol('formatter-meta')
16
+ export const SCOPE_META = Symbol('scope-meta')
17
+
18
+
19
+ // ==========================
20
+ // ## Field Decorator Type
21
+ // ==========================
22
+ export type FieldFlag = 'fillable' | 'selectable' | 'searchable' | 'hidden'
23
+ export type FieldMeta = {
24
+ cast ?: ModelCastType
25
+ fillable ?: boolean
26
+ selectable ?: boolean
27
+ searchable ?: boolean
28
+ hidden ?: boolean
29
+ }
30
+
31
+
32
+ // ==========================
33
+ // ## Payload Type
34
+ // ==========================
35
+ type NonFunctionKeys<T> = {
36
+ [K in keyof T]: T[K] extends Function ? never : K
37
+ }[keyof T]
38
+
39
+ type DataShape<T> = Pick<T, NonFunctionKeys<T>>
40
+
41
+ type ModelPayload<T> = Partial<DataShape<T>>
42
+
43
+
44
+ // ==========================
45
+ // ## Cast Type
46
+ // ==========================
47
+ export type ModelCastType = 'string' | 'number' | 'boolean' | 'date' | 'json'
48
+ const Casts = {
49
+ string : {
50
+ fromDB : (v: any) => v == null ? v : String(v),
51
+ toDB : (v: any) => v,
52
+ },
53
+ number : {
54
+ fromDB : (v: any) => v == null ? v : Number(v),
55
+ toDB : (v: any) => v,
56
+ },
57
+ boolean : {
58
+ fromDB : (v: any) => Boolean(v),
59
+ toDB : (v: any) => v ? 1 : 0,
60
+ },
61
+ date : {
62
+ fromDB : (v: any) => v ? new Date(v) : null,
63
+ toDB : (v: any) => v instanceof Date ? v.toISOString() : v,
64
+ },
65
+ json : {
66
+ fromDB: (v: any) => {
67
+ if (typeof v !== 'string') return v
68
+
69
+ try {
70
+ return JSON.parse(v)
71
+ } catch {
72
+ return null
73
+ }
74
+ },
75
+ toDB : (v: any) => JSON.stringify(v),
76
+ },
77
+ }
78
+
79
+
80
+ // ==========================
81
+ // ## Relation Type
82
+ // ==========================
83
+ export type ModelRelationType = 'hasMany' | 'hasOne' | 'belongsTo' | 'belongsToMany'
84
+ export type ModelRelationDescriptor = {
85
+ type : ModelRelationType
86
+ model : () => typeof Model
87
+ foreignKey : string
88
+ localKey : string
89
+ pivotTable ?: string
90
+ pivotLocal ?: string
91
+ pivotForeign ?: string
92
+ callback ?: (q: any) => void
93
+ }
94
+
95
+
96
+ // ==========================
97
+ // ## Hook type
98
+ // ==========================
99
+ export type ModelHookEventType = "before-create" | "after-create" | "before-update" | "after-update" | "before-delete" | "after-delete"
100
+ export type ModelHookFn = (ctx: ModelHookContextType) => any
101
+ export type ModelHookContextType<T extends Model = Model> = {
102
+ model : T
103
+ trx ?: any
104
+ snapshot ?: any
105
+ }
106
+
107
+
108
+ // ==========================
109
+ // ## Aggregate type
110
+ // ==========================
111
+ type AggregateType = 'count' | 'sum' | 'avg' | 'min' | 'max'
112
+
113
+
114
+ // ==========================
115
+ // ## Scope type
116
+ // ==========================
117
+ export type ScopeType = {
118
+ fn: Function
119
+ mode: 'global' | 'internal'
120
+ }
121
+
122
+
123
+ // ==========================
124
+ // ## Override knex Query builder interface
125
+ // ==========================
126
+ declare module 'knex' {
127
+ namespace Knex {
128
+ interface QueryBuilder<TRecord = any, TResult = any> {
129
+ $model?: any
130
+ _withTree?: Record<string, any>
131
+ _formatter?: ((item: any) => any) | null
132
+
133
+ _softDeleteScope?: 'default' | 'with' | 'only'
134
+
135
+ _withAggregates?: Array<{
136
+ relation: string
137
+ alias: string
138
+ fn: 'count' | 'sum' | 'avg' | 'min' | 'max'
139
+ column: string
140
+ callback?: (q: any) => void
141
+ }>
142
+
143
+ _orderByAggregates?: Array<{
144
+ relation: string
145
+ alias?: string
146
+ fn: 'count' | 'sum' | 'avg' | 'min' | 'max'
147
+ column: string
148
+ direction: 'asc' | 'desc'
149
+ callback?: (q: any) => void
150
+ }>
151
+ }
152
+ }
153
+ }
154
+
155
+ export interface ModelQueryBuilder<T extends Record<string, any> = Record<string, any> > extends Knex.QueryBuilder<T, T[]> {
156
+ _withTree?: Record<string, any>
157
+ _formatter?: ((item: T) => any) | null
158
+ _softDeleteScope?: 'default' | 'with' | 'only'
159
+
160
+ findOrNotFound(id: string | number): Promise<T>
161
+ firstOrNotFound(): Promise<T>
162
+
163
+ search(
164
+ keyword?: string,
165
+ options?: {
166
+ includes?: string[]
167
+ searchable?: string[]
168
+ }
169
+ ): this
170
+ filter(filters?: Record<string, string>): this
171
+ selects(options?: {
172
+ includes?: string[]
173
+ selectable?: string[]
174
+ }): this
175
+ sorts(sorts?: string[]): this
176
+
177
+ with(relation: string, callback?: any): this
178
+ expand(relations?: Array<string | Record<string, (q: any) => void>>): this
179
+
180
+ whereHas(
181
+ relation: string,
182
+ callback?: (q: ModelQueryBuilder<any>) => void
183
+ ): this
184
+ orWhereHas(
185
+ relation: string,
186
+ callback?: (q: ModelQueryBuilder<any>) => void
187
+ ): this
188
+ whereDoesntHave(
189
+ relation: string,
190
+ callback?: (q: ModelQueryBuilder<any>) => void
191
+ ): this
192
+ orWhereDoesntHave(
193
+ relation: string,
194
+ callback?: (q: ModelQueryBuilder<any>) => void
195
+ ): this
196
+
197
+ withAggregate(
198
+ expr: string,
199
+ fn: 'count' | 'sum' | 'avg' | 'min' | 'max',
200
+ column?: string,
201
+ callback?: (q: ModelQueryBuilder<any>) => void
202
+ ): this
203
+ orderByAggregate(
204
+ expr: string,
205
+ fn: 'count' | 'sum' | 'avg' | 'min' | 'max',
206
+ column?: string,
207
+ direction?: 'asc' | 'desc',
208
+ callback?: (q: ModelQueryBuilder<any>) => void
209
+ ): this
210
+
211
+
212
+ get(): Promise<T[]>
213
+ getFirst(): Promise<T>
214
+ paginate(
215
+ page?: number,
216
+ limit?: number
217
+ ): Promise<{ data: T[]; total: number }>
218
+ option(selectableOption?: string[]): Promise<Array<{ value: any; label: any }>>
219
+ paginateOrOption(
220
+ page?: number,
221
+ limit?: number,
222
+ option?: string | boolean,
223
+ selectableOption?: string[]
224
+ ): Promise<{ data: any[]; total: number }>
225
+ resolve(input?: any): Promise<{ data: T[]; total: number }>
226
+
227
+ format(formatter: string | ((item: T) => any)): this
228
+
229
+ withTrashed(): this
230
+ onlyTrashed(): this
231
+ }
232
+
233
+
234
+
235
+ export abstract class Model {
236
+ id!: number;
237
+ created_at!: Date;
238
+ updated_at!: Date;
239
+ deleted_at!: Date;
240
+
241
+ static table : string = ""
242
+ static primaryKey : string = 'id'
243
+ static softDelete : boolean = false
244
+ static deletedAtColumn : string = 'deleted_at'
245
+
246
+ protected _original : Record<string, any> = {}
247
+ protected _exists : boolean = false
248
+ protected _trx ?: Knex.Transaction = undefined
249
+
250
+ private static _hooks : Record<string, ModelHookFn[]> = {}
251
+ private _instanceHooks : Record<string, ModelHookFn[]> = {}
252
+ private _disabledHooks = new Set<string>()
253
+
254
+
255
+ // ==========================
256
+ // ## Constructor model
257
+ // ==========================
258
+ constructor(data: Record<string, any> = {}) {
259
+ Object.assign(this, data)
260
+
261
+ if ((this as any)[(this.constructor as typeof Model).primaryKey]) this._exists = true
262
+ }
263
+
264
+ protected static newInstance<T extends typeof Model>(this: T): InstanceType<T> & Model {
265
+ return new (this as any)()
266
+ }
267
+
268
+
269
+ // ==========================
270
+ // ## Field
271
+ // ==========================
272
+ static [FIELD_META] ?: Record<string, any>
273
+
274
+ static getDefaultFields(): Record<string, FieldMeta> {
275
+ const pk = (this as any)[PRIMARY_KEY_META] ?? this.primaryKey ?? 'id'
276
+
277
+ return {
278
+ [pk]: {
279
+ cast: 'number',
280
+ selectable: true,
281
+ },
282
+ created_at: { cast: 'date' },
283
+ updated_at: { cast: 'date' },
284
+ deleted_at: { cast: 'date' },
285
+ }
286
+ }
287
+
288
+ static get fields(): Record<string, FieldMeta> {
289
+ const defaults = this.getDefaultFields?.() ?? {};
290
+
291
+ return {...defaults,...(this[FIELD_META] ?? {})}
292
+ }
293
+
294
+ static get fillable() {
295
+ return Object.entries(this.fields).filter(([, v]) => v.fillable).map(([k]) => k)
296
+ }
297
+
298
+ static get selectable() {
299
+ return Object.entries(this.fields).filter(([, v]) => v.selectable).map(([k]) => k)
300
+ }
301
+
302
+ static get searchable() {
303
+ return Object.entries(this.fields).filter(([, v]) => v.searchable).map(([k]) => k)
304
+ }
305
+
306
+
307
+ // ==========================
308
+ // ## Table
309
+ // ==========================
310
+ static getTable(): string {
311
+ if (this.table) return this.table
312
+
313
+ return conversion.strPlural(conversion.strSnake(this.name))
314
+ }
315
+
316
+
317
+ // ==========================
318
+ // ## Primary key
319
+ // ==========================
320
+ static getPrimaryKey(): string {
321
+ return (this as any)[PRIMARY_KEY_META] ?? this.primaryKey ?? 'id'
322
+ }
323
+
324
+
325
+ // ==========================
326
+ // ## Relation
327
+ // ==========================
328
+ static [RELATION_META] ?: Record<string, any>
329
+
330
+ static get relations() {
331
+ return this[RELATION_META] ?? {}
332
+ }
333
+
334
+
335
+ // ==========================
336
+ // ## Attribute
337
+ // ==========================
338
+ static get attributes(): Record<string, Function> {
339
+ return (this as any)[ATTRIBUTE_META] ?? {}
340
+ }
341
+
342
+
343
+ // ==========================
344
+ // ## Formatter
345
+ // ==========================
346
+ static get formatters(): Record<string, Function> {
347
+ return (this as any)[FORMATTER_META] ?? {}
348
+ }
349
+
350
+
351
+ // ==========================
352
+ // ## Scope
353
+ // ==========================
354
+ static get scopes(): Record<string, ScopeType> {
355
+ return (this as any)[SCOPE_META] ?? {}
356
+ }
357
+
358
+
359
+ // ==========================
360
+ // ## Getter attribute
361
+ // ==========================
362
+ getOriginal(key?: string) {
363
+ if (!key) return { ...this._original }
364
+
365
+ return this._original[key]
366
+ }
367
+
368
+ getChanges() {
369
+ const changes: Record<string, any> = {}
370
+ for (const key in this._original) {
371
+ if ((this as any)[key] != this._original[key]) {
372
+ changes[key] = (this as any)[key]
373
+ }
374
+ }
375
+
376
+ return changes
377
+ }
378
+
379
+ getPrevious() {
380
+ const prev: Record<string, any> = {}
381
+ for (const key in this._original) {
382
+ if ((this as any)[key] != this._original[key]) {
383
+ prev[key] = this._original[key]
384
+ }
385
+ }
386
+
387
+ return prev
388
+ }
389
+
390
+
391
+ // ==========================
392
+ // ## Query builder
393
+ // ==========================
394
+ static query<T extends Record<string, any> = Record<string, any>>(trx?: Knex | Knex.Transaction): ModelQueryBuilder<T> {
395
+ const qb = (trx ?? db)(this.getTable())
396
+
397
+ return extendModelQuery(qb, this) as ModelQueryBuilder<T>
398
+ }
399
+
400
+
401
+ // ==========================
402
+ // ## Casting
403
+ // ==========================
404
+ castFromDB(row: Record<string, any>): this {
405
+ const ctor = this.constructor as typeof Model
406
+ const fields = ctor.fields
407
+
408
+ for (const [key, value] of Object.entries(row)) {
409
+ if (!fields[key]) continue
410
+ const meta = fields[key]
411
+ ;(this as any)[key] = meta.cast ? Casts[meta.cast].fromDB(value) : value
412
+ }
413
+
414
+ this._original = {}
415
+ for (const key of Object.keys(fields)) {
416
+ this._original[key] = (this as any)[key]
417
+ }
418
+
419
+ const attrs = ctor.attributes
420
+ for (const [name, fn] of Object.entries(attrs)) {
421
+ Object.defineProperty(this, name, {
422
+ get: () => fn.call(this),
423
+ enumerable: true,
424
+ })
425
+ }
426
+
427
+ this._exists = true
428
+ return this
429
+ }
430
+
431
+
432
+ castToDB() {
433
+ const fields = (this.constructor as typeof Model).fields
434
+ const data: Record<string, any> = {}
435
+
436
+ for (const [key, meta] of Object.entries(fields)) {
437
+ if (!meta.fillable) continue
438
+
439
+ const val = (this as any)[key]
440
+
441
+ if (val === undefined) continue
442
+
443
+ data [key] = meta.cast ? Casts[meta.cast].toDB(val): val
444
+ }
445
+
446
+ return data
447
+ }
448
+
449
+
450
+ // ==========================
451
+ // ## Dirty Field
452
+ // ==========================
453
+ getDirty(): Record<string, any> {
454
+ const dirty: any = {}
455
+ for (const key in this._original) {
456
+ if (Object.is((this as any)[key], this._original[key])) continue
457
+ dirty[key] = (this as any)[key]
458
+ }
459
+
460
+ return dirty
461
+ }
462
+
463
+
464
+ // ==========================
465
+ // ## Hydration
466
+ // ==========================
467
+ static hydrate<T extends typeof Model>(
468
+ this: T,
469
+ rows: any[] | null | undefined
470
+ ): InstanceType<T>[] {
471
+ if (!rows || !Array.isArray(rows)) return []
472
+
473
+ return rows.map(row => {
474
+ if (row instanceof this) return row as InstanceType<T>
475
+
476
+ const instance = this.newInstance()
477
+
478
+ return instance.castFromDB(row) as InstanceType<T>
479
+ })
480
+ }
481
+
482
+
483
+ // ==========================
484
+ // ## Fill model from payload
485
+ // ==========================
486
+ fill<T extends this>(this: T, payload: ModelPayload<T>): T {
487
+ const ctor = this.constructor as typeof Model
488
+ const fields = ctor.fields
489
+
490
+ for (const [key, value] of Object.entries(payload)) {
491
+ const meta = fields[key]
492
+ if (!meta || !meta.fillable) continue
493
+ ;(this as any)[key] = value
494
+ }
495
+
496
+ return this
497
+ }
498
+
499
+ // ==========================
500
+ // ## Create from fillable
501
+ // ==========================
502
+ static async create<T extends typeof Model>(
503
+ this: T,
504
+ payload: ModelPayload<InstanceType<T>>,
505
+ trx?: Knex.Transaction
506
+ ): Promise<InstanceType<T>> {
507
+
508
+ const instance = this.newInstance() as InstanceType<T>
509
+
510
+ instance.fill(payload)
511
+
512
+ const data = instance.castToDB()
513
+
514
+ await instance.runHook('before-create', { model: instance, trx })
515
+
516
+ const conn = trx ?? db
517
+ const [row] = await conn(this.getTable()).insert(data).returning('*')
518
+
519
+ await instance.runHook('after-create', { model: instance, trx })
520
+
521
+ return instance.castFromDB(row)
522
+ }
523
+
524
+
525
+
526
+
527
+ // ==========================
528
+ // ## update from fillable
529
+ // ==========================
530
+ static async update<T extends typeof Model>(
531
+ this: T,
532
+ payload: ModelPayload<InstanceType<T>>,
533
+ uniqueKeys: (keyof InstanceType<T> & string)[],
534
+ trx?: Knex.Transaction
535
+ ): Promise<InstanceType<T>> {
536
+
537
+ if (!uniqueKeys.length) {
538
+ throw new Error('updateByUnique requires uniqueKeys')
539
+ }
540
+
541
+ const instance = this.newInstance() as InstanceType<T>
542
+ instance.fill(payload)
543
+
544
+ const data = instance.castToDB()
545
+ const where: Record<string, any> = {}
546
+
547
+ for (const key of uniqueKeys) {
548
+ if ((payload as any)[key] === undefined) {
549
+ throw new Error(`Missing unique key: ${key}`)
550
+ }
551
+ where[key] = (payload as any)[key]
552
+ }
553
+
554
+ const conn = trx ?? db
555
+ const [row] = await conn(this.getTable()).where(where).update(data).returning('*')
556
+
557
+ if (!row) throw status(404, { message: 'Record not found' })
558
+
559
+ return instance.castFromDB(row)
560
+ }
561
+
562
+
563
+
564
+
565
+ // ==========================
566
+ // ## Update or insert from fillable
567
+ // ==========================
568
+ static async upsert<T extends typeof Model>(
569
+ this: T,
570
+ payload: ModelPayload<InstanceType<T>>,
571
+ uniqueKeys: (keyof InstanceType<T> & string)[],
572
+ trx?: Knex.Transaction
573
+ ): Promise<InstanceType<T>> {
574
+ if(uniqueKeys.length === 0) {
575
+ throw new Error('Upsert requires uniqueKeys')
576
+ }
577
+
578
+ const constraints: Record<string, any> = {}
579
+
580
+ for (const key of uniqueKeys) {
581
+ if ((payload as any)[key] === undefined) {
582
+ throw new Error(`Missing unique key: ${key}`)
583
+ }
584
+ constraints[key] = (payload as any)[key]
585
+ }
586
+
587
+ let row = await this.query().where(constraints).first();
588
+
589
+ if (!row) {
590
+ const instance = this.newInstance() as InstanceType<T>
591
+ instance.fill(payload)
592
+
593
+ const data = instance.castToDB()
594
+ const conn = trx ?? db
595
+ const [row] = await conn(this.getTable()).insert(data).returning('*')
596
+
597
+ return instance.castFromDB(row)
598
+ } else {
599
+ const instance = this.newInstance() as InstanceType<T>
600
+ instance.fill(payload)
601
+
602
+ const data = instance.castToDB()
603
+ const conn = trx ?? db
604
+ const [row] = await conn(this.getTable()).where(constraints).update(data).returning('*')
605
+
606
+ return instance.castFromDB(row)
607
+ }
608
+ }
609
+
610
+
611
+
612
+
613
+ // ==========================
614
+ // ## Save (insert / update)
615
+ // ==========================
616
+ async save() {
617
+ const model = this.constructor as typeof Model
618
+ const table = model.getTable()
619
+ const pk = model.primaryKey
620
+ const conn = this._trx ?? db
621
+ const hookCtx = { model: this, trx: this._trx }
622
+
623
+ if (!this._exists) {
624
+ const data = this.castToDB()
625
+
626
+ await this.runHook(`before-create` as ModelHookEventType, hookCtx)
627
+
628
+ const [row] = await conn(table).insert(data).returning('*')
629
+
630
+ this.castFromDB(row)
631
+
632
+ await this.runHook(`after-create` as ModelHookEventType, hookCtx)
633
+
634
+ return this
635
+ }
636
+
637
+ const dirty = this.getDirty()
638
+ if (Object.keys(dirty).length === 0) return this
639
+
640
+ await this.runHook(`before-update` as ModelHookEventType, hookCtx)
641
+
642
+ const fields = model.fields
643
+ const updateData: Record<string, any> = {}
644
+
645
+ for (const [key, meta] of Object.entries(fields)) {
646
+ if (!meta.fillable) continue
647
+ if (!(key in dirty)) continue
648
+
649
+ const val = dirty[key]
650
+ updateData[key] = meta.cast ? Casts[meta.cast].toDB(val) : val
651
+ }
652
+
653
+ if (Object.keys(updateData).length === 0) return this
654
+
655
+ await conn(table).where(pk, (this as any)[pk]).update(updateData)
656
+
657
+ Object.assign(this._original, dirty)
658
+
659
+ await this.runHook(`after-update` as ModelHookEventType, hookCtx)
660
+
661
+ return this
662
+ }
663
+
664
+
665
+
666
+ // ==========================
667
+ // ## Save with relation
668
+ // ==========================
669
+ async pump<T extends this>(
670
+ this : T,
671
+ payload : ModelPayload<T> | ModelPayload<T>[],
672
+ options : { trx?: Knex.Transaction } = {}
673
+ ) : Promise<T | T[]> {
674
+
675
+ const isRoot = !options.trx
676
+ const trx = options.trx ?? await db.transaction()
677
+
678
+ try {
679
+
680
+ // Handle Array (Bulk)
681
+ if (Array.isArray(payload)) {
682
+ const Ctor = this.constructor as typeof Model
683
+ const pkName = Ctor.primaryKey ?? 'id'
684
+ const results: T[] = []
685
+
686
+ for (const item of payload) {
687
+ let instance: T | null = null
688
+ const pkValue = (item as any)[pkName]
689
+
690
+ if (pkValue) {
691
+ const row = await Ctor.query(trx).where(pkName, pkValue).first()
692
+
693
+ if (row) {
694
+ instance = Ctor.hydrate([row])[0] as T
695
+ }
696
+ }
697
+
698
+ if (!instance) {
699
+ instance = Ctor.newInstance() as T
700
+ }
701
+
702
+ await instance.pump(item, { trx })
703
+ results.push(instance)
704
+ }
705
+
706
+ if (isRoot) await trx.commit()
707
+ return results
708
+ }
709
+
710
+
711
+ const ctor = this.constructor as typeof Model
712
+ const fields = ctor.fields
713
+ const relations = ctor.relations ?? {}
714
+
715
+ const flat: ModelPayload<T> = {}
716
+ const nested: Record<string, any> = {}
717
+
718
+ for (const key of Object.keys(payload) as Array<keyof DataShape<T>>) {
719
+ const value = (payload as any)[key]
720
+
721
+ if (fields[key as string]?.fillable) {
722
+ flat[key] = value
723
+ } else if (relations[key as string] && value !== null) {
724
+ nested[key as string] = value
725
+ }
726
+ }
727
+
728
+ this.fill(flat)
729
+ await this.useTransaction(trx).save()
730
+
731
+ for (const [name, value] of Object.entries(nested)) {
732
+ const relDef = relations[name]
733
+ if (!relDef) continue
734
+
735
+ const desc = relDef()
736
+ const Related = desc.model()
737
+
738
+ // ===== hasMany / belongsToMany =====
739
+ if (Array.isArray(value)) {
740
+ const existing = await Related.query(trx).where(desc.foreignKey, (this as any)[desc.localKey]).get()
741
+
742
+ const existingIds = existing.map((r: Model) => (r as any)[Related.primaryKey])
743
+ const incomingIds: any[] = []
744
+
745
+ for (const item of value) {
746
+ if ((item as any)[Related.primaryKey]) {
747
+ const child = await Related.query(trx).where(Related.primaryKey, (item as any)[Related.primaryKey]).getFirst()
748
+
749
+ if (child) {
750
+ await child.pump(item as any, { trx })
751
+ incomingIds.push(child[Related.primaryKey])
752
+ }
753
+ } else {
754
+ const child = Related.newInstance()
755
+ ;(child as any)[desc.foreignKey] = (this as any)[desc.localKey]
756
+ await child.pump(item as any, { trx })
757
+ incomingIds.push(child[Related.primaryKey])
758
+ }
759
+ }
760
+
761
+ const toDelete = existingIds.filter((id: number) => !incomingIds.includes(id))
762
+ if (toDelete.length) {
763
+ await Related.query(trx).whereIn(Related.primaryKey, toDelete).delete()
764
+ }
765
+
766
+ continue
767
+ }
768
+
769
+ const child = Related.newInstance()
770
+ if (desc.type !== 'belongsTo') {
771
+ ;(child as any)[desc.foreignKey] = (this as any)[desc.localKey]
772
+ }
773
+
774
+ await child.pump(value as any, { trx })
775
+ }
776
+
777
+ if (isRoot) await trx.commit()
778
+ return this
779
+
780
+ } catch (err) {
781
+ if (isRoot) await trx.rollback()
782
+ throw err
783
+ }
784
+ }
785
+
786
+
787
+
788
+ // ==========================
789
+ // ## Soft Delete
790
+ // ==========================
791
+ static getSoftDeleteConfig() {
792
+ return (this as any)[SOFT_DELETE_META] ?? null
793
+ }
794
+
795
+ static isSoftDelete(): boolean {
796
+ return !!this.getSoftDeleteConfig()
797
+ }
798
+
799
+ static getDeletedAtColumn(): string | null {
800
+ return this.getSoftDeleteConfig()?.column ?? null
801
+ }
802
+
803
+
804
+ // ==========================
805
+ // ## Delete (with or without soft delete)
806
+ // ==========================
807
+ async delete(): Promise<Record<string, any> | null> {
808
+ const model = this.constructor as typeof Model
809
+ const soft = model.getSoftDeleteConfig?.()
810
+
811
+ if (!this._exists) return null
812
+
813
+ if (!soft) return await this.forceDelete()
814
+
815
+ const trx = this._trx ?? await db.transaction()
816
+
817
+ try {
818
+ await this.runHook(`before-delete` as ModelHookEventType, { model: this, trx })
819
+
820
+ await trx(model.getTable()).where(model.primaryKey, (this as any)[model.primaryKey]).update({ [soft.column]: new Date() })
821
+
822
+ await this.runHook(`after-delete` as ModelHookEventType, { model: this, trx })
823
+
824
+ if (!this._trx) await trx.commit()
825
+
826
+ const snapshot = await (this._trx ?? db)(model.getTable()).where(model.primaryKey, (this as any)[model.primaryKey]).first()
827
+
828
+ this._exists = false
829
+
830
+ return snapshot
831
+ } catch (err) {
832
+ if (!this._trx) await trx.rollback()
833
+
834
+ throw err
835
+ }
836
+ }
837
+
838
+
839
+ // ==========================
840
+ // ## Delete without soft delete
841
+ // ==========================
842
+ async forceDelete() {
843
+ const model = this.constructor as typeof Model
844
+ const pk = model.primaryKey
845
+
846
+ if (!this._exists) return
847
+
848
+ const snapshot = await (this._trx ?? db)(model.getTable()).where(pk, (this as any)[pk]).first()
849
+
850
+ if (!snapshot) return null
851
+
852
+ await (this._trx ?? db)(model.getTable()).where(pk, (this as any)[pk]).delete()
853
+
854
+ this._exists = false
855
+
856
+ return snapshot
857
+ }
858
+
859
+ async restore(): Promise<this> {
860
+ const model = this.constructor as typeof Model
861
+ const soft = model.getSoftDeleteConfig?.()
862
+
863
+ if (!soft) return this
864
+
865
+ await this.runHook(`before-update` as ModelHookEventType, { model: this, trx: this._trx })
866
+
867
+ await (this._trx ?? db)(model.getTable()).where(model.primaryKey, (this as any)[model.primaryKey]).update({ [soft.column]: null })
868
+
869
+ await this.runHook(`after-update` as ModelHookEventType, { model: this, trx: this._trx })
870
+
871
+ this._exists = true
872
+
873
+ return this
874
+ }
875
+
876
+
877
+
878
+ // ==========================
879
+ // ## Model Hook
880
+ // ==========================
881
+ on<T extends this>(event: ModelHookEventType, fn: (ctx: ModelHookContextType<T>) => any) {
882
+ if (!this._instanceHooks[event]) this._instanceHooks[event] = []
883
+
884
+ this._instanceHooks[event].push(fn as ModelHookFn)
885
+
886
+ return this
887
+ }
888
+
889
+ off(event: ModelHookEventType) {
890
+ this._disabledHooks.add(event)
891
+
892
+ return this
893
+ }
894
+
895
+ protected async runHook(event: ModelHookEventType, ctx: ModelHookContextType) {
896
+ if (this._disabledHooks.has(event)) return
897
+
898
+ const ctor = this.constructor as typeof Model
899
+
900
+ const globals = ctor._hooks?.[event] ?? []
901
+ for (const fn of globals) await fn(ctx)
902
+
903
+ const locals = this._instanceHooks[event] ?? []
904
+ for (const fn of locals) await fn(ctx)
905
+ }
906
+
907
+
908
+
909
+ // ==========================
910
+ // ## To response data
911
+ // ==========================
912
+ toJSON() {
913
+ const data: Record<string, any> = {}
914
+ const ctor = this.constructor as typeof Model
915
+ const fields = ctor.fields ?? {}
916
+ const relations = ctor.relations ?? {}
917
+ const attributes = ctor.attributes ?? {}
918
+
919
+ for (const key of Object.keys(fields)) {
920
+ if ((fields[key] as any)?.hidden) continue
921
+ data[key] = (this as any)[key]
922
+ }
923
+
924
+ const expanded = (this as any).__expandedAttributes ?? []
925
+ for (const key of expanded) {
926
+ if (attributes[key]) {
927
+ data[key] = (this as any)[key]
928
+ }
929
+ }
930
+
931
+ for (const key of Object.keys(relations)) {
932
+ if ((this as any)[key] !== undefined) {
933
+ data[key] = (this as any)[key]
934
+ }
935
+ }
936
+
937
+ return data
938
+ }
939
+
940
+
941
+
942
+
943
+ // ==========================
944
+ // ## Transaction binding
945
+ // ==========================
946
+ useTransaction(trx: Knex.Transaction) {
947
+ this._trx = trx
948
+
949
+ return this
950
+ }
951
+ }
952
+
953
+
954
+
955
+ // ==========================
956
+ // ## Model query builder (extend from knex query builder)
957
+ // ==========================
958
+ export function extendModelQuery(
959
+ query: Knex.QueryBuilder,
960
+ Model: any
961
+ ) {
962
+ ;(query as any).$model = Model
963
+ ;(query as any)._withTree = {}
964
+ ;(query as any)._softDeleteScope = 'default' //? default | with | only
965
+ ;(query as any)._formatter = null
966
+ ;(query as any)._disabledScopes = new Set<string>()
967
+
968
+
969
+ // =========================
970
+ // ## Soft delete query
971
+ // =========================
972
+ ;(query as any).withTrashed = function () {
973
+ this._softDeleteScope = 'with'
974
+ return this
975
+ }
976
+
977
+ ;(query as any).onlyTrashed = function () {
978
+ this._softDeleteScope = 'only'
979
+ return this
980
+ }
981
+
982
+
983
+ // ==========================
984
+ // ## find or not found
985
+ // ==========================
986
+ if (!(query as any).findOrNotFound) {
987
+ ;(query as any).findOrNotFound = async function (id: any) {
988
+ applyGlobalScopes(this)
989
+
990
+ const pk = Model.primaryKey ?? 'id'
991
+ const row = await this.where(pk, id).first()
992
+
993
+ if (!row) throw status(404, { message: "Error: Record not found!" });
994
+
995
+ return Model.hydrate([row])[0]
996
+ }
997
+ }
998
+
999
+
1000
+ // ==========================
1001
+ // ## first or not found
1002
+ // ==========================
1003
+ if (!(query as any).firstOrNotFound) {
1004
+ ;(query as any).firstOrNotFound = async function () {
1005
+ applyGlobalScopes(this)
1006
+
1007
+ const row = await this.first()
1008
+
1009
+ if (!row) throw status(404, { message: "Error: Record not found!" });
1010
+
1011
+ return Model.hydrate([row])[0]
1012
+ }
1013
+ }
1014
+
1015
+
1016
+ // ==========================
1017
+ // ## find
1018
+ // ==========================
1019
+ if (!(query as any).find) {
1020
+ ;(query as any).find = async function (id: any) {
1021
+ applyGlobalScopes(this)
1022
+
1023
+ const pk = Model.primaryKey ?? 'id'
1024
+ const row = await this.where(pk, id).first()
1025
+
1026
+ if (!row) return null;
1027
+
1028
+ return Model.hydrate([row])[0]
1029
+ }
1030
+ }
1031
+
1032
+
1033
+ // ==========================
1034
+ // ## Search query
1035
+ // ==========================
1036
+ if (!(query as any).search) {
1037
+ ;(query as any).search = function (
1038
+ keyword : string,
1039
+ { includes = [], searchable = [] } : { includes?: string[], searchable?: string[] } = {}
1040
+ ) {
1041
+ const model = (this as any).$model
1042
+ if (!model) return this
1043
+
1044
+ const defaultSearchable = Model.searchable || []
1045
+ const mergedSearchable = searchable?.length ? searchable : [...defaultSearchable, ...includes]
1046
+
1047
+ if (!keyword || !mergedSearchable.length) return this
1048
+
1049
+
1050
+ this.where((q: any) => {
1051
+ mergedSearchable.forEach((column) => {
1052
+ if (column.includes(".")) {
1053
+ const [relation, col] = column.split(".")
1054
+ q.orWhereHas(relation, (rel: any) => rel.where(col, "ILIKE", `%${keyword}%`))
1055
+ } else {
1056
+ q.orWhere(column, "ILIKE", `%${keyword}%`)
1057
+ }
1058
+ })
1059
+ })
1060
+
1061
+ return this
1062
+ }
1063
+ }
1064
+
1065
+
1066
+ // ==========================
1067
+ // ## Filer query
1068
+ // ==========================
1069
+ if (!(query as any).filter) {
1070
+ ;(query as any).filter = function (filters?: Record<string, string>) {
1071
+ if (!filters) return this
1072
+
1073
+ for (const [field, filter] of Object.entries(filters)) {
1074
+ const [type, value] = filter.split(":")
1075
+ if (!type || value === undefined) continue
1076
+
1077
+ const applyWhere = (q: any, col: string) => {
1078
+ switch (type) {
1079
+ case "li": q.where(col, "ILIKE", `%${value}%`); break
1080
+ case "eq": q.where(col, value); break
1081
+ case "ne": q.where(col, "!=", value); break
1082
+ case "in": q.whereIn(col, value.split(",")); break
1083
+ case "ni": q.whereNotIn(col, value.split(",")); break
1084
+ case "bw": {
1085
+ const [min, max] = value.split(",")
1086
+ q.whereBetween(col, [min, max])
1087
+ break
1088
+ }
1089
+ }
1090
+ }
1091
+
1092
+ if (field.includes(".")) {
1093
+ const [relation, col] = field.split(".")
1094
+ this.whereHas(relation, (q: any) => applyWhere(q, col))
1095
+ } else {
1096
+ applyWhere(this, field)
1097
+ }
1098
+ }
1099
+
1100
+ return this
1101
+ }
1102
+ }
1103
+
1104
+
1105
+ // ==========================
1106
+ // ## Select query
1107
+ // ==========================
1108
+ if (!(query as any).selects) {
1109
+ ;(query as any).selects = function ({ includes = [], selectable = [] } : { includes?: string[], selectable?: string[] } = {}) {
1110
+ const model = (this as any).$model
1111
+ if (!model) return this
1112
+
1113
+ const defaultSelectable = Model.selectable || ["*"]
1114
+
1115
+ this.select(selectable?.length ? selectable : [...defaultSelectable, ...includes])
1116
+
1117
+ return this
1118
+ }
1119
+ }
1120
+
1121
+
1122
+ // ==========================
1123
+ // ## Sort query
1124
+ // ==========================
1125
+ if (!(query as any).sorts) {
1126
+ ;(query as any).sorts = function (sorts?: string[]) {
1127
+ if (!Array.isArray(sorts) || sorts.length === 0) return this;
1128
+
1129
+ sorts.forEach((sortExpr) => {
1130
+ if (typeof sortExpr !== "string") return;
1131
+
1132
+ const parts = sortExpr.trim().split(/\s+/);
1133
+ const column = parts[0];
1134
+ const direction = (parts[1] || "asc").toLowerCase();
1135
+
1136
+ if (!["asc", "desc"].includes(direction)) {
1137
+ this.orderBy(column, "asc");
1138
+ } else {
1139
+ this.orderBy(column, direction);
1140
+ }
1141
+ });
1142
+
1143
+ return this;
1144
+ };
1145
+ }
1146
+
1147
+
1148
+
1149
+ // ==========================
1150
+ // ## Get query
1151
+ // ==========================
1152
+ ;(query as any).get = async function () {
1153
+ applyGlobalScopes(this)
1154
+ applyWithAggregates(this)
1155
+ applyOrderByAggregates(this)
1156
+
1157
+ const rows = await this
1158
+ let result = this.$model.hydrate(rows)
1159
+
1160
+ if (this._withTree && Object.keys(this._withTree).length) {
1161
+ await loadRelations(result, this.$model, this._withTree)
1162
+ }
1163
+
1164
+ result.forEach((item: any) => {
1165
+ item.__expandedAttributes = Object.keys(this._withTree || {})
1166
+ .filter(k => this._withTree[k]?.__attribute)
1167
+ })
1168
+
1169
+ if (this._formatter) {
1170
+ result = result.map(this._formatter)
1171
+ }
1172
+
1173
+ return result
1174
+ }
1175
+
1176
+
1177
+ // ==========================
1178
+ // ## First query
1179
+ // ==========================
1180
+ ;(query as any).getFirst = async function () {
1181
+ applyGlobalScopes(this)
1182
+ applyWithAggregates(this)
1183
+ applyOrderByAggregates(this)
1184
+
1185
+ const rows = await this.limit(1)
1186
+ let result = this.$model.hydrate(rows)
1187
+
1188
+ if (this._withTree && Object.keys(this._withTree).length) {
1189
+ await loadRelations(result, this.$model, this._withTree)
1190
+ }
1191
+
1192
+ if(!result.at(0)) return null
1193
+
1194
+ result.at(0).__expandedAttributes = Object.keys(this._withTree || {}).filter(k => this._withTree[k]?.__attribute)
1195
+
1196
+ if (this._formatter) {
1197
+ result = result.map(this._formatter).at(0)
1198
+ }
1199
+
1200
+ return result.at(0)
1201
+ }
1202
+
1203
+
1204
+
1205
+ // ==========================
1206
+ // ## Paginate query
1207
+ // ==========================
1208
+ if (!(query as any).paginate) {
1209
+ ;(query as any).paginate = async function (page = 1, limit = 10) {
1210
+ applyGlobalScopes(this)
1211
+ applyWithAggregates(this)
1212
+ applyOrderByAggregates(this)
1213
+
1214
+ const offset = (page - 1) * limit
1215
+
1216
+ const raw = await this.clone().limit(limit).offset(offset)
1217
+ let data = Model.hydrate(raw)
1218
+
1219
+ const [{ count }] = await this.clone().clearSelect().clearOrder().count('* as count')
1220
+ const total = Number(count)
1221
+
1222
+ if(!total) return { data: [], total: 0 }
1223
+
1224
+ if (this._withTree && Object.keys(this._withTree).length) {
1225
+ await loadRelations(data, this.$model, this._withTree)
1226
+ }
1227
+
1228
+ data.forEach((item: any) => {
1229
+ item.__expandedAttributes = Object.keys(this._withTree || {})
1230
+ .filter(k => this._withTree[k]?.__attribute)
1231
+ })
1232
+
1233
+ if (this._formatter) {
1234
+ data = data.map(this._formatter)
1235
+ }
1236
+
1237
+ return { data, total }
1238
+ }
1239
+ }
1240
+
1241
+ // =================================>
1242
+ // ## Expand query (Eager loading)
1243
+ // =================================>
1244
+ ;(query as any).expand = function (entries: Array<string | Record<string, (q: any) => void>> = []) {
1245
+ if (!Array.isArray(entries) || !entries.length) return this
1246
+
1247
+ if (!this._withTree) this._withTree = {}
1248
+
1249
+ const applyPath = (
1250
+ path: string,
1251
+ callback?: (q: any) => void
1252
+ ) => {
1253
+ const parts = path.split('.')
1254
+ let cur = this._withTree
1255
+ let node: any
1256
+
1257
+ for (const part of parts) {
1258
+ cur[part] ??= { __children: {} }
1259
+ node = cur[part]
1260
+ cur = node.__children
1261
+ }
1262
+
1263
+ if (callback && node) {
1264
+ node.__callback = callback
1265
+ }
1266
+ }
1267
+
1268
+ for (const entry of entries) {
1269
+ if (typeof entry === 'string') {
1270
+ applyPath(entry)
1271
+ continue
1272
+ }
1273
+
1274
+ if (typeof entry === 'object') {
1275
+ for (const [path, cb] of Object.entries(entry)) {
1276
+ applyPath(path, cb)
1277
+ }
1278
+ }
1279
+ }
1280
+
1281
+ return this
1282
+ }
1283
+
1284
+
1285
+
1286
+
1287
+ // ==========================
1288
+ // ## Where has query
1289
+ // ==========================
1290
+ if (!(query as any).whereHas) {
1291
+ ;(query as any).whereHas = function (path: string, callback?: (q: any) => void) {
1292
+ const Model = this.$model
1293
+ if (!Model) return this
1294
+
1295
+ const relations = path.split('.')
1296
+
1297
+ whereHasSubquery(this, Model, relations, callback, false)
1298
+
1299
+ return this
1300
+ }
1301
+ }
1302
+
1303
+
1304
+ // ==========================
1305
+ // ## Or where has query
1306
+ // ==========================
1307
+ if (!(query as any).orWhereHas) {
1308
+ ;(query as any).orWhereHas = function (path: string, callback?: (q: any) => void) {
1309
+ const Model = this.$model
1310
+ if (!Model) return this
1311
+
1312
+ this.orWhere(() => {
1313
+ const relations = path.split('.')
1314
+ whereHasSubquery(this, Model, relations, callback, false)
1315
+ })
1316
+
1317
+ return this
1318
+ }
1319
+ }
1320
+
1321
+
1322
+ // ==========================
1323
+ // ## Where doesn't have has query
1324
+ // ==========================
1325
+ if (!(query as any).whereDoesntHave) {
1326
+ ;(query as any).whereDoesntHave = function (path: string, callback?: (q: any) => void) {
1327
+ const Model = this.$model
1328
+ if (!Model) return this
1329
+
1330
+ const relations = path.split('.')
1331
+
1332
+ whereHasSubquery(this, Model, relations, callback, true)
1333
+
1334
+ return this
1335
+ }
1336
+ }
1337
+
1338
+
1339
+ // ==========================
1340
+ // ## Or where doesn't have has query
1341
+ // ==========================
1342
+ if (!(query as any).orWhereDoesntHave) {
1343
+ ;(query as any).orWhereDoesntHave = function (path: string, callback?: (q: any) => void) {
1344
+ const Model = this.$model
1345
+ if (!Model) return this
1346
+
1347
+ this.orWhere(() => {
1348
+ const relations = path.split('.')
1349
+ whereHasSubquery(this, Model, relations, callback, true)
1350
+ })
1351
+
1352
+ return this
1353
+ }
1354
+ }
1355
+
1356
+
1357
+ // ==========================
1358
+ // ## Scope query
1359
+ // =========================
1360
+ if (!(query as any).scope) {
1361
+ ;(query as any).scope = function (name: string, ...args: any[]) {
1362
+ const Model = this.$model
1363
+ if (!Model) return this
1364
+
1365
+ const meta = Model.scopes?.[name]
1366
+ if (!meta) {
1367
+ throw new Error(`Scope "${name}" not found`)
1368
+ }
1369
+
1370
+ if (meta.mode !== 'internal') {
1371
+ throw new Error(`Scope "${name}" is global and cannot be called manually`)
1372
+ }
1373
+
1374
+ meta.fn.apply(this, args)
1375
+
1376
+ return this
1377
+ }
1378
+ }
1379
+
1380
+ if (!(query as any).withoutScope) {
1381
+ ;(query as any).withoutScope = function (...names: string[]) {
1382
+ for (const name of names) {
1383
+ this._disabledScopes.add(name)
1384
+ }
1385
+ return this
1386
+ }
1387
+ }
1388
+
1389
+
1390
+
1391
+ // ==========================
1392
+ // ## with aggregate query
1393
+ // ==========================
1394
+ if (!(query as any).withAggregate) {
1395
+ ;(query as any).withAggregate = function (expr: string, fn: 'count' | 'sum' | 'avg' | 'min' | 'max', column: string = '*', callback?: (q: any) => void) {
1396
+ if (!this._withAggregates) this._withAggregates = []
1397
+
1398
+ const [rel, aliasRaw] = expr.split(/\s+as\s+/i)
1399
+
1400
+ this._withAggregates.push({
1401
+ relation : rel.trim(),
1402
+ alias : aliasRaw?.trim() || `${rel}_${fn}`,
1403
+ fn,
1404
+ column,
1405
+ callback,
1406
+ })
1407
+
1408
+ return this
1409
+ }
1410
+ }
1411
+
1412
+
1413
+ // ==========================
1414
+ // ## Order by aggregate query
1415
+ // ==========================
1416
+ if (!(query as any).orderByAggregate) {
1417
+ ;(query as any).orderByAggregate = function (expr: string, fn: 'count' | 'sum' | 'avg' | 'min' | 'max', column: string = '*', direction: 'asc' | 'desc' = 'asc', callback?: (q: any) => void) {
1418
+ if (!this._orderByAggregates) this._orderByAggregates = []
1419
+
1420
+ const [rel, aliasRaw] = expr.split(/\s+as\s+/i)
1421
+
1422
+ this._orderByAggregates.push({
1423
+ relation : rel.trim(),
1424
+ alias : aliasRaw?.trim(),
1425
+ fn,
1426
+ column,
1427
+ direction,
1428
+ callback,
1429
+ })
1430
+
1431
+ return this
1432
+ }
1433
+ }
1434
+
1435
+
1436
+
1437
+
1438
+ // =================================>
1439
+ // ## Get option query
1440
+ // =================================>
1441
+ if (!(query as any).option) {
1442
+ ;(query as any).option = async function (selectableOption?: string[]) {
1443
+ applyGlobalScopes(this)
1444
+
1445
+ const model = (this as any).$model;
1446
+ const q = this.clone();
1447
+ let defaultSelectable: string[] = [];
1448
+
1449
+ if (model) {
1450
+ defaultSelectable = model.selectable || [];
1451
+ }
1452
+
1453
+ let processedCols: string[] = [];
1454
+
1455
+ if (!Array.isArray(selectableOption) || selectableOption.length === 0) {
1456
+ const valueCol = defaultSelectable.length > 0 ? defaultSelectable[0] : model.primaryKey;
1457
+ const labelCol = defaultSelectable.length > 0 ? defaultSelectable[1] : defaultSelectable[0] ?? model.primaryKey;
1458
+
1459
+ processedCols = [`${valueCol} as value`, `${labelCol} as label`];
1460
+ } else {
1461
+ processedCols = selectableOption.map((col, index) => {
1462
+ const hasAlias = /\s+as\s+/i.test(col);
1463
+
1464
+ if (!hasAlias) {
1465
+ if (index === 0) return `${col} as value`;
1466
+ if (index === 1) return `${col} as label`;
1467
+ }
1468
+
1469
+ return col;
1470
+ });
1471
+ }
1472
+
1473
+ q.clearSelect().select(processedCols);
1474
+
1475
+ return await q;
1476
+ };
1477
+ }
1478
+
1479
+
1480
+ // ==========================
1481
+ // ## Paginate or option query
1482
+ // ==========================
1483
+ if (!(query as any).paginateOrOption) {
1484
+ ;(query as any).paginateOrOption = async function (
1485
+ page : number = 1,
1486
+ limit : number = 10,
1487
+ option ?: string | boolean,
1488
+ selectableOption ?: string[],
1489
+ ) {
1490
+ const isOption = ["true", "1", "yes"].includes(String(option).toLowerCase());
1491
+
1492
+ if (isOption) {
1493
+ const data = await this.option(selectableOption);
1494
+
1495
+ return { data, total: data.length };
1496
+ }
1497
+
1498
+ const result = await this.paginate(page, limit);
1499
+
1500
+ return result;
1501
+ };
1502
+ }
1503
+
1504
+
1505
+ // ==========================
1506
+ // ## Resolve query
1507
+ // ==========================
1508
+ if (!(query as any).resolve) {
1509
+ ;(query as any).resolve = async function (input: any = {}) {
1510
+ const gq = input?.getQuery ? input.getQuery : input
1511
+ const isOption = input?.headers?.["x-option"] || gq?.isOption || false
1512
+
1513
+ this.
1514
+ expand?.(gq.expand).
1515
+ search?.(gq.search, {
1516
+ includes: [],
1517
+ searchable: gq.searchable
1518
+ }).
1519
+ filter?.(gq.filter).
1520
+ selects?.({
1521
+ includes: [],
1522
+ selectable: gq.selectable
1523
+ }).
1524
+ sorts?.(gq.sort)
1525
+
1526
+ if (isOption || gq.paginate) return await this.paginateOrOption?.(gq.page, gq.paginate, isOption, gq.selectableOption)
1527
+
1528
+ const data = await this.query().get()
1529
+
1530
+ return { data, total: data.length }
1531
+ }
1532
+ }
1533
+
1534
+
1535
+ // ==========================
1536
+ // ## format result query
1537
+ // ==========================
1538
+ if (!(query as any).format) {
1539
+ ;(query as any).format = function (formatter: string | ((item: any) => any)) {
1540
+ const Model = this.$model
1541
+
1542
+ if (typeof formatter === 'string') {
1543
+ const fn = Model?.formatters?.[formatter]
1544
+ if (!fn) throw new Error(`Formatter "${formatter}" not found on model ${Model?.name}`)
1545
+
1546
+ this._formatter = fn
1547
+ return this
1548
+ }
1549
+
1550
+ if (typeof formatter === 'function') {
1551
+ this._formatter = formatter
1552
+ return this
1553
+ }
1554
+
1555
+ throw new Error('format() only accepts string or function')
1556
+ }
1557
+ }
1558
+
1559
+
1560
+
1561
+ return query
1562
+ }
1563
+
1564
+
1565
+
1566
+
1567
+ // ?? Public model helpers
1568
+
1569
+
1570
+ // =================================>
1571
+ // ## Primary key decorator model helpers
1572
+ // =================================>
1573
+ export function PrimaryKey() {
1574
+ return function (target: any, key: string) {
1575
+ const ctor = target.constructor
1576
+
1577
+ ctor.primaryKey = key
1578
+
1579
+ if (!ctor[FIELD_META]) ctor[FIELD_META] = {}
1580
+
1581
+ ctor[FIELD_META][key] = {
1582
+ cast: 'number',
1583
+ selectable: true,
1584
+ }
1585
+
1586
+ ctor[PRIMARY_KEY_META] = key
1587
+ }
1588
+ }
1589
+
1590
+
1591
+ // =================================>
1592
+ // ## Field decorator model helpers
1593
+ // =================================>
1594
+ export function Field(defs: string[]) {
1595
+ return function (target: any, key: string) {
1596
+ const ctor = target.constructor
1597
+ if (!ctor[FIELD_META]) ctor[FIELD_META] = {}
1598
+
1599
+ const meta: FieldMeta = {}
1600
+
1601
+ const [first, ...rest] = defs
1602
+ if (['string','number','boolean','date','json'].includes(first)) {
1603
+ meta.cast = first as ModelCastType
1604
+ } else {
1605
+ rest.unshift(first)
1606
+ }
1607
+
1608
+ for (const flag of rest) {
1609
+ if (flag === 'fillable') meta.fillable = true
1610
+ if (flag === 'selectable') meta.selectable = true
1611
+ if (flag === 'searchable') meta.searchable = true
1612
+ }
1613
+
1614
+ ctor[FIELD_META][key] = meta
1615
+ }
1616
+ }
1617
+
1618
+
1619
+
1620
+ // =================================>
1621
+ // ## Relation decorator model helpers
1622
+ // =================================>
1623
+ export function HasMany(
1624
+ model : () => typeof Model,
1625
+ options ?: {
1626
+ foreignKey ?: string
1627
+ localKey ?: string
1628
+ callback ?: (q: any) => void }
1629
+ ) {
1630
+ return (target: any, key: string) => {
1631
+ const parent = target.constructor
1632
+ const parentKey = options?.localKey ?? 'id'
1633
+ const foreignKey = options?.foreignKey ?? `${conversion.strSnake(parent.name)}_id`
1634
+
1635
+ pushRelation(target, key, { type: 'hasMany', model, foreignKey, localKey: parentKey, callback: options?.callback })
1636
+ }
1637
+ }
1638
+
1639
+
1640
+ export function HasOne(
1641
+ model : () => typeof Model,
1642
+ options ?: {
1643
+ foreignKey ?: string
1644
+ localKey ?: string
1645
+ callback ?: (q: any) => void
1646
+ }
1647
+ ) {
1648
+ return (target: any, key: string) => {
1649
+ const parent = target.constructor
1650
+ const parentKey = options?.localKey ?? 'id'
1651
+ const foreignKey = options?.foreignKey ?? `${conversion.strSnake(parent.name)}_id`
1652
+
1653
+ pushRelation(target, key, { type: 'hasOne', model, foreignKey, localKey: parentKey, callback: options?.callback })
1654
+ }
1655
+ }
1656
+
1657
+
1658
+ export function BelongsTo(
1659
+ model : () => typeof Model,
1660
+ options ?: {
1661
+ foreignKey ?: string
1662
+ ownerKey ?: string
1663
+ callback ?: (q: any) => void
1664
+ }
1665
+ ) {
1666
+ return (target: any, key: string) => {
1667
+ const ctor = target.constructor
1668
+ const foreignKey = options?.foreignKey ?? `${conversion.strSnake(key)}_id`
1669
+ const ownerKey = options?.ownerKey ?? 'id'
1670
+
1671
+ pushRelation(target, key, { type: 'belongsTo', model, foreignKey, localKey: ownerKey, callback: options?.callback })
1672
+
1673
+ if (!ctor[FIELD_META]) ctor[FIELD_META] = {}
1674
+
1675
+ if (!ctor[FIELD_META][foreignKey]) {
1676
+ ctor[FIELD_META][foreignKey] = {
1677
+ cast: 'number',
1678
+ fillable: true,
1679
+ selectable: true,
1680
+ }
1681
+ }
1682
+ }
1683
+ }
1684
+
1685
+
1686
+ // export function BelongsToMany(
1687
+ // model : () => typeof Model,
1688
+ // options ?: {
1689
+ // pivotTable ?: string
1690
+ // pivotLocal ?: string
1691
+ // pivotForeign ?: string
1692
+ // localKey ?: string
1693
+ // callback ?: (q: any) => void
1694
+ // }
1695
+ // ) {
1696
+ // return (target: any, key: string) => {
1697
+ // const parent = target.constructor
1698
+ // const parentName = conversion.strSnake(parent.name)
1699
+ // const relatedName = conversion.strSnake(model().name)
1700
+ // const pivotTable = options?.pivotTable ?? conversion.strPlural(`${parentName}_${relatedName}`)
1701
+ // const localKey = options?.localKey ?? 'id'
1702
+ // const pivotLocal = options?.pivotLocal ?? `${parentName}_id`
1703
+ // const pivotForeign = options?.pivotForeign ?? `${relatedName}_id`
1704
+
1705
+ // pushRelation(target, key, { type: 'belongsToMany', model, localKey, foreignKey: localKey, pivotTable, pivotLocal, pivotForeign, callback: options?.callback })
1706
+ // }
1707
+ // }
1708
+ export function BelongsToMany(
1709
+ model : () => typeof Model,
1710
+ options ?: {
1711
+ pivotTable ?: string
1712
+ pivotLocal ?: string
1713
+ pivotForeign ?: string
1714
+ localKey ?: string
1715
+ callback ?: (q: any) => void
1716
+ }
1717
+ ) {
1718
+ return (target: any, key: string) => {
1719
+ const localKey = options?.localKey ?? 'id'
1720
+
1721
+ pushRelation(target, key, {
1722
+ type: 'belongsToMany',
1723
+ model,
1724
+ localKey,
1725
+ foreignKey: localKey,
1726
+ pivotTable: options?.pivotTable,
1727
+ pivotLocal: options?.pivotLocal,
1728
+ pivotForeign: options?.pivotForeign,
1729
+ callback: options?.callback,
1730
+ })
1731
+ }
1732
+ }
1733
+
1734
+
1735
+
1736
+ // =================================>
1737
+ // ## Soft delete decorator model helpers
1738
+ // =================================>
1739
+ export function SoftDelete() {
1740
+ return function (target: any, propertyKey: string) {
1741
+ const ctor = target.constructor
1742
+
1743
+ ctor[SOFT_DELETE_META] = { enabled: true, column: propertyKey }
1744
+ }
1745
+ }
1746
+
1747
+
1748
+
1749
+ // =================================>
1750
+ // ## Attribute decorator model helpers
1751
+ // =================================>
1752
+ export function Attribute() {
1753
+ return function (
1754
+ target: any,
1755
+ key: string,
1756
+ descriptor: PropertyDescriptor
1757
+ ) {
1758
+ const ctor = target.constructor
1759
+ if (!ctor[ATTRIBUTE_META]) ctor[ATTRIBUTE_META] = {}
1760
+ ctor[ATTRIBUTE_META][key] = descriptor.value
1761
+ }
1762
+ }
1763
+
1764
+
1765
+
1766
+ // =================================>
1767
+ // ## Formatter decorator model helpers
1768
+ // =================================>
1769
+ export function Formatter() {
1770
+ return function (
1771
+ target: any,
1772
+ propertyKey: string,
1773
+ descriptor: PropertyDescriptor
1774
+ ) {
1775
+ const ctor = target
1776
+ if (!ctor[FORMATTER_META]) {
1777
+ ctor[FORMATTER_META] = {}
1778
+ }
1779
+
1780
+ ctor[FORMATTER_META][propertyKey] = descriptor.value
1781
+ }
1782
+ }
1783
+
1784
+
1785
+
1786
+ // =================================>
1787
+ // ## Scope decorator model helpers
1788
+ // =================================>
1789
+ export function Scope(
1790
+ mode: 'global' | 'internal' = 'internal'
1791
+ ) {
1792
+ return function (
1793
+ target: any,
1794
+ propertyKey: string,
1795
+ descriptor: PropertyDescriptor
1796
+ ) {
1797
+ const ctor = target.constructor
1798
+ if (!ctor[SCOPE_META]) ctor[SCOPE_META] = {}
1799
+
1800
+ ctor[SCOPE_META][propertyKey] = {
1801
+ fn: descriptor.value,
1802
+ mode,
1803
+ } satisfies ScopeType
1804
+ }
1805
+ }
1806
+
1807
+
1808
+ // =================================>
1809
+ // ## Hook
1810
+ // =================================>
1811
+ export function On(event: ModelHookEventType) {
1812
+ return function (
1813
+ target: any,
1814
+ propertyKey: string,
1815
+ descriptor: PropertyDescriptor
1816
+ ) {
1817
+ const ctor = target.constructor
1818
+
1819
+ if (!ctor._hooks) {
1820
+ ctor._hooks = {}
1821
+ }
1822
+
1823
+ if (!ctor._hooks[event]) {
1824
+ ctor._hooks[event] = []
1825
+ }
1826
+
1827
+ ctor._hooks[event].push(
1828
+ async ({ model, trx }: ModelHookContextType) => {
1829
+ return descriptor.value.call(model, { trx })
1830
+ }
1831
+ )
1832
+ }
1833
+ }
1834
+
1835
+
1836
+
1837
+
1838
+
1839
+ // ?? Private model helpers
1840
+
1841
+
1842
+ // =================================>
1843
+ // ## Global scope model helpers
1844
+ // =================================>
1845
+ function applyGlobalScopes(query: any) {
1846
+ const Model = query.$model
1847
+ if (!Model) return
1848
+
1849
+ applyScopes(query)
1850
+
1851
+ if (Model.isSoftDelete?.()) {
1852
+ const col = Model.getDeletedAtColumn()
1853
+ const mode = query._softDeleteScope ?? 'default'
1854
+
1855
+ if (mode === 'with') return
1856
+ if (mode === 'default') query.whereNull(col)
1857
+ if (mode === 'only') query.whereNotNull(col)
1858
+ }
1859
+ }
1860
+
1861
+
1862
+
1863
+ // =================================>
1864
+ // ## Load relation model helpers
1865
+ // =================================>
1866
+ async function loadRelations(
1867
+ rows : any[],
1868
+ Model : any,
1869
+ tree : Record<string, any>
1870
+ ) {
1871
+ if (!rows.length) return
1872
+
1873
+ for (const [name, node] of Object.entries(tree)) {
1874
+ const rel = Model.relations[name]
1875
+ if (!rel) continue
1876
+
1877
+ const desc = rel()
1878
+ let related: any[] = []
1879
+
1880
+ if (desc.type === 'belongsTo') {
1881
+ related = await loadBelongsTo(rows, desc, name, node.__callback)
1882
+ }
1883
+
1884
+ if (desc.type === 'belongsToMany') {
1885
+ related = await loadBelongsToMany(rows, desc, name, node.__callback)
1886
+ }
1887
+
1888
+ if (desc.type === 'hasMany') {
1889
+ related = await loadHasMany(rows, desc, name, node.__callback)
1890
+ }
1891
+
1892
+ if (desc.type === 'hasOne') {
1893
+ related = await loadHasOne(rows, desc, name, node.__callback)
1894
+ }
1895
+
1896
+ if (
1897
+ node.__children &&
1898
+ Object.keys(node.__children).length &&
1899
+ related.length
1900
+ ) {
1901
+ await loadRelations(related, desc.model(), node.__children)
1902
+ }
1903
+ }
1904
+ }
1905
+
1906
+ async function loadBelongsTo(
1907
+ rows: any[],
1908
+ rel: any,
1909
+ name: string,
1910
+ callback?: (q: any) => void
1911
+ ) {
1912
+ const ids = [...new Set(rows.map(r => r[rel.foreignKey]).filter(Boolean))]
1913
+ if (!ids.length) {
1914
+ rows.forEach(r => (r[name] = null))
1915
+
1916
+ return []
1917
+ }
1918
+
1919
+ const q = rel.model().query().whereIn(rel.localKey, ids)
1920
+
1921
+ rel.callback?.(q)
1922
+ callback?.(q)
1923
+
1924
+ const related = rel.model().hydrate(await q)
1925
+ const map = new Map(related.map((r: any) => [String(r[rel.localKey]), r]))
1926
+
1927
+ rows.forEach(r => (r[name] = map.get(String(r[rel.foreignKey])) ?? null))
1928
+
1929
+ return related
1930
+ }
1931
+
1932
+
1933
+ async function loadBelongsToMany(
1934
+ rows: any[],
1935
+ rel: any,
1936
+ name: string,
1937
+ callback?: (q: any) => void
1938
+ ) {
1939
+ const ids = rows.map(r => r[rel.localKey])
1940
+ if (!ids.length) {
1941
+ rows.forEach(r => (r[name] = []))
1942
+ return []
1943
+ }
1944
+
1945
+ const Parent = rows[0].constructor
1946
+ const Related = rel.model()
1947
+ const parentName = conversion.strSnake(Parent.name)
1948
+ const relatedName = conversion.strSnake(Related.name)
1949
+
1950
+ const pivotTable = rel.pivotTable ?? conversion.strPlural(`${parentName}_${relatedName}`)
1951
+ const pivotLocal = rel.pivotLocal ?? `${parentName}_id`
1952
+ const pivotForeign = rel.pivotForeign ?? `${relatedName}_id`
1953
+ const relatedTable = Related.getTable()
1954
+
1955
+ const q = Related.query().join(pivotTable, `${relatedTable}.${Related.primaryKey}`, '=', `${pivotTable}.${pivotForeign}`).whereIn(`${pivotTable}.${pivotLocal}`, ids)
1956
+
1957
+ rel.callback?.(q)
1958
+ callback?.(q)
1959
+
1960
+ const related = Related.hydrate(await q)
1961
+
1962
+ const grouped: Record<string, any[]> = {}
1963
+
1964
+ for (const r of related) {
1965
+ const pivotValue = (r as any)[pivotLocal]
1966
+ ;(grouped[pivotValue] ??= []).push(r)
1967
+ }
1968
+
1969
+ rows.forEach(r => { r[name] = grouped[r[rel.localKey]] ?? [] })
1970
+
1971
+ return related
1972
+ }
1973
+
1974
+
1975
+
1976
+ async function loadHasMany(rows: any[], rel: any, name: string, callback?: (q: any) => void) {
1977
+ const ids = rows.map(r => r[rel.localKey])
1978
+ if (!ids.length) {
1979
+ rows.forEach(r => (r[name] = []))
1980
+
1981
+ return []
1982
+ }
1983
+
1984
+ const q = rel.model().query().whereIn(rel.foreignKey, ids)
1985
+
1986
+ rel.callback?.(q)
1987
+ callback?.(q)
1988
+
1989
+ const related = rel.model().hydrate(await q)
1990
+ const grouped: Record<string, any[]> = {}
1991
+
1992
+ for (const r of related) {
1993
+ ;(grouped[String(r[rel.foreignKey])] ??= []).push(r)
1994
+ }
1995
+
1996
+ rows.forEach(r => (r[name] = grouped[String(r[rel.localKey])] ?? []))
1997
+
1998
+ return related
1999
+ }
2000
+
2001
+
2002
+ async function loadHasOne(
2003
+ rows: any[],
2004
+ rel: any,
2005
+ name: string,
2006
+ callback?: (q: any) => void
2007
+ ) {
2008
+ const ids = rows.map(r => r[rel.localKey])
2009
+ if (!ids.length) {
2010
+ rows.forEach(r => (r[name] = null))
2011
+ return []
2012
+ }
2013
+
2014
+ const q = rel.model().query().whereIn(rel.foreignKey, ids)
2015
+
2016
+ rel.callback?.(q)
2017
+ callback?.(q)
2018
+
2019
+ const related = rel.model().hydrate(await q)
2020
+ const map = new Map(
2021
+ related.map((r: any) => [String(r[rel.foreignKey]), r])
2022
+ )
2023
+
2024
+ rows.forEach(r => r[name] = map.get(String(r[rel.localKey])) ?? null)
2025
+
2026
+ return related
2027
+ }
2028
+
2029
+
2030
+
2031
+ // =================================>
2032
+ // ## Add relation model helpers
2033
+ // =================================>
2034
+ function pushRelation(
2035
+ target : any,
2036
+ key : string,
2037
+ desc : ModelRelationDescriptor
2038
+ ) {
2039
+ const ctor = target.constructor
2040
+ if (!ctor[RELATION_META]) ctor[RELATION_META] = {}
2041
+
2042
+ ctor[RELATION_META][key] = () => desc
2043
+ }
2044
+
2045
+
2046
+
2047
+ // =================================>
2048
+ // ## Where has model helpers
2049
+ // =================================>
2050
+ function whereHasSubquery(
2051
+ parentQuery: any,
2052
+ Model: any,
2053
+ relations: string[],
2054
+ callback?: (q: any) => void,
2055
+ negate: boolean = false
2056
+ ) {
2057
+ const relation = relations[0]
2058
+ const relDef = Model.relations?.[relation]
2059
+ if (!relDef) return
2060
+
2061
+ const desc = relDef()
2062
+ const Related = desc.model()
2063
+
2064
+ const parentTable = Model.getTable()
2065
+ const relatedTable = Related.getTable()
2066
+
2067
+ const method = negate ? 'whereNotExists' : 'whereExists'
2068
+
2069
+ parentQuery[method](function (this: Knex.QueryBuilder) {
2070
+ this.select(1).from(relatedTable)
2071
+
2072
+ if (desc.type === 'hasMany' || desc.type === 'hasOne') {
2073
+ this.whereRaw(`${relatedTable}.${desc.foreignKey} = ${parentTable}.${desc.localKey}`)
2074
+ }
2075
+
2076
+ if (desc.type === 'belongsTo') {
2077
+ this.whereRaw(`${relatedTable}.${desc.localKey} = ${parentTable}.${desc.foreignKey}`)
2078
+ }
2079
+
2080
+ if (desc.type === 'belongsToMany') {
2081
+ const pivot = desc.pivotTable
2082
+
2083
+ this.join(pivot, `${pivot}.${desc.pivotForeign}`, '=', `${relatedTable}.${Related.primaryKey}`)
2084
+
2085
+ this.whereRaw(`${pivot}.${desc.pivotLocal} = ${parentTable}.${desc.localKey}`)
2086
+ }
2087
+
2088
+ if (relations.length > 1) {
2089
+ whereHasSubquery(this, Related, relations.slice(1), callback, negate)
2090
+ return
2091
+ }
2092
+
2093
+ if (callback) {
2094
+ const qb = Related.query().from(relatedTable)
2095
+
2096
+ desc.callback?.(qb)
2097
+ callback(qb)
2098
+
2099
+ this.whereExists(qb)
2100
+ }
2101
+ })
2102
+ }
2103
+
2104
+
2105
+
2106
+ // =================================>
2107
+ // ## Add aggregate model helpers
2108
+ // =================================>
2109
+ function applyWithAggregates(query: any) {
2110
+ const Model = query.$model
2111
+ if (!Model || !query._withAggregates?.length) return
2112
+
2113
+ const parentTable = Model.getTable()
2114
+
2115
+ for (const item of query._withAggregates) {
2116
+ const relDef = Model.relations?.[item.relation]
2117
+ if (!relDef) continue
2118
+
2119
+ const desc = relDef()
2120
+ const Related = desc.model()
2121
+ const relatedTable = Related.getTable()
2122
+
2123
+ const fn = item.fn as AggregateType
2124
+ const sub = (db(Related.getTable()) as any)[fn](item.column)
2125
+
2126
+ if (desc.type === 'hasMany' || desc.type === 'hasOne') {
2127
+ sub.whereRaw(`${relatedTable}.${desc.foreignKey} = ${parentTable}.${desc.localKey}`)
2128
+ }
2129
+
2130
+ if (desc.type === 'belongsTo') {
2131
+ sub.whereRaw(`${relatedTable}.${desc.localKey} = ${parentTable}.${desc.foreignKey}`)
2132
+ }
2133
+
2134
+ if (desc.type === 'belongsToMany') {
2135
+ const pivot = desc.pivotTable
2136
+
2137
+ sub.join(pivot, `${pivot}.${desc.pivotForeign}`, '=', `${relatedTable}.${Related.primaryKey}`)
2138
+
2139
+ sub.whereRaw(`${pivot}.${desc.pivotLocal} = ${parentTable}.${desc.localKey}`)
2140
+ }
2141
+
2142
+ desc.callback?.(sub)
2143
+ item.callback?.(sub)
2144
+
2145
+ query.select(query.client.raw(`(${sub.toQuery()}) as ${item.alias}`))
2146
+ }
2147
+ }
2148
+
2149
+
2150
+
2151
+ // =================================>
2152
+ // ## Add order by aggregate model helpers
2153
+ // =================================>
2154
+ function applyOrderByAggregates(query: any) {
2155
+ const Model = query.$model
2156
+ if (!Model || !query._orderByAggregates?.length) return
2157
+
2158
+ const parentTable = Model.getTable()
2159
+
2160
+ for (const item of query._orderByAggregates) {
2161
+ const relDef = Model.relations?.[item.relation]
2162
+ if (!relDef) continue
2163
+
2164
+ const desc = relDef()
2165
+ const Related = desc.model()
2166
+ const relatedTable = Related.getTable()
2167
+
2168
+ const fn = item.fn as AggregateType
2169
+ const sub = (db(Related.getTable()) as any)[fn](item.column)
2170
+
2171
+ if (desc.type === 'hasMany') {
2172
+ sub.whereRaw(
2173
+ `${relatedTable}.${desc.foreignKey} = ${parentTable}.${desc.localKey}`
2174
+ )
2175
+ }
2176
+
2177
+ if (desc.type === 'belongsTo') {
2178
+ sub.whereRaw(
2179
+ `${relatedTable}.${desc.localKey} = ${parentTable}.${desc.foreignKey}`
2180
+ )
2181
+ }
2182
+
2183
+ if (Related.isSoftDelete?.()) {
2184
+ sub.whereNull(Related.getDeletedAtColumn())
2185
+ }
2186
+
2187
+ desc.callback?.(sub)
2188
+ item.callback?.(sub)
2189
+
2190
+ query.orderByRaw(`(${sub.toQuery()}) ${item.direction}`)
2191
+ }
2192
+ }
2193
+
2194
+
2195
+ // =================================>
2196
+ // ## Add scope model helpers
2197
+ // =================================>
2198
+ function applyScopes(query: any) {
2199
+ const Model = query.$model
2200
+ if (!Model) return
2201
+
2202
+ const scopes = Model.scopes ?? {}
2203
+ const disabled = query._disabledScopes ?? new Set<string>()
2204
+
2205
+ for (const [name, meta] of Object.entries(scopes) as [string, ScopeType][]) {
2206
+ if (meta.mode !== 'global') continue
2207
+ if (disabled.has(name)) continue
2208
+
2209
+ meta.fn.call(Model, query)
2210
+ }
2211
2211
  }