@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.
- package/.github/workflows/publish.yml +40 -0
- package/dist/{auth.util.d.ts → auth/auth.d.ts} +1 -5
- package/dist/{auth.util.js → auth/auth.js} +38 -30
- package/dist/auth/auth.js.map +1 -0
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.js +2 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/{context.util.js → context/context.js} +1 -1
- package/dist/context/context.js.map +1 -0
- package/dist/context/index.d.ts +1 -0
- package/dist/context/index.js +2 -0
- package/dist/context/index.js.map +1 -0
- package/dist/{controller.util.js → controller/controller.js} +1 -1
- package/dist/controller/controller.js.map +1 -0
- package/dist/controller/index.d.ts +1 -0
- package/dist/controller/index.js +2 -0
- package/dist/controller/index.js.map +1 -0
- package/dist/{conversion.util.js → conversion/conversion.js} +1 -1
- package/dist/conversion/conversion.js.map +1 -0
- package/dist/conversion/index.d.ts +1 -0
- package/dist/conversion/index.js +2 -0
- package/dist/conversion/index.js.map +1 -0
- package/dist/{db.util.d.ts → db/db.d.ts} +9 -5
- package/dist/{db.util.js → db/db.js} +40 -29
- package/dist/db/db.js.map +1 -0
- package/dist/db/index.d.ts +1 -0
- package/dist/db/index.js +2 -0
- package/dist/db/index.js.map +1 -0
- package/dist/index.d.ts +14 -13
- package/dist/index.js +14 -13
- package/dist/index.js.map +1 -1
- package/dist/logger/index.d.ts +1 -0
- package/dist/logger/index.js +2 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/{logger.util.js → logger/logger.js} +16 -7
- package/dist/logger/logger.js.map +1 -0
- package/dist/mail/index.d.ts +1 -0
- package/dist/mail/index.js +2 -0
- package/dist/mail/index.js.map +1 -0
- package/dist/{mail.util.js → mail/mail.js} +1 -1
- package/dist/mail/mail.js.map +1 -0
- package/dist/middleware/index.d.ts +1 -0
- package/dist/middleware/index.js +2 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/{middleware.util.js → middleware/middleware.js} +1 -1
- package/dist/middleware/middleware.js.map +1 -0
- package/dist/model/index.d.ts +1 -0
- package/dist/model/index.js +2 -0
- package/dist/model/index.js.map +1 -0
- package/dist/{model.util.js → model/model.js} +1 -1
- package/dist/model/model.js.map +1 -0
- package/dist/permission/index.d.ts +1 -0
- package/dist/permission/index.js +2 -0
- package/dist/permission/index.js.map +1 -0
- package/dist/{permission.util.js → permission/permission.js} +1 -1
- package/dist/permission/permission.js.map +1 -0
- package/dist/registry/index.d.ts +1 -0
- package/dist/registry/index.js +2 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/registry/registry.d.ts +28 -0
- package/dist/registry/registry.js +19 -0
- package/dist/registry/registry.js.map +1 -0
- package/dist/route/index.d.ts +1 -0
- package/dist/route/index.js +2 -0
- package/dist/route/index.js.map +1 -0
- package/dist/{route.util.js → route/route.js} +1 -1
- package/dist/route/route.js.map +1 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/{storage.util.js → storage/storage.js} +2 -2
- package/dist/storage/storage.js.map +1 -0
- package/dist/validation/index.d.ts +1 -0
- package/dist/validation/index.js +2 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/{validation.util.js → validation/validation.js} +1 -1
- package/dist/validation/validation.js.map +1 -0
- package/package.json +2 -2
- package/src/{auth.util.ts → auth/auth.ts} +255 -241
- package/src/auth/index.ts +1 -0
- package/src/{context.util.ts → context/context.ts} +17 -17
- package/src/context/index.ts +1 -0
- package/src/{controller.util.ts → controller/controller.ts} +236 -236
- package/src/controller/index.ts +1 -0
- package/src/{conversion.util.ts → conversion/conversion.ts} +64 -64
- package/src/conversion/index.ts +1 -0
- package/src/{db.util.ts → db/db.ts} +420 -405
- package/src/db/index.ts +1 -0
- package/src/index.ts +14 -13
- package/src/logger/index.ts +1 -0
- package/src/{logger.util.ts → logger/logger.ts} +176 -169
- package/src/mail/index.ts +1 -0
- package/src/{mail.util.ts → mail/mail.ts} +85 -85
- package/src/middleware/index.ts +1 -0
- package/src/{middleware.util.ts → middleware/middleware.ts} +288 -288
- package/src/model/index.ts +1 -0
- package/src/{model.util.ts → model/model.ts} +2210 -2210
- package/src/permission/index.ts +1 -0
- package/src/{permission.util.ts → permission/permission.ts} +136 -136
- package/src/registry/index.ts +1 -0
- package/src/registry/registry.ts +37 -0
- package/src/route/index.ts +1 -0
- package/src/{route.util.ts → route/route.ts} +11 -11
- package/src/storage/index.ts +1 -0
- package/src/{storage.util.ts → storage/storage.ts} +101 -101
- package/src/validation/index.ts +1 -0
- package/src/{validation.util.ts → validation/validation.ts} +338 -338
- package/tsconfig.json +1 -1
- package/bun.lock +0 -160
- package/dist/auth.util.js.map +0 -1
- package/dist/context.util.js.map +0 -1
- package/dist/controller.util.js.map +0 -1
- package/dist/conversion.util.js.map +0 -1
- package/dist/db.util.js.map +0 -1
- package/dist/logger.util.js.map +0 -1
- package/dist/mail.util.js.map +0 -1
- package/dist/middleware.util.js.map +0 -1
- package/dist/model.util.js.map +0 -1
- package/dist/permission.util.js.map +0 -1
- package/dist/route.util.js.map +0 -1
- package/dist/storage.util.js.map +0 -1
- package/dist/validation.util.js.map +0 -1
- /package/dist/{context.util.d.ts → context/context.d.ts} +0 -0
- /package/dist/{controller.util.d.ts → controller/controller.d.ts} +0 -0
- /package/dist/{conversion.util.d.ts → conversion/conversion.d.ts} +0 -0
- /package/dist/{logger.util.d.ts → logger/logger.d.ts} +0 -0
- /package/dist/{mail.util.d.ts → mail/mail.d.ts} +0 -0
- /package/dist/{middleware.util.d.ts → middleware/middleware.d.ts} +0 -0
- /package/dist/{model.util.d.ts → model/model.d.ts} +0 -0
- /package/dist/{permission.util.d.ts → permission/permission.d.ts} +0 -0
- /package/dist/{route.util.d.ts → route/route.d.ts} +0 -0
- /package/dist/{storage.util.d.ts → storage/storage.d.ts} +0 -0
- /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
|
}
|