@kava/kava-api-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bun.lock +160 -0
- package/dist/auth.util.d.ts +23 -0
- package/dist/auth.util.js +175 -0
- package/dist/auth.util.js.map +1 -0
- package/dist/context.util.d.ts +7 -0
- package/dist/context.util.js +11 -0
- package/dist/context.util.js.map +1 -0
- package/dist/controller.util.d.ts +118 -0
- package/dist/controller.util.js +144 -0
- package/dist/controller.util.js.map +1 -0
- package/dist/conversion.util.d.ts +8 -0
- package/dist/conversion.util.js +52 -0
- package/dist/conversion.util.js.map +1 -0
- package/dist/db.util.d.ts +80 -0
- package/dist/db.util.js +166 -0
- package/dist/db.util.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.util.d.ts +30 -0
- package/dist/logger.util.js +117 -0
- package/dist/logger.util.js.map +1 -0
- package/dist/mail.util.d.ts +21 -0
- package/dist/mail.util.js +53 -0
- package/dist/mail.util.js.map +1 -0
- package/dist/middleware.util.d.ts +263 -0
- package/dist/middleware.util.js +233 -0
- package/dist/middleware.util.js.map +1 -0
- package/dist/model.util.d.ts +204 -0
- package/dist/model.util.js +1495 -0
- package/dist/model.util.js.map +1 -0
- package/dist/permission.util.d.ts +38 -0
- package/dist/permission.util.js +91 -0
- package/dist/permission.util.js.map +1 -0
- package/dist/route.util.d.ts +1 -0
- package/dist/route.util.js +12 -0
- package/dist/route.util.js.map +1 -0
- package/dist/storage.util.d.ts +56 -0
- package/dist/storage.util.js +82 -0
- package/dist/storage.util.js.map +1 -0
- package/dist/validation.util.d.ts +7 -0
- package/dist/validation.util.js +237 -0
- package/dist/validation.util.js.map +1 -0
- package/package.json +34 -0
- package/src/auth.util.ts +242 -0
- package/src/context.util.ts +17 -0
- package/src/controller.util.ts +237 -0
- package/src/conversion.util.ts +65 -0
- package/src/db.util.ts +405 -0
- package/src/index.ts +13 -0
- package/src/logger.util.ts +170 -0
- package/src/mail.util.ts +86 -0
- package/src/middleware.util.ts +289 -0
- package/src/model.util.ts +2211 -0
- package/src/permission.util.ts +136 -0
- package/src/route.util.ts +12 -0
- package/src/storage.util.ts +102 -0
- package/src/validation.util.ts +338 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,1495 @@
|
|
|
1
|
+
import { status } from 'elysia';
|
|
2
|
+
import { conversion, db } from '@utils';
|
|
3
|
+
// ==========================
|
|
4
|
+
// ## Decorator Type
|
|
5
|
+
// ==========================
|
|
6
|
+
export const PRIMARY_KEY_META = Symbol('primary-key-meta');
|
|
7
|
+
export const FIELD_META = Symbol('field-meta');
|
|
8
|
+
export const RELATION_META = Symbol('relation-meta');
|
|
9
|
+
export const SOFT_DELETE_META = Symbol('soft-delete');
|
|
10
|
+
export const ATTRIBUTE_META = Symbol('attribute-meta');
|
|
11
|
+
export const FORMATTER_META = Symbol('formatter-meta');
|
|
12
|
+
export const SCOPE_META = Symbol('scope-meta');
|
|
13
|
+
const Casts = {
|
|
14
|
+
string: {
|
|
15
|
+
fromDB: (v) => v == null ? v : String(v),
|
|
16
|
+
toDB: (v) => v,
|
|
17
|
+
},
|
|
18
|
+
number: {
|
|
19
|
+
fromDB: (v) => v == null ? v : Number(v),
|
|
20
|
+
toDB: (v) => v,
|
|
21
|
+
},
|
|
22
|
+
boolean: {
|
|
23
|
+
fromDB: (v) => Boolean(v),
|
|
24
|
+
toDB: (v) => v ? 1 : 0,
|
|
25
|
+
},
|
|
26
|
+
date: {
|
|
27
|
+
fromDB: (v) => v ? new Date(v) : null,
|
|
28
|
+
toDB: (v) => v instanceof Date ? v.toISOString() : v,
|
|
29
|
+
},
|
|
30
|
+
json: {
|
|
31
|
+
fromDB: (v) => {
|
|
32
|
+
if (typeof v !== 'string')
|
|
33
|
+
return v;
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(v);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
toDB: (v) => JSON.stringify(v),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
export class Model {
|
|
45
|
+
// ==========================
|
|
46
|
+
// ## Constructor model
|
|
47
|
+
// ==========================
|
|
48
|
+
constructor(data = {}) {
|
|
49
|
+
this._original = {};
|
|
50
|
+
this._exists = false;
|
|
51
|
+
this._trx = undefined;
|
|
52
|
+
this._instanceHooks = {};
|
|
53
|
+
this._disabledHooks = new Set();
|
|
54
|
+
Object.assign(this, data);
|
|
55
|
+
if (this[this.constructor.primaryKey])
|
|
56
|
+
this._exists = true;
|
|
57
|
+
}
|
|
58
|
+
static newInstance() {
|
|
59
|
+
return new this();
|
|
60
|
+
}
|
|
61
|
+
static getDefaultFields() {
|
|
62
|
+
const pk = this[PRIMARY_KEY_META] ?? this.primaryKey ?? 'id';
|
|
63
|
+
return {
|
|
64
|
+
[pk]: {
|
|
65
|
+
cast: 'number',
|
|
66
|
+
selectable: true,
|
|
67
|
+
},
|
|
68
|
+
created_at: { cast: 'date' },
|
|
69
|
+
updated_at: { cast: 'date' },
|
|
70
|
+
deleted_at: { cast: 'date' },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
static get fields() {
|
|
74
|
+
const defaults = this.getDefaultFields?.() ?? {};
|
|
75
|
+
return { ...defaults, ...(this[FIELD_META] ?? {}) };
|
|
76
|
+
}
|
|
77
|
+
static get fillable() {
|
|
78
|
+
return Object.entries(this.fields).filter(([, v]) => v.fillable).map(([k]) => k);
|
|
79
|
+
}
|
|
80
|
+
static get selectable() {
|
|
81
|
+
return Object.entries(this.fields).filter(([, v]) => v.selectable).map(([k]) => k);
|
|
82
|
+
}
|
|
83
|
+
static get searchable() {
|
|
84
|
+
return Object.entries(this.fields).filter(([, v]) => v.searchable).map(([k]) => k);
|
|
85
|
+
}
|
|
86
|
+
// ==========================
|
|
87
|
+
// ## Table
|
|
88
|
+
// ==========================
|
|
89
|
+
static getTable() {
|
|
90
|
+
if (this.table)
|
|
91
|
+
return this.table;
|
|
92
|
+
return conversion.strPlural(conversion.strSnake(this.name));
|
|
93
|
+
}
|
|
94
|
+
// ==========================
|
|
95
|
+
// ## Primary key
|
|
96
|
+
// ==========================
|
|
97
|
+
static getPrimaryKey() {
|
|
98
|
+
return this[PRIMARY_KEY_META] ?? this.primaryKey ?? 'id';
|
|
99
|
+
}
|
|
100
|
+
static get relations() {
|
|
101
|
+
return this[RELATION_META] ?? {};
|
|
102
|
+
}
|
|
103
|
+
// ==========================
|
|
104
|
+
// ## Attribute
|
|
105
|
+
// ==========================
|
|
106
|
+
static get attributes() {
|
|
107
|
+
return this[ATTRIBUTE_META] ?? {};
|
|
108
|
+
}
|
|
109
|
+
// ==========================
|
|
110
|
+
// ## Formatter
|
|
111
|
+
// ==========================
|
|
112
|
+
static get formatters() {
|
|
113
|
+
return this[FORMATTER_META] ?? {};
|
|
114
|
+
}
|
|
115
|
+
// ==========================
|
|
116
|
+
// ## Scope
|
|
117
|
+
// ==========================
|
|
118
|
+
static get scopes() {
|
|
119
|
+
return this[SCOPE_META] ?? {};
|
|
120
|
+
}
|
|
121
|
+
// ==========================
|
|
122
|
+
// ## Getter attribute
|
|
123
|
+
// ==========================
|
|
124
|
+
getOriginal(key) {
|
|
125
|
+
if (!key)
|
|
126
|
+
return { ...this._original };
|
|
127
|
+
return this._original[key];
|
|
128
|
+
}
|
|
129
|
+
getChanges() {
|
|
130
|
+
const changes = {};
|
|
131
|
+
for (const key in this._original) {
|
|
132
|
+
if (this[key] != this._original[key]) {
|
|
133
|
+
changes[key] = this[key];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return changes;
|
|
137
|
+
}
|
|
138
|
+
getPrevious() {
|
|
139
|
+
const prev = {};
|
|
140
|
+
for (const key in this._original) {
|
|
141
|
+
if (this[key] != this._original[key]) {
|
|
142
|
+
prev[key] = this._original[key];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return prev;
|
|
146
|
+
}
|
|
147
|
+
// ==========================
|
|
148
|
+
// ## Query builder
|
|
149
|
+
// ==========================
|
|
150
|
+
static query(trx) {
|
|
151
|
+
const qb = (trx ?? db)(this.getTable());
|
|
152
|
+
return extendModelQuery(qb, this);
|
|
153
|
+
}
|
|
154
|
+
// ==========================
|
|
155
|
+
// ## Casting
|
|
156
|
+
// ==========================
|
|
157
|
+
castFromDB(row) {
|
|
158
|
+
const ctor = this.constructor;
|
|
159
|
+
const fields = ctor.fields;
|
|
160
|
+
for (const [key, value] of Object.entries(row)) {
|
|
161
|
+
if (!fields[key])
|
|
162
|
+
continue;
|
|
163
|
+
const meta = fields[key];
|
|
164
|
+
this[key] = meta.cast ? Casts[meta.cast].fromDB(value) : value;
|
|
165
|
+
}
|
|
166
|
+
this._original = {};
|
|
167
|
+
for (const key of Object.keys(fields)) {
|
|
168
|
+
this._original[key] = this[key];
|
|
169
|
+
}
|
|
170
|
+
const attrs = ctor.attributes;
|
|
171
|
+
for (const [name, fn] of Object.entries(attrs)) {
|
|
172
|
+
Object.defineProperty(this, name, {
|
|
173
|
+
get: () => fn.call(this),
|
|
174
|
+
enumerable: true,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
this._exists = true;
|
|
178
|
+
return this;
|
|
179
|
+
}
|
|
180
|
+
castToDB() {
|
|
181
|
+
const fields = this.constructor.fields;
|
|
182
|
+
const data = {};
|
|
183
|
+
for (const [key, meta] of Object.entries(fields)) {
|
|
184
|
+
if (!meta.fillable)
|
|
185
|
+
continue;
|
|
186
|
+
const val = this[key];
|
|
187
|
+
if (val === undefined)
|
|
188
|
+
continue;
|
|
189
|
+
data[key] = meta.cast ? Casts[meta.cast].toDB(val) : val;
|
|
190
|
+
}
|
|
191
|
+
return data;
|
|
192
|
+
}
|
|
193
|
+
// ==========================
|
|
194
|
+
// ## Dirty Field
|
|
195
|
+
// ==========================
|
|
196
|
+
getDirty() {
|
|
197
|
+
const dirty = {};
|
|
198
|
+
for (const key in this._original) {
|
|
199
|
+
if (Object.is(this[key], this._original[key]))
|
|
200
|
+
continue;
|
|
201
|
+
dirty[key] = this[key];
|
|
202
|
+
}
|
|
203
|
+
return dirty;
|
|
204
|
+
}
|
|
205
|
+
// ==========================
|
|
206
|
+
// ## Hydration
|
|
207
|
+
// ==========================
|
|
208
|
+
static hydrate(rows) {
|
|
209
|
+
if (!rows || !Array.isArray(rows))
|
|
210
|
+
return [];
|
|
211
|
+
return rows.map(row => {
|
|
212
|
+
if (row instanceof this)
|
|
213
|
+
return row;
|
|
214
|
+
const instance = this.newInstance();
|
|
215
|
+
return instance.castFromDB(row);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
// ==========================
|
|
219
|
+
// ## Fill model from payload
|
|
220
|
+
// ==========================
|
|
221
|
+
fill(payload) {
|
|
222
|
+
const ctor = this.constructor;
|
|
223
|
+
const fields = ctor.fields;
|
|
224
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
225
|
+
const meta = fields[key];
|
|
226
|
+
if (!meta || !meta.fillable)
|
|
227
|
+
continue;
|
|
228
|
+
this[key] = value;
|
|
229
|
+
}
|
|
230
|
+
return this;
|
|
231
|
+
}
|
|
232
|
+
// ==========================
|
|
233
|
+
// ## Create from fillable
|
|
234
|
+
// ==========================
|
|
235
|
+
static async create(payload, trx) {
|
|
236
|
+
const instance = this.newInstance();
|
|
237
|
+
instance.fill(payload);
|
|
238
|
+
const data = instance.castToDB();
|
|
239
|
+
await instance.runHook('before-create', { model: instance, trx });
|
|
240
|
+
const conn = trx ?? db;
|
|
241
|
+
const [row] = await conn(this.getTable()).insert(data).returning('*');
|
|
242
|
+
await instance.runHook('after-create', { model: instance, trx });
|
|
243
|
+
return instance.castFromDB(row);
|
|
244
|
+
}
|
|
245
|
+
// ==========================
|
|
246
|
+
// ## update from fillable
|
|
247
|
+
// ==========================
|
|
248
|
+
static async update(payload, uniqueKeys, trx) {
|
|
249
|
+
if (!uniqueKeys.length) {
|
|
250
|
+
throw new Error('updateByUnique requires uniqueKeys');
|
|
251
|
+
}
|
|
252
|
+
const instance = this.newInstance();
|
|
253
|
+
instance.fill(payload);
|
|
254
|
+
const data = instance.castToDB();
|
|
255
|
+
const where = {};
|
|
256
|
+
for (const key of uniqueKeys) {
|
|
257
|
+
if (payload[key] === undefined) {
|
|
258
|
+
throw new Error(`Missing unique key: ${key}`);
|
|
259
|
+
}
|
|
260
|
+
where[key] = payload[key];
|
|
261
|
+
}
|
|
262
|
+
const conn = trx ?? db;
|
|
263
|
+
const [row] = await conn(this.getTable()).where(where).update(data).returning('*');
|
|
264
|
+
if (!row)
|
|
265
|
+
throw status(404, { message: 'Record not found' });
|
|
266
|
+
return instance.castFromDB(row);
|
|
267
|
+
}
|
|
268
|
+
// ==========================
|
|
269
|
+
// ## Update or insert from fillable
|
|
270
|
+
// ==========================
|
|
271
|
+
static async upsert(payload, uniqueKeys, trx) {
|
|
272
|
+
if (uniqueKeys.length === 0) {
|
|
273
|
+
throw new Error('Upsert requires uniqueKeys');
|
|
274
|
+
}
|
|
275
|
+
const constraints = {};
|
|
276
|
+
for (const key of uniqueKeys) {
|
|
277
|
+
if (payload[key] === undefined) {
|
|
278
|
+
throw new Error(`Missing unique key: ${key}`);
|
|
279
|
+
}
|
|
280
|
+
constraints[key] = payload[key];
|
|
281
|
+
}
|
|
282
|
+
let row = await this.query().where(constraints).first();
|
|
283
|
+
if (!row) {
|
|
284
|
+
const instance = this.newInstance();
|
|
285
|
+
instance.fill(payload);
|
|
286
|
+
const data = instance.castToDB();
|
|
287
|
+
const conn = trx ?? db;
|
|
288
|
+
const [row] = await conn(this.getTable()).insert(data).returning('*');
|
|
289
|
+
return instance.castFromDB(row);
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
const instance = this.newInstance();
|
|
293
|
+
instance.fill(payload);
|
|
294
|
+
const data = instance.castToDB();
|
|
295
|
+
const conn = trx ?? db;
|
|
296
|
+
const [row] = await conn(this.getTable()).where(constraints).update(data).returning('*');
|
|
297
|
+
return instance.castFromDB(row);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// ==========================
|
|
301
|
+
// ## Save (insert / update)
|
|
302
|
+
// ==========================
|
|
303
|
+
async save() {
|
|
304
|
+
const model = this.constructor;
|
|
305
|
+
const table = model.getTable();
|
|
306
|
+
const pk = model.primaryKey;
|
|
307
|
+
const conn = this._trx ?? db;
|
|
308
|
+
const hookCtx = { model: this, trx: this._trx };
|
|
309
|
+
if (!this._exists) {
|
|
310
|
+
const data = this.castToDB();
|
|
311
|
+
await this.runHook(`before-create`, hookCtx);
|
|
312
|
+
const [row] = await conn(table).insert(data).returning('*');
|
|
313
|
+
this.castFromDB(row);
|
|
314
|
+
await this.runHook(`after-create`, hookCtx);
|
|
315
|
+
return this;
|
|
316
|
+
}
|
|
317
|
+
const dirty = this.getDirty();
|
|
318
|
+
if (Object.keys(dirty).length === 0)
|
|
319
|
+
return this;
|
|
320
|
+
await this.runHook(`before-update`, hookCtx);
|
|
321
|
+
const fields = model.fields;
|
|
322
|
+
const updateData = {};
|
|
323
|
+
for (const [key, meta] of Object.entries(fields)) {
|
|
324
|
+
if (!meta.fillable)
|
|
325
|
+
continue;
|
|
326
|
+
if (!(key in dirty))
|
|
327
|
+
continue;
|
|
328
|
+
const val = dirty[key];
|
|
329
|
+
updateData[key] = meta.cast ? Casts[meta.cast].toDB(val) : val;
|
|
330
|
+
}
|
|
331
|
+
if (Object.keys(updateData).length === 0)
|
|
332
|
+
return this;
|
|
333
|
+
await conn(table).where(pk, this[pk]).update(updateData);
|
|
334
|
+
Object.assign(this._original, dirty);
|
|
335
|
+
await this.runHook(`after-update`, hookCtx);
|
|
336
|
+
return this;
|
|
337
|
+
}
|
|
338
|
+
// ==========================
|
|
339
|
+
// ## Save with relation
|
|
340
|
+
// ==========================
|
|
341
|
+
async pump(payload, options = {}) {
|
|
342
|
+
const isRoot = !options.trx;
|
|
343
|
+
const trx = options.trx ?? await db.transaction();
|
|
344
|
+
try {
|
|
345
|
+
// Handle Array (Bulk)
|
|
346
|
+
if (Array.isArray(payload)) {
|
|
347
|
+
const Ctor = this.constructor;
|
|
348
|
+
const pkName = Ctor.primaryKey ?? 'id';
|
|
349
|
+
const results = [];
|
|
350
|
+
for (const item of payload) {
|
|
351
|
+
let instance = null;
|
|
352
|
+
const pkValue = item[pkName];
|
|
353
|
+
if (pkValue) {
|
|
354
|
+
const row = await Ctor.query(trx).where(pkName, pkValue).first();
|
|
355
|
+
if (row) {
|
|
356
|
+
instance = Ctor.hydrate([row])[0];
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (!instance) {
|
|
360
|
+
instance = Ctor.newInstance();
|
|
361
|
+
}
|
|
362
|
+
await instance.pump(item, { trx });
|
|
363
|
+
results.push(instance);
|
|
364
|
+
}
|
|
365
|
+
if (isRoot)
|
|
366
|
+
await trx.commit();
|
|
367
|
+
return results;
|
|
368
|
+
}
|
|
369
|
+
const ctor = this.constructor;
|
|
370
|
+
const fields = ctor.fields;
|
|
371
|
+
const relations = ctor.relations ?? {};
|
|
372
|
+
const flat = {};
|
|
373
|
+
const nested = {};
|
|
374
|
+
for (const key of Object.keys(payload)) {
|
|
375
|
+
const value = payload[key];
|
|
376
|
+
if (fields[key]?.fillable) {
|
|
377
|
+
flat[key] = value;
|
|
378
|
+
}
|
|
379
|
+
else if (relations[key] && value !== null) {
|
|
380
|
+
nested[key] = value;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
this.fill(flat);
|
|
384
|
+
await this.useTransaction(trx).save();
|
|
385
|
+
for (const [name, value] of Object.entries(nested)) {
|
|
386
|
+
const relDef = relations[name];
|
|
387
|
+
if (!relDef)
|
|
388
|
+
continue;
|
|
389
|
+
const desc = relDef();
|
|
390
|
+
const Related = desc.model();
|
|
391
|
+
// ===== hasMany / belongsToMany =====
|
|
392
|
+
if (Array.isArray(value)) {
|
|
393
|
+
const existing = await Related.query(trx).where(desc.foreignKey, this[desc.localKey]).get();
|
|
394
|
+
const existingIds = existing.map((r) => r[Related.primaryKey]);
|
|
395
|
+
const incomingIds = [];
|
|
396
|
+
for (const item of value) {
|
|
397
|
+
if (item[Related.primaryKey]) {
|
|
398
|
+
const child = await Related.query(trx).where(Related.primaryKey, item[Related.primaryKey]).getFirst();
|
|
399
|
+
if (child) {
|
|
400
|
+
await child.pump(item, { trx });
|
|
401
|
+
incomingIds.push(child[Related.primaryKey]);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
const child = Related.newInstance();
|
|
406
|
+
child[desc.foreignKey] = this[desc.localKey];
|
|
407
|
+
await child.pump(item, { trx });
|
|
408
|
+
incomingIds.push(child[Related.primaryKey]);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const toDelete = existingIds.filter((id) => !incomingIds.includes(id));
|
|
412
|
+
if (toDelete.length) {
|
|
413
|
+
await Related.query(trx).whereIn(Related.primaryKey, toDelete).delete();
|
|
414
|
+
}
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
const child = Related.newInstance();
|
|
418
|
+
if (desc.type !== 'belongsTo') {
|
|
419
|
+
;
|
|
420
|
+
child[desc.foreignKey] = this[desc.localKey];
|
|
421
|
+
}
|
|
422
|
+
await child.pump(value, { trx });
|
|
423
|
+
}
|
|
424
|
+
if (isRoot)
|
|
425
|
+
await trx.commit();
|
|
426
|
+
return this;
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
if (isRoot)
|
|
430
|
+
await trx.rollback();
|
|
431
|
+
throw err;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
// ==========================
|
|
435
|
+
// ## Soft Delete
|
|
436
|
+
// ==========================
|
|
437
|
+
static getSoftDeleteConfig() {
|
|
438
|
+
return this[SOFT_DELETE_META] ?? null;
|
|
439
|
+
}
|
|
440
|
+
static isSoftDelete() {
|
|
441
|
+
return !!this.getSoftDeleteConfig();
|
|
442
|
+
}
|
|
443
|
+
static getDeletedAtColumn() {
|
|
444
|
+
return this.getSoftDeleteConfig()?.column ?? null;
|
|
445
|
+
}
|
|
446
|
+
// ==========================
|
|
447
|
+
// ## Delete (with or without soft delete)
|
|
448
|
+
// ==========================
|
|
449
|
+
async delete() {
|
|
450
|
+
const model = this.constructor;
|
|
451
|
+
const soft = model.getSoftDeleteConfig?.();
|
|
452
|
+
if (!this._exists)
|
|
453
|
+
return null;
|
|
454
|
+
if (!soft)
|
|
455
|
+
return await this.forceDelete();
|
|
456
|
+
const trx = this._trx ?? await db.transaction();
|
|
457
|
+
try {
|
|
458
|
+
await this.runHook(`before-delete`, { model: this, trx });
|
|
459
|
+
await trx(model.getTable()).where(model.primaryKey, this[model.primaryKey]).update({ [soft.column]: new Date() });
|
|
460
|
+
await this.runHook(`after-delete`, { model: this, trx });
|
|
461
|
+
if (!this._trx)
|
|
462
|
+
await trx.commit();
|
|
463
|
+
const snapshot = await (this._trx ?? db)(model.getTable()).where(model.primaryKey, this[model.primaryKey]).first();
|
|
464
|
+
this._exists = false;
|
|
465
|
+
return snapshot;
|
|
466
|
+
}
|
|
467
|
+
catch (err) {
|
|
468
|
+
if (!this._trx)
|
|
469
|
+
await trx.rollback();
|
|
470
|
+
throw err;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// ==========================
|
|
474
|
+
// ## Delete without soft delete
|
|
475
|
+
// ==========================
|
|
476
|
+
async forceDelete() {
|
|
477
|
+
const model = this.constructor;
|
|
478
|
+
const pk = model.primaryKey;
|
|
479
|
+
if (!this._exists)
|
|
480
|
+
return;
|
|
481
|
+
const snapshot = await (this._trx ?? db)(model.getTable()).where(pk, this[pk]).first();
|
|
482
|
+
if (!snapshot)
|
|
483
|
+
return null;
|
|
484
|
+
await (this._trx ?? db)(model.getTable()).where(pk, this[pk]).delete();
|
|
485
|
+
this._exists = false;
|
|
486
|
+
return snapshot;
|
|
487
|
+
}
|
|
488
|
+
async restore() {
|
|
489
|
+
const model = this.constructor;
|
|
490
|
+
const soft = model.getSoftDeleteConfig?.();
|
|
491
|
+
if (!soft)
|
|
492
|
+
return this;
|
|
493
|
+
await this.runHook(`before-update`, { model: this, trx: this._trx });
|
|
494
|
+
await (this._trx ?? db)(model.getTable()).where(model.primaryKey, this[model.primaryKey]).update({ [soft.column]: null });
|
|
495
|
+
await this.runHook(`after-update`, { model: this, trx: this._trx });
|
|
496
|
+
this._exists = true;
|
|
497
|
+
return this;
|
|
498
|
+
}
|
|
499
|
+
// ==========================
|
|
500
|
+
// ## Model Hook
|
|
501
|
+
// ==========================
|
|
502
|
+
on(event, fn) {
|
|
503
|
+
if (!this._instanceHooks[event])
|
|
504
|
+
this._instanceHooks[event] = [];
|
|
505
|
+
this._instanceHooks[event].push(fn);
|
|
506
|
+
return this;
|
|
507
|
+
}
|
|
508
|
+
off(event) {
|
|
509
|
+
this._disabledHooks.add(event);
|
|
510
|
+
return this;
|
|
511
|
+
}
|
|
512
|
+
async runHook(event, ctx) {
|
|
513
|
+
if (this._disabledHooks.has(event))
|
|
514
|
+
return;
|
|
515
|
+
const ctor = this.constructor;
|
|
516
|
+
const globals = ctor._hooks?.[event] ?? [];
|
|
517
|
+
for (const fn of globals)
|
|
518
|
+
await fn(ctx);
|
|
519
|
+
const locals = this._instanceHooks[event] ?? [];
|
|
520
|
+
for (const fn of locals)
|
|
521
|
+
await fn(ctx);
|
|
522
|
+
}
|
|
523
|
+
// ==========================
|
|
524
|
+
// ## To response data
|
|
525
|
+
// ==========================
|
|
526
|
+
toJSON() {
|
|
527
|
+
const data = {};
|
|
528
|
+
const ctor = this.constructor;
|
|
529
|
+
const fields = ctor.fields ?? {};
|
|
530
|
+
const relations = ctor.relations ?? {};
|
|
531
|
+
const attributes = ctor.attributes ?? {};
|
|
532
|
+
for (const key of Object.keys(fields)) {
|
|
533
|
+
if (fields[key]?.hidden)
|
|
534
|
+
continue;
|
|
535
|
+
data[key] = this[key];
|
|
536
|
+
}
|
|
537
|
+
const expanded = this.__expandedAttributes ?? [];
|
|
538
|
+
for (const key of expanded) {
|
|
539
|
+
if (attributes[key]) {
|
|
540
|
+
data[key] = this[key];
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
for (const key of Object.keys(relations)) {
|
|
544
|
+
if (this[key] !== undefined) {
|
|
545
|
+
data[key] = this[key];
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return data;
|
|
549
|
+
}
|
|
550
|
+
// ==========================
|
|
551
|
+
// ## Transaction binding
|
|
552
|
+
// ==========================
|
|
553
|
+
useTransaction(trx) {
|
|
554
|
+
this._trx = trx;
|
|
555
|
+
return this;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
Model.table = "";
|
|
559
|
+
Model.primaryKey = 'id';
|
|
560
|
+
Model.softDelete = false;
|
|
561
|
+
Model.deletedAtColumn = 'deleted_at';
|
|
562
|
+
Model._hooks = {};
|
|
563
|
+
// ==========================
|
|
564
|
+
// ## Model query builder (extend from knex query builder)
|
|
565
|
+
// ==========================
|
|
566
|
+
export function extendModelQuery(query, Model) {
|
|
567
|
+
;
|
|
568
|
+
query.$model = Model;
|
|
569
|
+
query._withTree = {};
|
|
570
|
+
query._softDeleteScope = 'default' //? default | with | only
|
|
571
|
+
;
|
|
572
|
+
query._formatter = null;
|
|
573
|
+
query._disabledScopes = new Set();
|
|
574
|
+
query.withTrashed = function () {
|
|
575
|
+
this._softDeleteScope = 'with';
|
|
576
|
+
return this;
|
|
577
|
+
};
|
|
578
|
+
query.onlyTrashed = function () {
|
|
579
|
+
this._softDeleteScope = 'only';
|
|
580
|
+
return this;
|
|
581
|
+
};
|
|
582
|
+
// ==========================
|
|
583
|
+
// ## find or not found
|
|
584
|
+
// ==========================
|
|
585
|
+
if (!query.findOrNotFound) {
|
|
586
|
+
;
|
|
587
|
+
query.findOrNotFound = async function (id) {
|
|
588
|
+
applyGlobalScopes(this);
|
|
589
|
+
const pk = Model.primaryKey ?? 'id';
|
|
590
|
+
const row = await this.where(pk, id).first();
|
|
591
|
+
if (!row)
|
|
592
|
+
throw status(404, { message: "Error: Record not found!" });
|
|
593
|
+
return Model.hydrate([row])[0];
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
// ==========================
|
|
597
|
+
// ## first or not found
|
|
598
|
+
// ==========================
|
|
599
|
+
if (!query.firstOrNotFound) {
|
|
600
|
+
;
|
|
601
|
+
query.firstOrNotFound = async function () {
|
|
602
|
+
applyGlobalScopes(this);
|
|
603
|
+
const row = await this.first();
|
|
604
|
+
if (!row)
|
|
605
|
+
throw status(404, { message: "Error: Record not found!" });
|
|
606
|
+
return Model.hydrate([row])[0];
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
// ==========================
|
|
610
|
+
// ## find
|
|
611
|
+
// ==========================
|
|
612
|
+
if (!query.find) {
|
|
613
|
+
;
|
|
614
|
+
query.find = async function (id) {
|
|
615
|
+
applyGlobalScopes(this);
|
|
616
|
+
const pk = Model.primaryKey ?? 'id';
|
|
617
|
+
const row = await this.where(pk, id).first();
|
|
618
|
+
if (!row)
|
|
619
|
+
return null;
|
|
620
|
+
return Model.hydrate([row])[0];
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
// ==========================
|
|
624
|
+
// ## Search query
|
|
625
|
+
// ==========================
|
|
626
|
+
if (!query.search) {
|
|
627
|
+
;
|
|
628
|
+
query.search = function (keyword, { includes = [], searchable = [] } = {}) {
|
|
629
|
+
const model = this.$model;
|
|
630
|
+
if (!model)
|
|
631
|
+
return this;
|
|
632
|
+
const defaultSearchable = Model.searchable || [];
|
|
633
|
+
const mergedSearchable = searchable?.length ? searchable : [...defaultSearchable, ...includes];
|
|
634
|
+
if (!keyword || !mergedSearchable.length)
|
|
635
|
+
return this;
|
|
636
|
+
this.where((q) => {
|
|
637
|
+
mergedSearchable.forEach((column) => {
|
|
638
|
+
if (column.includes(".")) {
|
|
639
|
+
const [relation, col] = column.split(".");
|
|
640
|
+
q.orWhereHas(relation, (rel) => rel.where(col, "ILIKE", `%${keyword}%`));
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
q.orWhere(column, "ILIKE", `%${keyword}%`);
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
return this;
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
// ==========================
|
|
651
|
+
// ## Filer query
|
|
652
|
+
// ==========================
|
|
653
|
+
if (!query.filter) {
|
|
654
|
+
;
|
|
655
|
+
query.filter = function (filters) {
|
|
656
|
+
if (!filters)
|
|
657
|
+
return this;
|
|
658
|
+
for (const [field, filter] of Object.entries(filters)) {
|
|
659
|
+
const [type, value] = filter.split(":");
|
|
660
|
+
if (!type || value === undefined)
|
|
661
|
+
continue;
|
|
662
|
+
const applyWhere = (q, col) => {
|
|
663
|
+
switch (type) {
|
|
664
|
+
case "li":
|
|
665
|
+
q.where(col, "ILIKE", `%${value}%`);
|
|
666
|
+
break;
|
|
667
|
+
case "eq":
|
|
668
|
+
q.where(col, value);
|
|
669
|
+
break;
|
|
670
|
+
case "ne":
|
|
671
|
+
q.where(col, "!=", value);
|
|
672
|
+
break;
|
|
673
|
+
case "in":
|
|
674
|
+
q.whereIn(col, value.split(","));
|
|
675
|
+
break;
|
|
676
|
+
case "ni":
|
|
677
|
+
q.whereNotIn(col, value.split(","));
|
|
678
|
+
break;
|
|
679
|
+
case "bw": {
|
|
680
|
+
const [min, max] = value.split(",");
|
|
681
|
+
q.whereBetween(col, [min, max]);
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
if (field.includes(".")) {
|
|
687
|
+
const [relation, col] = field.split(".");
|
|
688
|
+
this.whereHas(relation, (q) => applyWhere(q, col));
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
applyWhere(this, field);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return this;
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
// ==========================
|
|
698
|
+
// ## Select query
|
|
699
|
+
// ==========================
|
|
700
|
+
if (!query.selects) {
|
|
701
|
+
;
|
|
702
|
+
query.selects = function ({ includes = [], selectable = [] } = {}) {
|
|
703
|
+
const model = this.$model;
|
|
704
|
+
if (!model)
|
|
705
|
+
return this;
|
|
706
|
+
const defaultSelectable = Model.selectable || ["*"];
|
|
707
|
+
this.select(selectable?.length ? selectable : [...defaultSelectable, ...includes]);
|
|
708
|
+
return this;
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
// ==========================
|
|
712
|
+
// ## Sort query
|
|
713
|
+
// ==========================
|
|
714
|
+
if (!query.sorts) {
|
|
715
|
+
;
|
|
716
|
+
query.sorts = function (sorts) {
|
|
717
|
+
if (!Array.isArray(sorts) || sorts.length === 0)
|
|
718
|
+
return this;
|
|
719
|
+
sorts.forEach((sortExpr) => {
|
|
720
|
+
if (typeof sortExpr !== "string")
|
|
721
|
+
return;
|
|
722
|
+
const parts = sortExpr.trim().split(/\s+/);
|
|
723
|
+
const column = parts[0];
|
|
724
|
+
const direction = (parts[1] || "asc").toLowerCase();
|
|
725
|
+
if (!["asc", "desc"].includes(direction)) {
|
|
726
|
+
this.orderBy(column, "asc");
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
this.orderBy(column, direction);
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
return this;
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
// ==========================
|
|
736
|
+
// ## Get query
|
|
737
|
+
// ==========================
|
|
738
|
+
;
|
|
739
|
+
query.get = async function () {
|
|
740
|
+
applyGlobalScopes(this);
|
|
741
|
+
applyWithAggregates(this);
|
|
742
|
+
applyOrderByAggregates(this);
|
|
743
|
+
const rows = await this;
|
|
744
|
+
let result = this.$model.hydrate(rows);
|
|
745
|
+
if (this._withTree && Object.keys(this._withTree).length) {
|
|
746
|
+
await loadRelations(result, this.$model, this._withTree);
|
|
747
|
+
}
|
|
748
|
+
result.forEach((item) => {
|
|
749
|
+
item.__expandedAttributes = Object.keys(this._withTree || {})
|
|
750
|
+
.filter(k => this._withTree[k]?.__attribute);
|
|
751
|
+
});
|
|
752
|
+
if (this._formatter) {
|
|
753
|
+
result = result.map(this._formatter);
|
|
754
|
+
}
|
|
755
|
+
return result;
|
|
756
|
+
};
|
|
757
|
+
query.getFirst = async function () {
|
|
758
|
+
applyGlobalScopes(this);
|
|
759
|
+
applyWithAggregates(this);
|
|
760
|
+
applyOrderByAggregates(this);
|
|
761
|
+
const rows = await this.limit(1);
|
|
762
|
+
let result = this.$model.hydrate(rows);
|
|
763
|
+
if (this._withTree && Object.keys(this._withTree).length) {
|
|
764
|
+
await loadRelations(result, this.$model, this._withTree);
|
|
765
|
+
}
|
|
766
|
+
if (!result.at(0))
|
|
767
|
+
return null;
|
|
768
|
+
result.at(0).__expandedAttributes = Object.keys(this._withTree || {}).filter(k => this._withTree[k]?.__attribute);
|
|
769
|
+
if (this._formatter) {
|
|
770
|
+
result = result.map(this._formatter).at(0);
|
|
771
|
+
}
|
|
772
|
+
return result.at(0);
|
|
773
|
+
};
|
|
774
|
+
// ==========================
|
|
775
|
+
// ## Paginate query
|
|
776
|
+
// ==========================
|
|
777
|
+
if (!query.paginate) {
|
|
778
|
+
;
|
|
779
|
+
query.paginate = async function (page = 1, limit = 10) {
|
|
780
|
+
applyGlobalScopes(this);
|
|
781
|
+
applyWithAggregates(this);
|
|
782
|
+
applyOrderByAggregates(this);
|
|
783
|
+
const offset = (page - 1) * limit;
|
|
784
|
+
const raw = await this.clone().limit(limit).offset(offset);
|
|
785
|
+
let data = Model.hydrate(raw);
|
|
786
|
+
const [{ count }] = await this.clone().clearSelect().clearOrder().count('* as count');
|
|
787
|
+
const total = Number(count);
|
|
788
|
+
if (!total)
|
|
789
|
+
return { data: [], total: 0 };
|
|
790
|
+
if (this._withTree && Object.keys(this._withTree).length) {
|
|
791
|
+
await loadRelations(data, this.$model, this._withTree);
|
|
792
|
+
}
|
|
793
|
+
data.forEach((item) => {
|
|
794
|
+
item.__expandedAttributes = Object.keys(this._withTree || {})
|
|
795
|
+
.filter(k => this._withTree[k]?.__attribute);
|
|
796
|
+
});
|
|
797
|
+
if (this._formatter) {
|
|
798
|
+
data = data.map(this._formatter);
|
|
799
|
+
}
|
|
800
|
+
return { data, total };
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
// =================================>
|
|
804
|
+
// ## Expand query (Eager loading)
|
|
805
|
+
// =================================>
|
|
806
|
+
;
|
|
807
|
+
query.expand = function (entries = []) {
|
|
808
|
+
if (!Array.isArray(entries) || !entries.length)
|
|
809
|
+
return this;
|
|
810
|
+
if (!this._withTree)
|
|
811
|
+
this._withTree = {};
|
|
812
|
+
const applyPath = (path, callback) => {
|
|
813
|
+
const parts = path.split('.');
|
|
814
|
+
let cur = this._withTree;
|
|
815
|
+
let node;
|
|
816
|
+
for (const part of parts) {
|
|
817
|
+
cur[part] ??= { __children: {} };
|
|
818
|
+
node = cur[part];
|
|
819
|
+
cur = node.__children;
|
|
820
|
+
}
|
|
821
|
+
if (callback && node) {
|
|
822
|
+
node.__callback = callback;
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
for (const entry of entries) {
|
|
826
|
+
if (typeof entry === 'string') {
|
|
827
|
+
applyPath(entry);
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
if (typeof entry === 'object') {
|
|
831
|
+
for (const [path, cb] of Object.entries(entry)) {
|
|
832
|
+
applyPath(path, cb);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return this;
|
|
837
|
+
};
|
|
838
|
+
// ==========================
|
|
839
|
+
// ## Where has query
|
|
840
|
+
// ==========================
|
|
841
|
+
if (!query.whereHas) {
|
|
842
|
+
;
|
|
843
|
+
query.whereHas = function (path, callback) {
|
|
844
|
+
const Model = this.$model;
|
|
845
|
+
if (!Model)
|
|
846
|
+
return this;
|
|
847
|
+
const relations = path.split('.');
|
|
848
|
+
whereHasSubquery(this, Model, relations, callback, false);
|
|
849
|
+
return this;
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
// ==========================
|
|
853
|
+
// ## Or where has query
|
|
854
|
+
// ==========================
|
|
855
|
+
if (!query.orWhereHas) {
|
|
856
|
+
;
|
|
857
|
+
query.orWhereHas = function (path, callback) {
|
|
858
|
+
const Model = this.$model;
|
|
859
|
+
if (!Model)
|
|
860
|
+
return this;
|
|
861
|
+
this.orWhere(() => {
|
|
862
|
+
const relations = path.split('.');
|
|
863
|
+
whereHasSubquery(this, Model, relations, callback, false);
|
|
864
|
+
});
|
|
865
|
+
return this;
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
// ==========================
|
|
869
|
+
// ## Where doesn't have has query
|
|
870
|
+
// ==========================
|
|
871
|
+
if (!query.whereDoesntHave) {
|
|
872
|
+
;
|
|
873
|
+
query.whereDoesntHave = function (path, callback) {
|
|
874
|
+
const Model = this.$model;
|
|
875
|
+
if (!Model)
|
|
876
|
+
return this;
|
|
877
|
+
const relations = path.split('.');
|
|
878
|
+
whereHasSubquery(this, Model, relations, callback, true);
|
|
879
|
+
return this;
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
// ==========================
|
|
883
|
+
// ## Or where doesn't have has query
|
|
884
|
+
// ==========================
|
|
885
|
+
if (!query.orWhereDoesntHave) {
|
|
886
|
+
;
|
|
887
|
+
query.orWhereDoesntHave = function (path, callback) {
|
|
888
|
+
const Model = this.$model;
|
|
889
|
+
if (!Model)
|
|
890
|
+
return this;
|
|
891
|
+
this.orWhere(() => {
|
|
892
|
+
const relations = path.split('.');
|
|
893
|
+
whereHasSubquery(this, Model, relations, callback, true);
|
|
894
|
+
});
|
|
895
|
+
return this;
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
// ==========================
|
|
899
|
+
// ## Scope query
|
|
900
|
+
// =========================
|
|
901
|
+
if (!query.scope) {
|
|
902
|
+
;
|
|
903
|
+
query.scope = function (name, ...args) {
|
|
904
|
+
const Model = this.$model;
|
|
905
|
+
if (!Model)
|
|
906
|
+
return this;
|
|
907
|
+
const meta = Model.scopes?.[name];
|
|
908
|
+
if (!meta) {
|
|
909
|
+
throw new Error(`Scope "${name}" not found`);
|
|
910
|
+
}
|
|
911
|
+
if (meta.mode !== 'internal') {
|
|
912
|
+
throw new Error(`Scope "${name}" is global and cannot be called manually`);
|
|
913
|
+
}
|
|
914
|
+
meta.fn.apply(this, args);
|
|
915
|
+
return this;
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
if (!query.withoutScope) {
|
|
919
|
+
;
|
|
920
|
+
query.withoutScope = function (...names) {
|
|
921
|
+
for (const name of names) {
|
|
922
|
+
this._disabledScopes.add(name);
|
|
923
|
+
}
|
|
924
|
+
return this;
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
// ==========================
|
|
928
|
+
// ## with aggregate query
|
|
929
|
+
// ==========================
|
|
930
|
+
if (!query.withAggregate) {
|
|
931
|
+
;
|
|
932
|
+
query.withAggregate = function (expr, fn, column = '*', callback) {
|
|
933
|
+
if (!this._withAggregates)
|
|
934
|
+
this._withAggregates = [];
|
|
935
|
+
const [rel, aliasRaw] = expr.split(/\s+as\s+/i);
|
|
936
|
+
this._withAggregates.push({
|
|
937
|
+
relation: rel.trim(),
|
|
938
|
+
alias: aliasRaw?.trim() || `${rel}_${fn}`,
|
|
939
|
+
fn,
|
|
940
|
+
column,
|
|
941
|
+
callback,
|
|
942
|
+
});
|
|
943
|
+
return this;
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
// ==========================
|
|
947
|
+
// ## Order by aggregate query
|
|
948
|
+
// ==========================
|
|
949
|
+
if (!query.orderByAggregate) {
|
|
950
|
+
;
|
|
951
|
+
query.orderByAggregate = function (expr, fn, column = '*', direction = 'asc', callback) {
|
|
952
|
+
if (!this._orderByAggregates)
|
|
953
|
+
this._orderByAggregates = [];
|
|
954
|
+
const [rel, aliasRaw] = expr.split(/\s+as\s+/i);
|
|
955
|
+
this._orderByAggregates.push({
|
|
956
|
+
relation: rel.trim(),
|
|
957
|
+
alias: aliasRaw?.trim(),
|
|
958
|
+
fn,
|
|
959
|
+
column,
|
|
960
|
+
direction,
|
|
961
|
+
callback,
|
|
962
|
+
});
|
|
963
|
+
return this;
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
// =================================>
|
|
967
|
+
// ## Get option query
|
|
968
|
+
// =================================>
|
|
969
|
+
if (!query.option) {
|
|
970
|
+
;
|
|
971
|
+
query.option = async function (selectableOption) {
|
|
972
|
+
applyGlobalScopes(this);
|
|
973
|
+
const model = this.$model;
|
|
974
|
+
const q = this.clone();
|
|
975
|
+
let defaultSelectable = [];
|
|
976
|
+
if (model) {
|
|
977
|
+
defaultSelectable = model.selectable || [];
|
|
978
|
+
}
|
|
979
|
+
let processedCols = [];
|
|
980
|
+
if (!Array.isArray(selectableOption) || selectableOption.length === 0) {
|
|
981
|
+
const valueCol = defaultSelectable.length > 0 ? defaultSelectable[0] : model.primaryKey;
|
|
982
|
+
const labelCol = defaultSelectable.length > 0 ? defaultSelectable[1] : defaultSelectable[0] ?? model.primaryKey;
|
|
983
|
+
processedCols = [`${valueCol} as value`, `${labelCol} as label`];
|
|
984
|
+
}
|
|
985
|
+
else {
|
|
986
|
+
processedCols = selectableOption.map((col, index) => {
|
|
987
|
+
const hasAlias = /\s+as\s+/i.test(col);
|
|
988
|
+
if (!hasAlias) {
|
|
989
|
+
if (index === 0)
|
|
990
|
+
return `${col} as value`;
|
|
991
|
+
if (index === 1)
|
|
992
|
+
return `${col} as label`;
|
|
993
|
+
}
|
|
994
|
+
return col;
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
q.clearSelect().select(processedCols);
|
|
998
|
+
return await q;
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
// ==========================
|
|
1002
|
+
// ## Paginate or option query
|
|
1003
|
+
// ==========================
|
|
1004
|
+
if (!query.paginateOrOption) {
|
|
1005
|
+
;
|
|
1006
|
+
query.paginateOrOption = async function (page = 1, limit = 10, option, selectableOption) {
|
|
1007
|
+
const isOption = ["true", "1", "yes"].includes(String(option).toLowerCase());
|
|
1008
|
+
if (isOption) {
|
|
1009
|
+
const data = await this.option(selectableOption);
|
|
1010
|
+
return { data, total: data.length };
|
|
1011
|
+
}
|
|
1012
|
+
const result = await this.paginate(page, limit);
|
|
1013
|
+
return result;
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
// ==========================
|
|
1017
|
+
// ## Resolve query
|
|
1018
|
+
// ==========================
|
|
1019
|
+
if (!query.resolve) {
|
|
1020
|
+
;
|
|
1021
|
+
query.resolve = async function (input = {}) {
|
|
1022
|
+
const gq = input?.getQuery ? input.getQuery : input;
|
|
1023
|
+
const isOption = input?.headers?.["x-option"] || gq?.isOption || false;
|
|
1024
|
+
this.
|
|
1025
|
+
expand?.(gq.expand).
|
|
1026
|
+
search?.(gq.search, {
|
|
1027
|
+
includes: [],
|
|
1028
|
+
searchable: gq.searchable
|
|
1029
|
+
}).
|
|
1030
|
+
filter?.(gq.filter).
|
|
1031
|
+
selects?.({
|
|
1032
|
+
includes: [],
|
|
1033
|
+
selectable: gq.selectable
|
|
1034
|
+
}).
|
|
1035
|
+
sorts?.(gq.sort);
|
|
1036
|
+
if (isOption || gq.paginate)
|
|
1037
|
+
return await this.paginateOrOption?.(gq.page, gq.paginate, isOption, gq.selectableOption);
|
|
1038
|
+
const data = await this.query().get();
|
|
1039
|
+
return { data, total: data.length };
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
// ==========================
|
|
1043
|
+
// ## format result query
|
|
1044
|
+
// ==========================
|
|
1045
|
+
if (!query.format) {
|
|
1046
|
+
;
|
|
1047
|
+
query.format = function (formatter) {
|
|
1048
|
+
const Model = this.$model;
|
|
1049
|
+
if (typeof formatter === 'string') {
|
|
1050
|
+
const fn = Model?.formatters?.[formatter];
|
|
1051
|
+
if (!fn)
|
|
1052
|
+
throw new Error(`Formatter "${formatter}" not found on model ${Model?.name}`);
|
|
1053
|
+
this._formatter = fn;
|
|
1054
|
+
return this;
|
|
1055
|
+
}
|
|
1056
|
+
if (typeof formatter === 'function') {
|
|
1057
|
+
this._formatter = formatter;
|
|
1058
|
+
return this;
|
|
1059
|
+
}
|
|
1060
|
+
throw new Error('format() only accepts string or function');
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
return query;
|
|
1064
|
+
}
|
|
1065
|
+
// ?? Public model helpers
|
|
1066
|
+
// =================================>
|
|
1067
|
+
// ## Primary key decorator model helpers
|
|
1068
|
+
// =================================>
|
|
1069
|
+
export function PrimaryKey() {
|
|
1070
|
+
return function (target, key) {
|
|
1071
|
+
const ctor = target.constructor;
|
|
1072
|
+
ctor.primaryKey = key;
|
|
1073
|
+
if (!ctor[FIELD_META])
|
|
1074
|
+
ctor[FIELD_META] = {};
|
|
1075
|
+
ctor[FIELD_META][key] = {
|
|
1076
|
+
cast: 'number',
|
|
1077
|
+
selectable: true,
|
|
1078
|
+
};
|
|
1079
|
+
ctor[PRIMARY_KEY_META] = key;
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
// =================================>
|
|
1083
|
+
// ## Field decorator model helpers
|
|
1084
|
+
// =================================>
|
|
1085
|
+
export function Field(defs) {
|
|
1086
|
+
return function (target, key) {
|
|
1087
|
+
const ctor = target.constructor;
|
|
1088
|
+
if (!ctor[FIELD_META])
|
|
1089
|
+
ctor[FIELD_META] = {};
|
|
1090
|
+
const meta = {};
|
|
1091
|
+
const [first, ...rest] = defs;
|
|
1092
|
+
if (['string', 'number', 'boolean', 'date', 'json'].includes(first)) {
|
|
1093
|
+
meta.cast = first;
|
|
1094
|
+
}
|
|
1095
|
+
else {
|
|
1096
|
+
rest.unshift(first);
|
|
1097
|
+
}
|
|
1098
|
+
for (const flag of rest) {
|
|
1099
|
+
if (flag === 'fillable')
|
|
1100
|
+
meta.fillable = true;
|
|
1101
|
+
if (flag === 'selectable')
|
|
1102
|
+
meta.selectable = true;
|
|
1103
|
+
if (flag === 'searchable')
|
|
1104
|
+
meta.searchable = true;
|
|
1105
|
+
}
|
|
1106
|
+
ctor[FIELD_META][key] = meta;
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
// =================================>
|
|
1110
|
+
// ## Relation decorator model helpers
|
|
1111
|
+
// =================================>
|
|
1112
|
+
export function HasMany(model, options) {
|
|
1113
|
+
return (target, key) => {
|
|
1114
|
+
const parent = target.constructor;
|
|
1115
|
+
const parentKey = options?.localKey ?? 'id';
|
|
1116
|
+
const foreignKey = options?.foreignKey ?? `${conversion.strSnake(parent.name)}_id`;
|
|
1117
|
+
pushRelation(target, key, { type: 'hasMany', model, foreignKey, localKey: parentKey, callback: options?.callback });
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
export function HasOne(model, options) {
|
|
1121
|
+
return (target, key) => {
|
|
1122
|
+
const parent = target.constructor;
|
|
1123
|
+
const parentKey = options?.localKey ?? 'id';
|
|
1124
|
+
const foreignKey = options?.foreignKey ?? `${conversion.strSnake(parent.name)}_id`;
|
|
1125
|
+
pushRelation(target, key, { type: 'hasOne', model, foreignKey, localKey: parentKey, callback: options?.callback });
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
export function BelongsTo(model, options) {
|
|
1129
|
+
return (target, key) => {
|
|
1130
|
+
const ctor = target.constructor;
|
|
1131
|
+
const foreignKey = options?.foreignKey ?? `${conversion.strSnake(key)}_id`;
|
|
1132
|
+
const ownerKey = options?.ownerKey ?? 'id';
|
|
1133
|
+
pushRelation(target, key, { type: 'belongsTo', model, foreignKey, localKey: ownerKey, callback: options?.callback });
|
|
1134
|
+
if (!ctor[FIELD_META])
|
|
1135
|
+
ctor[FIELD_META] = {};
|
|
1136
|
+
if (!ctor[FIELD_META][foreignKey]) {
|
|
1137
|
+
ctor[FIELD_META][foreignKey] = {
|
|
1138
|
+
cast: 'number',
|
|
1139
|
+
fillable: true,
|
|
1140
|
+
selectable: true,
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
// export function BelongsToMany(
|
|
1146
|
+
// model : () => typeof Model,
|
|
1147
|
+
// options ?: {
|
|
1148
|
+
// pivotTable ?: string
|
|
1149
|
+
// pivotLocal ?: string
|
|
1150
|
+
// pivotForeign ?: string
|
|
1151
|
+
// localKey ?: string
|
|
1152
|
+
// callback ?: (q: any) => void
|
|
1153
|
+
// }
|
|
1154
|
+
// ) {
|
|
1155
|
+
// return (target: any, key: string) => {
|
|
1156
|
+
// const parent = target.constructor
|
|
1157
|
+
// const parentName = conversion.strSnake(parent.name)
|
|
1158
|
+
// const relatedName = conversion.strSnake(model().name)
|
|
1159
|
+
// const pivotTable = options?.pivotTable ?? conversion.strPlural(`${parentName}_${relatedName}`)
|
|
1160
|
+
// const localKey = options?.localKey ?? 'id'
|
|
1161
|
+
// const pivotLocal = options?.pivotLocal ?? `${parentName}_id`
|
|
1162
|
+
// const pivotForeign = options?.pivotForeign ?? `${relatedName}_id`
|
|
1163
|
+
// pushRelation(target, key, { type: 'belongsToMany', model, localKey, foreignKey: localKey, pivotTable, pivotLocal, pivotForeign, callback: options?.callback })
|
|
1164
|
+
// }
|
|
1165
|
+
// }
|
|
1166
|
+
export function BelongsToMany(model, options) {
|
|
1167
|
+
return (target, key) => {
|
|
1168
|
+
const localKey = options?.localKey ?? 'id';
|
|
1169
|
+
pushRelation(target, key, {
|
|
1170
|
+
type: 'belongsToMany',
|
|
1171
|
+
model,
|
|
1172
|
+
localKey,
|
|
1173
|
+
foreignKey: localKey,
|
|
1174
|
+
pivotTable: options?.pivotTable,
|
|
1175
|
+
pivotLocal: options?.pivotLocal,
|
|
1176
|
+
pivotForeign: options?.pivotForeign,
|
|
1177
|
+
callback: options?.callback,
|
|
1178
|
+
});
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
// =================================>
|
|
1182
|
+
// ## Soft delete decorator model helpers
|
|
1183
|
+
// =================================>
|
|
1184
|
+
export function SoftDelete() {
|
|
1185
|
+
return function (target, propertyKey) {
|
|
1186
|
+
const ctor = target.constructor;
|
|
1187
|
+
ctor[SOFT_DELETE_META] = { enabled: true, column: propertyKey };
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
// =================================>
|
|
1191
|
+
// ## Attribute decorator model helpers
|
|
1192
|
+
// =================================>
|
|
1193
|
+
export function Attribute() {
|
|
1194
|
+
return function (target, key, descriptor) {
|
|
1195
|
+
const ctor = target.constructor;
|
|
1196
|
+
if (!ctor[ATTRIBUTE_META])
|
|
1197
|
+
ctor[ATTRIBUTE_META] = {};
|
|
1198
|
+
ctor[ATTRIBUTE_META][key] = descriptor.value;
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
// =================================>
|
|
1202
|
+
// ## Formatter decorator model helpers
|
|
1203
|
+
// =================================>
|
|
1204
|
+
export function Formatter() {
|
|
1205
|
+
return function (target, propertyKey, descriptor) {
|
|
1206
|
+
const ctor = target;
|
|
1207
|
+
if (!ctor[FORMATTER_META]) {
|
|
1208
|
+
ctor[FORMATTER_META] = {};
|
|
1209
|
+
}
|
|
1210
|
+
ctor[FORMATTER_META][propertyKey] = descriptor.value;
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
// =================================>
|
|
1214
|
+
// ## Scope decorator model helpers
|
|
1215
|
+
// =================================>
|
|
1216
|
+
export function Scope(mode = 'internal') {
|
|
1217
|
+
return function (target, propertyKey, descriptor) {
|
|
1218
|
+
const ctor = target.constructor;
|
|
1219
|
+
if (!ctor[SCOPE_META])
|
|
1220
|
+
ctor[SCOPE_META] = {};
|
|
1221
|
+
ctor[SCOPE_META][propertyKey] = {
|
|
1222
|
+
fn: descriptor.value,
|
|
1223
|
+
mode,
|
|
1224
|
+
};
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
// =================================>
|
|
1228
|
+
// ## Hook
|
|
1229
|
+
// =================================>
|
|
1230
|
+
export function On(event) {
|
|
1231
|
+
return function (target, propertyKey, descriptor) {
|
|
1232
|
+
const ctor = target.constructor;
|
|
1233
|
+
if (!ctor._hooks) {
|
|
1234
|
+
ctor._hooks = {};
|
|
1235
|
+
}
|
|
1236
|
+
if (!ctor._hooks[event]) {
|
|
1237
|
+
ctor._hooks[event] = [];
|
|
1238
|
+
}
|
|
1239
|
+
ctor._hooks[event].push(async ({ model, trx }) => {
|
|
1240
|
+
return descriptor.value.call(model, { trx });
|
|
1241
|
+
});
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
// ?? Private model helpers
|
|
1245
|
+
// =================================>
|
|
1246
|
+
// ## Global scope model helpers
|
|
1247
|
+
// =================================>
|
|
1248
|
+
function applyGlobalScopes(query) {
|
|
1249
|
+
const Model = query.$model;
|
|
1250
|
+
if (!Model)
|
|
1251
|
+
return;
|
|
1252
|
+
applyScopes(query);
|
|
1253
|
+
if (Model.isSoftDelete?.()) {
|
|
1254
|
+
const col = Model.getDeletedAtColumn();
|
|
1255
|
+
const mode = query._softDeleteScope ?? 'default';
|
|
1256
|
+
if (mode === 'with')
|
|
1257
|
+
return;
|
|
1258
|
+
if (mode === 'default')
|
|
1259
|
+
query.whereNull(col);
|
|
1260
|
+
if (mode === 'only')
|
|
1261
|
+
query.whereNotNull(col);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
// =================================>
|
|
1265
|
+
// ## Load relation model helpers
|
|
1266
|
+
// =================================>
|
|
1267
|
+
async function loadRelations(rows, Model, tree) {
|
|
1268
|
+
if (!rows.length)
|
|
1269
|
+
return;
|
|
1270
|
+
for (const [name, node] of Object.entries(tree)) {
|
|
1271
|
+
const rel = Model.relations[name];
|
|
1272
|
+
if (!rel)
|
|
1273
|
+
continue;
|
|
1274
|
+
const desc = rel();
|
|
1275
|
+
let related = [];
|
|
1276
|
+
if (desc.type === 'belongsTo') {
|
|
1277
|
+
related = await loadBelongsTo(rows, desc, name, node.__callback);
|
|
1278
|
+
}
|
|
1279
|
+
if (desc.type === 'belongsToMany') {
|
|
1280
|
+
related = await loadBelongsToMany(rows, desc, name, node.__callback);
|
|
1281
|
+
}
|
|
1282
|
+
if (desc.type === 'hasMany') {
|
|
1283
|
+
related = await loadHasMany(rows, desc, name, node.__callback);
|
|
1284
|
+
}
|
|
1285
|
+
if (desc.type === 'hasOne') {
|
|
1286
|
+
related = await loadHasOne(rows, desc, name, node.__callback);
|
|
1287
|
+
}
|
|
1288
|
+
if (node.__children &&
|
|
1289
|
+
Object.keys(node.__children).length &&
|
|
1290
|
+
related.length) {
|
|
1291
|
+
await loadRelations(related, desc.model(), node.__children);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
async function loadBelongsTo(rows, rel, name, callback) {
|
|
1296
|
+
const ids = [...new Set(rows.map(r => r[rel.foreignKey]).filter(Boolean))];
|
|
1297
|
+
if (!ids.length) {
|
|
1298
|
+
rows.forEach(r => (r[name] = null));
|
|
1299
|
+
return [];
|
|
1300
|
+
}
|
|
1301
|
+
const q = rel.model().query().whereIn(rel.localKey, ids);
|
|
1302
|
+
rel.callback?.(q);
|
|
1303
|
+
callback?.(q);
|
|
1304
|
+
const related = rel.model().hydrate(await q);
|
|
1305
|
+
const map = new Map(related.map((r) => [String(r[rel.localKey]), r]));
|
|
1306
|
+
rows.forEach(r => (r[name] = map.get(String(r[rel.foreignKey])) ?? null));
|
|
1307
|
+
return related;
|
|
1308
|
+
}
|
|
1309
|
+
async function loadBelongsToMany(rows, rel, name, callback) {
|
|
1310
|
+
const ids = rows.map(r => r[rel.localKey]);
|
|
1311
|
+
if (!ids.length) {
|
|
1312
|
+
rows.forEach(r => (r[name] = []));
|
|
1313
|
+
return [];
|
|
1314
|
+
}
|
|
1315
|
+
const Parent = rows[0].constructor;
|
|
1316
|
+
const Related = rel.model();
|
|
1317
|
+
const parentName = conversion.strSnake(Parent.name);
|
|
1318
|
+
const relatedName = conversion.strSnake(Related.name);
|
|
1319
|
+
const pivotTable = rel.pivotTable ?? conversion.strPlural(`${parentName}_${relatedName}`);
|
|
1320
|
+
const pivotLocal = rel.pivotLocal ?? `${parentName}_id`;
|
|
1321
|
+
const pivotForeign = rel.pivotForeign ?? `${relatedName}_id`;
|
|
1322
|
+
const relatedTable = Related.getTable();
|
|
1323
|
+
const q = Related.query().join(pivotTable, `${relatedTable}.${Related.primaryKey}`, '=', `${pivotTable}.${pivotForeign}`).whereIn(`${pivotTable}.${pivotLocal}`, ids);
|
|
1324
|
+
rel.callback?.(q);
|
|
1325
|
+
callback?.(q);
|
|
1326
|
+
const related = Related.hydrate(await q);
|
|
1327
|
+
const grouped = {};
|
|
1328
|
+
for (const r of related) {
|
|
1329
|
+
const pivotValue = r[pivotLocal];
|
|
1330
|
+
(grouped[pivotValue] ??= []).push(r);
|
|
1331
|
+
}
|
|
1332
|
+
rows.forEach(r => { r[name] = grouped[r[rel.localKey]] ?? []; });
|
|
1333
|
+
return related;
|
|
1334
|
+
}
|
|
1335
|
+
async function loadHasMany(rows, rel, name, callback) {
|
|
1336
|
+
const ids = rows.map(r => r[rel.localKey]);
|
|
1337
|
+
if (!ids.length) {
|
|
1338
|
+
rows.forEach(r => (r[name] = []));
|
|
1339
|
+
return [];
|
|
1340
|
+
}
|
|
1341
|
+
const q = rel.model().query().whereIn(rel.foreignKey, ids);
|
|
1342
|
+
rel.callback?.(q);
|
|
1343
|
+
callback?.(q);
|
|
1344
|
+
const related = rel.model().hydrate(await q);
|
|
1345
|
+
const grouped = {};
|
|
1346
|
+
for (const r of related) {
|
|
1347
|
+
;
|
|
1348
|
+
(grouped[String(r[rel.foreignKey])] ??= []).push(r);
|
|
1349
|
+
}
|
|
1350
|
+
rows.forEach(r => (r[name] = grouped[String(r[rel.localKey])] ?? []));
|
|
1351
|
+
return related;
|
|
1352
|
+
}
|
|
1353
|
+
async function loadHasOne(rows, rel, name, callback) {
|
|
1354
|
+
const ids = rows.map(r => r[rel.localKey]);
|
|
1355
|
+
if (!ids.length) {
|
|
1356
|
+
rows.forEach(r => (r[name] = null));
|
|
1357
|
+
return [];
|
|
1358
|
+
}
|
|
1359
|
+
const q = rel.model().query().whereIn(rel.foreignKey, ids);
|
|
1360
|
+
rel.callback?.(q);
|
|
1361
|
+
callback?.(q);
|
|
1362
|
+
const related = rel.model().hydrate(await q);
|
|
1363
|
+
const map = new Map(related.map((r) => [String(r[rel.foreignKey]), r]));
|
|
1364
|
+
rows.forEach(r => r[name] = map.get(String(r[rel.localKey])) ?? null);
|
|
1365
|
+
return related;
|
|
1366
|
+
}
|
|
1367
|
+
// =================================>
|
|
1368
|
+
// ## Add relation model helpers
|
|
1369
|
+
// =================================>
|
|
1370
|
+
function pushRelation(target, key, desc) {
|
|
1371
|
+
const ctor = target.constructor;
|
|
1372
|
+
if (!ctor[RELATION_META])
|
|
1373
|
+
ctor[RELATION_META] = {};
|
|
1374
|
+
ctor[RELATION_META][key] = () => desc;
|
|
1375
|
+
}
|
|
1376
|
+
// =================================>
|
|
1377
|
+
// ## Where has model helpers
|
|
1378
|
+
// =================================>
|
|
1379
|
+
function whereHasSubquery(parentQuery, Model, relations, callback, negate = false) {
|
|
1380
|
+
const relation = relations[0];
|
|
1381
|
+
const relDef = Model.relations?.[relation];
|
|
1382
|
+
if (!relDef)
|
|
1383
|
+
return;
|
|
1384
|
+
const desc = relDef();
|
|
1385
|
+
const Related = desc.model();
|
|
1386
|
+
const parentTable = Model.getTable();
|
|
1387
|
+
const relatedTable = Related.getTable();
|
|
1388
|
+
const method = negate ? 'whereNotExists' : 'whereExists';
|
|
1389
|
+
parentQuery[method](function () {
|
|
1390
|
+
this.select(1).from(relatedTable);
|
|
1391
|
+
if (desc.type === 'hasMany' || desc.type === 'hasOne') {
|
|
1392
|
+
this.whereRaw(`${relatedTable}.${desc.foreignKey} = ${parentTable}.${desc.localKey}`);
|
|
1393
|
+
}
|
|
1394
|
+
if (desc.type === 'belongsTo') {
|
|
1395
|
+
this.whereRaw(`${relatedTable}.${desc.localKey} = ${parentTable}.${desc.foreignKey}`);
|
|
1396
|
+
}
|
|
1397
|
+
if (desc.type === 'belongsToMany') {
|
|
1398
|
+
const pivot = desc.pivotTable;
|
|
1399
|
+
this.join(pivot, `${pivot}.${desc.pivotForeign}`, '=', `${relatedTable}.${Related.primaryKey}`);
|
|
1400
|
+
this.whereRaw(`${pivot}.${desc.pivotLocal} = ${parentTable}.${desc.localKey}`);
|
|
1401
|
+
}
|
|
1402
|
+
if (relations.length > 1) {
|
|
1403
|
+
whereHasSubquery(this, Related, relations.slice(1), callback, negate);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
if (callback) {
|
|
1407
|
+
const qb = Related.query().from(relatedTable);
|
|
1408
|
+
desc.callback?.(qb);
|
|
1409
|
+
callback(qb);
|
|
1410
|
+
this.whereExists(qb);
|
|
1411
|
+
}
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
// =================================>
|
|
1415
|
+
// ## Add aggregate model helpers
|
|
1416
|
+
// =================================>
|
|
1417
|
+
function applyWithAggregates(query) {
|
|
1418
|
+
const Model = query.$model;
|
|
1419
|
+
if (!Model || !query._withAggregates?.length)
|
|
1420
|
+
return;
|
|
1421
|
+
const parentTable = Model.getTable();
|
|
1422
|
+
for (const item of query._withAggregates) {
|
|
1423
|
+
const relDef = Model.relations?.[item.relation];
|
|
1424
|
+
if (!relDef)
|
|
1425
|
+
continue;
|
|
1426
|
+
const desc = relDef();
|
|
1427
|
+
const Related = desc.model();
|
|
1428
|
+
const relatedTable = Related.getTable();
|
|
1429
|
+
const fn = item.fn;
|
|
1430
|
+
const sub = db(Related.getTable())[fn](item.column);
|
|
1431
|
+
if (desc.type === 'hasMany' || desc.type === 'hasOne') {
|
|
1432
|
+
sub.whereRaw(`${relatedTable}.${desc.foreignKey} = ${parentTable}.${desc.localKey}`);
|
|
1433
|
+
}
|
|
1434
|
+
if (desc.type === 'belongsTo') {
|
|
1435
|
+
sub.whereRaw(`${relatedTable}.${desc.localKey} = ${parentTable}.${desc.foreignKey}`);
|
|
1436
|
+
}
|
|
1437
|
+
if (desc.type === 'belongsToMany') {
|
|
1438
|
+
const pivot = desc.pivotTable;
|
|
1439
|
+
sub.join(pivot, `${pivot}.${desc.pivotForeign}`, '=', `${relatedTable}.${Related.primaryKey}`);
|
|
1440
|
+
sub.whereRaw(`${pivot}.${desc.pivotLocal} = ${parentTable}.${desc.localKey}`);
|
|
1441
|
+
}
|
|
1442
|
+
desc.callback?.(sub);
|
|
1443
|
+
item.callback?.(sub);
|
|
1444
|
+
query.select(query.client.raw(`(${sub.toQuery()}) as ${item.alias}`));
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
// =================================>
|
|
1448
|
+
// ## Add order by aggregate model helpers
|
|
1449
|
+
// =================================>
|
|
1450
|
+
function applyOrderByAggregates(query) {
|
|
1451
|
+
const Model = query.$model;
|
|
1452
|
+
if (!Model || !query._orderByAggregates?.length)
|
|
1453
|
+
return;
|
|
1454
|
+
const parentTable = Model.getTable();
|
|
1455
|
+
for (const item of query._orderByAggregates) {
|
|
1456
|
+
const relDef = Model.relations?.[item.relation];
|
|
1457
|
+
if (!relDef)
|
|
1458
|
+
continue;
|
|
1459
|
+
const desc = relDef();
|
|
1460
|
+
const Related = desc.model();
|
|
1461
|
+
const relatedTable = Related.getTable();
|
|
1462
|
+
const fn = item.fn;
|
|
1463
|
+
const sub = db(Related.getTable())[fn](item.column);
|
|
1464
|
+
if (desc.type === 'hasMany') {
|
|
1465
|
+
sub.whereRaw(`${relatedTable}.${desc.foreignKey} = ${parentTable}.${desc.localKey}`);
|
|
1466
|
+
}
|
|
1467
|
+
if (desc.type === 'belongsTo') {
|
|
1468
|
+
sub.whereRaw(`${relatedTable}.${desc.localKey} = ${parentTable}.${desc.foreignKey}`);
|
|
1469
|
+
}
|
|
1470
|
+
if (Related.isSoftDelete?.()) {
|
|
1471
|
+
sub.whereNull(Related.getDeletedAtColumn());
|
|
1472
|
+
}
|
|
1473
|
+
desc.callback?.(sub);
|
|
1474
|
+
item.callback?.(sub);
|
|
1475
|
+
query.orderByRaw(`(${sub.toQuery()}) ${item.direction}`);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
// =================================>
|
|
1479
|
+
// ## Add scope model helpers
|
|
1480
|
+
// =================================>
|
|
1481
|
+
function applyScopes(query) {
|
|
1482
|
+
const Model = query.$model;
|
|
1483
|
+
if (!Model)
|
|
1484
|
+
return;
|
|
1485
|
+
const scopes = Model.scopes ?? {};
|
|
1486
|
+
const disabled = query._disabledScopes ?? new Set();
|
|
1487
|
+
for (const [name, meta] of Object.entries(scopes)) {
|
|
1488
|
+
if (meta.mode !== 'global')
|
|
1489
|
+
continue;
|
|
1490
|
+
if (disabled.has(name))
|
|
1491
|
+
continue;
|
|
1492
|
+
meta.fn.call(Model, query);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
//# sourceMappingURL=model.util.js.map
|