@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.
Files changed (59) hide show
  1. package/bun.lock +160 -0
  2. package/dist/auth.util.d.ts +23 -0
  3. package/dist/auth.util.js +175 -0
  4. package/dist/auth.util.js.map +1 -0
  5. package/dist/context.util.d.ts +7 -0
  6. package/dist/context.util.js +11 -0
  7. package/dist/context.util.js.map +1 -0
  8. package/dist/controller.util.d.ts +118 -0
  9. package/dist/controller.util.js +144 -0
  10. package/dist/controller.util.js.map +1 -0
  11. package/dist/conversion.util.d.ts +8 -0
  12. package/dist/conversion.util.js +52 -0
  13. package/dist/conversion.util.js.map +1 -0
  14. package/dist/db.util.d.ts +80 -0
  15. package/dist/db.util.js +166 -0
  16. package/dist/db.util.js.map +1 -0
  17. package/dist/index.d.ts +13 -0
  18. package/dist/index.js +14 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/logger.util.d.ts +30 -0
  21. package/dist/logger.util.js +117 -0
  22. package/dist/logger.util.js.map +1 -0
  23. package/dist/mail.util.d.ts +21 -0
  24. package/dist/mail.util.js +53 -0
  25. package/dist/mail.util.js.map +1 -0
  26. package/dist/middleware.util.d.ts +263 -0
  27. package/dist/middleware.util.js +233 -0
  28. package/dist/middleware.util.js.map +1 -0
  29. package/dist/model.util.d.ts +204 -0
  30. package/dist/model.util.js +1495 -0
  31. package/dist/model.util.js.map +1 -0
  32. package/dist/permission.util.d.ts +38 -0
  33. package/dist/permission.util.js +91 -0
  34. package/dist/permission.util.js.map +1 -0
  35. package/dist/route.util.d.ts +1 -0
  36. package/dist/route.util.js +12 -0
  37. package/dist/route.util.js.map +1 -0
  38. package/dist/storage.util.d.ts +56 -0
  39. package/dist/storage.util.js +82 -0
  40. package/dist/storage.util.js.map +1 -0
  41. package/dist/validation.util.d.ts +7 -0
  42. package/dist/validation.util.js +237 -0
  43. package/dist/validation.util.js.map +1 -0
  44. package/package.json +34 -0
  45. package/src/auth.util.ts +242 -0
  46. package/src/context.util.ts +17 -0
  47. package/src/controller.util.ts +237 -0
  48. package/src/conversion.util.ts +65 -0
  49. package/src/db.util.ts +405 -0
  50. package/src/index.ts +13 -0
  51. package/src/logger.util.ts +170 -0
  52. package/src/mail.util.ts +86 -0
  53. package/src/middleware.util.ts +289 -0
  54. package/src/model.util.ts +2211 -0
  55. package/src/permission.util.ts +136 -0
  56. package/src/route.util.ts +12 -0
  57. package/src/storage.util.ts +102 -0
  58. package/src/validation.util.ts +338 -0
  59. 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