ilana-orm 1.0.14 → 1.0.16
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/README.md +178 -115
- package/database/connection.js +21 -10
- package/database/schema-builder.js +18 -12
- package/index.js +1 -0
- package/index.mjs +1 -0
- package/orm/Factory.js +20 -18
- package/orm/Model.d.ts +26 -3
- package/orm/Model.js +159 -68
- package/orm/QueryBuilder.d.ts +24 -2
- package/orm/QueryBuilder.js +437 -75
- package/orm/Relation.d.ts +15 -3
- package/orm/Relation.js +74 -11
- package/orm/Relation.mjs +1 -1
- package/package.json +10 -9
package/orm/QueryBuilder.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const Database = require('../database/connection');
|
|
2
2
|
const Collection = require('./Collection');
|
|
3
|
+
const ModelRegistry = require('./ModelRegistry');
|
|
3
4
|
|
|
4
5
|
class QueryBuilder {
|
|
5
6
|
constructor(tableName, modelClass, connectionName) {
|
|
@@ -75,12 +76,18 @@ class QueryBuilder {
|
|
|
75
76
|
return this;
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
static _validOp(op) {
|
|
80
|
+
const allowed = new Set(['=', '!=', '<>', '<', '>', '<=', '>=']);
|
|
81
|
+
if (!allowed.has(op)) throw new Error(`Invalid SQL operator: "${op}"`);
|
|
82
|
+
return op;
|
|
83
|
+
}
|
|
84
|
+
|
|
78
85
|
whereJsonContains(column, value) {
|
|
79
86
|
const client = this.query.client.config.client;
|
|
80
87
|
if (client === 'pg') {
|
|
81
|
-
this.query.whereRaw(
|
|
88
|
+
this.query.whereRaw(`?? @> ?`, [column, JSON.stringify(value)]);
|
|
82
89
|
} else if (client === 'mysql2') {
|
|
83
|
-
this.query.whereRaw(`JSON_CONTAINS(
|
|
90
|
+
this.query.whereRaw(`JSON_CONTAINS(??, ?)`, [column, JSON.stringify(value)]);
|
|
84
91
|
} else {
|
|
85
92
|
this.query.whereJsonObject(column, value);
|
|
86
93
|
}
|
|
@@ -88,11 +95,12 @@ class QueryBuilder {
|
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
whereJsonLength(column, operator, value) {
|
|
98
|
+
QueryBuilder._validOp(operator);
|
|
91
99
|
const client = this.query.client.config.client;
|
|
92
100
|
if (client === 'pg') {
|
|
93
|
-
this.query.whereRaw(`jsonb_array_length(
|
|
101
|
+
this.query.whereRaw(`jsonb_array_length(??) ${operator} ?`, [column, value]);
|
|
94
102
|
} else if (client === 'mysql2') {
|
|
95
|
-
this.query.whereRaw(`JSON_LENGTH(
|
|
103
|
+
this.query.whereRaw(`JSON_LENGTH(??) ${operator} ?`, [column, value]);
|
|
96
104
|
} else {
|
|
97
105
|
this.query.whereJsonPath(column, '$.length()', operator, value);
|
|
98
106
|
}
|
|
@@ -101,23 +109,137 @@ class QueryBuilder {
|
|
|
101
109
|
|
|
102
110
|
whereDate(column, operatorOrValue, value) {
|
|
103
111
|
if (value === undefined) {
|
|
104
|
-
this.query.whereRaw(`DATE(
|
|
112
|
+
this.query.whereRaw(`DATE(??) = ?`, [column, operatorOrValue]);
|
|
105
113
|
} else {
|
|
106
|
-
|
|
114
|
+
QueryBuilder._validOp(operatorOrValue);
|
|
115
|
+
this.query.whereRaw(`DATE(??) ${operatorOrValue} ?`, [column, value]);
|
|
107
116
|
}
|
|
108
117
|
return this;
|
|
109
118
|
}
|
|
110
119
|
|
|
111
120
|
whereMonth(column, month) {
|
|
112
|
-
this.query.
|
|
121
|
+
const client = this.query.client?.config?.client;
|
|
122
|
+
if (client === 'pg' || client === 'postgres') {
|
|
123
|
+
this.query.whereRaw(`EXTRACT(MONTH FROM ??) = ?`, [column, month]);
|
|
124
|
+
} else {
|
|
125
|
+
this.query.whereRaw(`MONTH(??) = ?`, [column, month]);
|
|
126
|
+
}
|
|
113
127
|
return this;
|
|
114
128
|
}
|
|
115
129
|
|
|
116
130
|
whereYear(column, year) {
|
|
117
|
-
this.query.
|
|
131
|
+
const client = this.query.client?.config?.client;
|
|
132
|
+
if (client === 'pg' || client === 'postgres') {
|
|
133
|
+
this.query.whereRaw(`EXTRACT(YEAR FROM ??) = ?`, [column, year]);
|
|
134
|
+
} else {
|
|
135
|
+
this.query.whereRaw(`YEAR(??) = ?`, [column, year]);
|
|
136
|
+
}
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
whereDay(column, operatorOrValue, value) {
|
|
141
|
+
const client = this.query.client?.config?.client;
|
|
142
|
+
const isPg = client === 'pg' || client === 'postgres';
|
|
143
|
+
const isSqlite = client === 'sqlite3' || client === 'better-sqlite3';
|
|
144
|
+
if (isPg) {
|
|
145
|
+
if (value === undefined) {
|
|
146
|
+
this.query.whereRaw(`EXTRACT(DAY FROM ??) = ?`, [column, operatorOrValue]);
|
|
147
|
+
} else {
|
|
148
|
+
QueryBuilder._validOp(operatorOrValue);
|
|
149
|
+
this.query.whereRaw(`EXTRACT(DAY FROM ??) ${operatorOrValue} ?`, [column, value]);
|
|
150
|
+
}
|
|
151
|
+
} else if (isSqlite) {
|
|
152
|
+
if (value === undefined) {
|
|
153
|
+
this.query.whereRaw(`CAST(strftime('%d', ??) AS INTEGER) = ?`, [column, operatorOrValue]);
|
|
154
|
+
} else {
|
|
155
|
+
QueryBuilder._validOp(operatorOrValue);
|
|
156
|
+
this.query.whereRaw(`CAST(strftime('%d', ??) AS INTEGER) ${operatorOrValue} ?`, [column, value]);
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
if (value === undefined) {
|
|
160
|
+
this.query.whereRaw(`DAY(??) = ?`, [column, operatorOrValue]);
|
|
161
|
+
} else {
|
|
162
|
+
QueryBuilder._validOp(operatorOrValue);
|
|
163
|
+
this.query.whereRaw(`DAY(??) ${operatorOrValue} ?`, [column, value]);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return this;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
whereTime(column, operatorOrValue, value) {
|
|
170
|
+
const client = this.query.client?.config?.client;
|
|
171
|
+
const isPg = client === 'pg' || client === 'postgres';
|
|
172
|
+
const isSqlite = client === 'sqlite3' || client === 'better-sqlite3';
|
|
173
|
+
if (isPg) {
|
|
174
|
+
if (value === undefined) {
|
|
175
|
+
this.query.whereRaw(`(??)::time = ?`, [column, operatorOrValue]);
|
|
176
|
+
} else {
|
|
177
|
+
QueryBuilder._validOp(operatorOrValue);
|
|
178
|
+
this.query.whereRaw(`(??)::time ${operatorOrValue} ?`, [column, value]);
|
|
179
|
+
}
|
|
180
|
+
} else if (isSqlite) {
|
|
181
|
+
if (value === undefined) {
|
|
182
|
+
this.query.whereRaw(`strftime('%H:%M:%S', ??) = ?`, [column, operatorOrValue]);
|
|
183
|
+
} else {
|
|
184
|
+
QueryBuilder._validOp(operatorOrValue);
|
|
185
|
+
this.query.whereRaw(`strftime('%H:%M:%S', ??) ${operatorOrValue} ?`, [column, value]);
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
if (value === undefined) {
|
|
189
|
+
this.query.whereRaw(`TIME(??) = ?`, [column, operatorOrValue]);
|
|
190
|
+
} else {
|
|
191
|
+
QueryBuilder._validOp(operatorOrValue);
|
|
192
|
+
this.query.whereRaw(`TIME(??) ${operatorOrValue} ?`, [column, value]);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return this;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
whereNotBetween(column, range) {
|
|
199
|
+
this.query.whereNotBetween(column, range);
|
|
200
|
+
return this;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
orWhereNull(column) {
|
|
204
|
+
this.query.orWhereNull(column);
|
|
205
|
+
return this;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
orWhereNotNull(column) {
|
|
209
|
+
this.query.orWhereNotNull(column);
|
|
210
|
+
return this;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
orWhereIn(column, values) {
|
|
214
|
+
this.query.orWhereIn(column, values);
|
|
215
|
+
return this;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
orWhereNotIn(column, values) {
|
|
219
|
+
this.query.orWhereNotIn(column, values);
|
|
220
|
+
return this;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
orWhereRaw(sql, bindings) {
|
|
224
|
+
this.query.orWhereRaw(sql, bindings);
|
|
225
|
+
return this;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
addSelect(...columns) {
|
|
229
|
+
this.query.column(...columns);
|
|
118
230
|
return this;
|
|
119
231
|
}
|
|
120
232
|
|
|
233
|
+
inRandomOrder() {
|
|
234
|
+
const client = this.query.client?.config?.client;
|
|
235
|
+
this.query.orderByRaw(client === 'mysql2' ? 'RAND()' : 'RANDOM()');
|
|
236
|
+
return this;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
forPage(page, perPage = 15) {
|
|
240
|
+
return this.offset((page - 1) * perPage).limit(perPage);
|
|
241
|
+
}
|
|
242
|
+
|
|
121
243
|
whereExists(callback) {
|
|
122
244
|
this.query.whereExists((builder) => {
|
|
123
245
|
const subQuery = new QueryBuilder('', this.modelClass, this.connectionName);
|
|
@@ -136,6 +258,15 @@ class QueryBuilder {
|
|
|
136
258
|
return this;
|
|
137
259
|
}
|
|
138
260
|
|
|
261
|
+
unless(condition, callback, otherwise) {
|
|
262
|
+
if (!condition) {
|
|
263
|
+
callback(this);
|
|
264
|
+
} else if (otherwise) {
|
|
265
|
+
otherwise(this, condition);
|
|
266
|
+
}
|
|
267
|
+
return this;
|
|
268
|
+
}
|
|
269
|
+
|
|
139
270
|
// Joins
|
|
140
271
|
join(table, first, operator, second) {
|
|
141
272
|
this.query.join(table, first, operator, second);
|
|
@@ -152,6 +283,16 @@ class QueryBuilder {
|
|
|
152
283
|
return this;
|
|
153
284
|
}
|
|
154
285
|
|
|
286
|
+
innerJoin(table, first, operator, second) {
|
|
287
|
+
this.query.join(table, first, operator, second);
|
|
288
|
+
return this;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
crossJoin(table) {
|
|
292
|
+
this.query.crossJoin(table);
|
|
293
|
+
return this;
|
|
294
|
+
}
|
|
295
|
+
|
|
155
296
|
// Ordering and limits
|
|
156
297
|
orderBy(column, direction = 'asc') {
|
|
157
298
|
this.query.orderBy(column, direction);
|
|
@@ -184,6 +325,11 @@ class QueryBuilder {
|
|
|
184
325
|
return this.offset(count);
|
|
185
326
|
}
|
|
186
327
|
|
|
328
|
+
from(table) {
|
|
329
|
+
this.query.from(table);
|
|
330
|
+
return this;
|
|
331
|
+
}
|
|
332
|
+
|
|
187
333
|
// Grouping
|
|
188
334
|
groupBy(...columns) {
|
|
189
335
|
this.query.groupBy(...columns);
|
|
@@ -201,6 +347,20 @@ class QueryBuilder {
|
|
|
201
347
|
return this;
|
|
202
348
|
}
|
|
203
349
|
|
|
350
|
+
havingRaw(sql, bindings) {
|
|
351
|
+
this.query.havingRaw(sql, bindings);
|
|
352
|
+
return this;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
whereNotExists(callback) {
|
|
356
|
+
this.query.whereNotExists((builder) => {
|
|
357
|
+
const subQb = new QueryBuilder('', null, this.connectionName);
|
|
358
|
+
subQb.query = builder;
|
|
359
|
+
callback(subQb);
|
|
360
|
+
});
|
|
361
|
+
return this;
|
|
362
|
+
}
|
|
363
|
+
|
|
204
364
|
// Locking
|
|
205
365
|
lockForUpdate() {
|
|
206
366
|
this.query.forUpdate();
|
|
@@ -244,31 +404,40 @@ class QueryBuilder {
|
|
|
244
404
|
return this;
|
|
245
405
|
}
|
|
246
406
|
|
|
407
|
+
_softQuery() {
|
|
408
|
+
let q = this.query.clone();
|
|
409
|
+
if (this.modelClass && this.modelClass.softDeletes) {
|
|
410
|
+
const col = this.modelClass.deletedAt || 'deleted_at';
|
|
411
|
+
if (this._onlyTrashed) q = q.whereNotNull(col);
|
|
412
|
+
else if (!this._includeTrashed) q = q.whereNull(col);
|
|
413
|
+
}
|
|
414
|
+
return q;
|
|
415
|
+
}
|
|
416
|
+
|
|
247
417
|
// Aggregates
|
|
248
418
|
async count(column = '*') {
|
|
249
|
-
const result = await this.
|
|
250
|
-
// Handle different database result formats
|
|
419
|
+
const result = await this._softQuery().count(column);
|
|
251
420
|
const countValue = result[0]['count(*)'] || result[0].count || result[0]['COUNT(*)'] || result[0]['COUNT'] || 0;
|
|
252
421
|
return parseInt(countValue) || 0;
|
|
253
422
|
}
|
|
254
423
|
|
|
255
424
|
async sum(column) {
|
|
256
|
-
const result = await this.
|
|
425
|
+
const result = await this._softQuery().sum(column);
|
|
257
426
|
return parseFloat(result[0][`sum(\`${column}\`)`] || result[0].sum);
|
|
258
427
|
}
|
|
259
428
|
|
|
260
429
|
async avg(column) {
|
|
261
|
-
const result = await this.
|
|
430
|
+
const result = await this._softQuery().avg(column);
|
|
262
431
|
return parseFloat(result[0][`avg(\`${column}\`)`] || result[0].avg);
|
|
263
432
|
}
|
|
264
433
|
|
|
265
434
|
async min(column) {
|
|
266
|
-
const result = await this.
|
|
435
|
+
const result = await this._softQuery().min(column);
|
|
267
436
|
return result[0][`min(\`${column}\`)`] || result[0].min;
|
|
268
437
|
}
|
|
269
438
|
|
|
270
439
|
async max(column) {
|
|
271
|
-
const result = await this.
|
|
440
|
+
const result = await this._softQuery().max(column);
|
|
272
441
|
return result[0][`max(\`${column}\`)`] || result[0].max;
|
|
273
442
|
}
|
|
274
443
|
|
|
@@ -292,33 +461,108 @@ class QueryBuilder {
|
|
|
292
461
|
}
|
|
293
462
|
|
|
294
463
|
withCount(...relations) {
|
|
464
|
+
// Ensure regular model columns are not dropped when we add count subqueries
|
|
465
|
+
const existing = this.query._single?.columns;
|
|
466
|
+
if (!existing || existing.length === 0) {
|
|
467
|
+
this.query.select(`${this.modelClass.getTableName()}.*`);
|
|
468
|
+
}
|
|
295
469
|
for (const relation of relations) {
|
|
296
|
-
|
|
470
|
+
try {
|
|
471
|
+
const dummy = this._makeDummy();
|
|
472
|
+
if (!dummy) continue;
|
|
473
|
+
const relFn = this.modelClass.prototype[relation];
|
|
474
|
+
if (typeof relFn !== 'function') continue;
|
|
475
|
+
const rel = relFn.call(dummy);
|
|
476
|
+
const relatedClass = rel.getRelatedClass();
|
|
477
|
+
const relatedTable = relatedClass.getTableName();
|
|
478
|
+
const parentTable = this.modelClass.getTableName();
|
|
479
|
+
let countSql;
|
|
480
|
+
if (rel.constructor.name === 'BelongsTo') {
|
|
481
|
+
countSql = `(SELECT COUNT(*) FROM ${relatedTable} WHERE ${relatedTable}.${rel.localKey || relatedClass.getPrimaryKey()} = ${parentTable}.${rel.foreignKey})`;
|
|
482
|
+
} else {
|
|
483
|
+
countSql = `(SELECT COUNT(*) FROM ${relatedTable} WHERE ${relatedTable}.${rel.foreignKey} = ${parentTable}.${rel.localKey || this.modelClass.getPrimaryKey()})`;
|
|
484
|
+
}
|
|
485
|
+
this.query.column(Database.raw(`${countSql} as ${relation}_count`));
|
|
486
|
+
} catch (_) { /* skip unresolvable relations */ }
|
|
297
487
|
}
|
|
298
488
|
return this;
|
|
299
489
|
}
|
|
300
490
|
|
|
301
491
|
whereHas(relation, callback) {
|
|
302
|
-
if (
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
492
|
+
if (!this.modelClass) return this;
|
|
493
|
+
try {
|
|
494
|
+
const dummy = this._makeDummy();
|
|
495
|
+
const relFn = this.modelClass.prototype[relation];
|
|
496
|
+
if (typeof relFn !== 'function') return this;
|
|
497
|
+
const rel = relFn.call(dummy);
|
|
498
|
+
const relatedClass = rel.getRelatedClass();
|
|
499
|
+
const relatedTable = relatedClass.getTableName();
|
|
500
|
+
const parentTable = this.modelClass.getTableName();
|
|
501
|
+
const isBelongsTo = rel.constructor.name === 'BelongsTo';
|
|
502
|
+
this.query.whereExists((builder) => {
|
|
503
|
+
builder.from(relatedTable);
|
|
504
|
+
if (isBelongsTo) {
|
|
505
|
+
builder.whereRaw(`${relatedTable}.${rel.localKey || relatedClass.getPrimaryKey()} = ${parentTable}.${rel.foreignKey}`);
|
|
506
|
+
} else {
|
|
507
|
+
builder.whereRaw(`${relatedTable}.${rel.foreignKey} = ${parentTable}.${rel.localKey || this.modelClass.getPrimaryKey()}`);
|
|
508
|
+
}
|
|
509
|
+
if (callback) {
|
|
510
|
+
const subQb = new QueryBuilder(relatedTable, relatedClass, this.connectionName);
|
|
511
|
+
subQb.query = builder;
|
|
512
|
+
callback(subQb);
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
} catch (_) { /* skip if relation can't be resolved at query-build time */ }
|
|
306
516
|
return this;
|
|
307
517
|
}
|
|
308
518
|
|
|
519
|
+
whereDoesntHave(relation, callback) {
|
|
520
|
+
if (!this.modelClass) return this;
|
|
521
|
+
try {
|
|
522
|
+
const dummy = this._makeDummy();
|
|
523
|
+
const relFn = this.modelClass.prototype[relation];
|
|
524
|
+
if (typeof relFn !== 'function') return this;
|
|
525
|
+
const rel = relFn.call(dummy);
|
|
526
|
+
const relatedClass = rel.getRelatedClass();
|
|
527
|
+
const relatedTable = relatedClass.getTableName();
|
|
528
|
+
const parentTable = this.modelClass.getTableName();
|
|
529
|
+
const isBelongsTo = rel.constructor.name === 'BelongsTo';
|
|
530
|
+
this.query.whereNotExists((builder) => {
|
|
531
|
+
builder.from(relatedTable);
|
|
532
|
+
if (isBelongsTo) {
|
|
533
|
+
builder.whereRaw(`${relatedTable}.${rel.localKey || relatedClass.getPrimaryKey()} = ${parentTable}.${rel.foreignKey}`);
|
|
534
|
+
} else {
|
|
535
|
+
builder.whereRaw(`${relatedTable}.${rel.foreignKey} = ${parentTable}.${rel.localKey || this.modelClass.getPrimaryKey()}`);
|
|
536
|
+
}
|
|
537
|
+
if (typeof callback === 'function') {
|
|
538
|
+
const constraintQB = new QueryBuilder(relatedTable, relatedClass, this.connectionName);
|
|
539
|
+
constraintQB.query = builder;
|
|
540
|
+
callback(constraintQB);
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
} catch (_) {}
|
|
544
|
+
return this;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
doesntHave(relation) {
|
|
548
|
+
return this.whereDoesntHave(relation);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
_makeDummy() {
|
|
552
|
+
const dummy = Object.create(this.modelClass.prototype);
|
|
553
|
+
dummy.attributes = {};
|
|
554
|
+
dummy.relations = {};
|
|
555
|
+
dummy.casts = this.modelClass.casts || {};
|
|
556
|
+
dummy.fillable = this.modelClass.fillable || [];
|
|
557
|
+
dummy.guarded = this.modelClass.guarded || ['*'];
|
|
558
|
+
dummy.appends = this.modelClass.appends || [];
|
|
559
|
+
dummy.constructor = this.modelClass;
|
|
560
|
+
return dummy;
|
|
561
|
+
}
|
|
562
|
+
|
|
309
563
|
// Execution methods
|
|
310
564
|
async get() {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
// Apply soft delete filtering if model has soft deletes
|
|
314
|
-
if (this.modelClass && this.modelClass.softDeletes) {
|
|
315
|
-
if (this._onlyTrashed) {
|
|
316
|
-
query = query.whereNotNull('deleted_at');
|
|
317
|
-
} else if (!this._includeTrashed) {
|
|
318
|
-
query = query.whereNull('deleted_at');
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
565
|
+
const query = this._softQuery();
|
|
322
566
|
const rows = await query;
|
|
323
567
|
const models = rows.map(row => {
|
|
324
568
|
const model = new this.modelClass(row);
|
|
@@ -333,18 +577,7 @@ class QueryBuilder {
|
|
|
333
577
|
}
|
|
334
578
|
|
|
335
579
|
async first() {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
// Apply soft delete filtering if model has soft deletes
|
|
339
|
-
if (this.modelClass && this.modelClass.softDeletes) {
|
|
340
|
-
if (this._onlyTrashed) {
|
|
341
|
-
query = query.whereNotNull('deleted_at');
|
|
342
|
-
} else if (!this._includeTrashed) {
|
|
343
|
-
query = query.whereNull('deleted_at');
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const row = await query.first();
|
|
580
|
+
const row = await this._softQuery().first();
|
|
348
581
|
if (!row) return null;
|
|
349
582
|
const model = new this.modelClass(row);
|
|
350
583
|
model.exists = true;
|
|
@@ -357,18 +590,8 @@ class QueryBuilder {
|
|
|
357
590
|
|
|
358
591
|
|
|
359
592
|
async find(id) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
// Apply soft delete filtering if model has soft deletes
|
|
363
|
-
if (this.modelClass && this.modelClass.softDeletes) {
|
|
364
|
-
if (this._onlyTrashed) {
|
|
365
|
-
query = query.whereNotNull('deleted_at');
|
|
366
|
-
} else if (!this._includeTrashed) {
|
|
367
|
-
query = query.whereNull('deleted_at');
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const result = await query.first();
|
|
593
|
+
const pk = this.modelClass?.primaryKey || 'id';
|
|
594
|
+
const result = await this._softQuery().where(pk, id).first();
|
|
372
595
|
if (!result) return null;
|
|
373
596
|
|
|
374
597
|
const model = this.modelClass ? new this.modelClass(result) : result;
|
|
@@ -386,27 +609,37 @@ class QueryBuilder {
|
|
|
386
609
|
|
|
387
610
|
async findOrFail(id) {
|
|
388
611
|
const result = await this.find(id);
|
|
389
|
-
if (!result) {
|
|
390
|
-
throw new Error(`Model not found with id: ${id}`);
|
|
391
|
-
}
|
|
612
|
+
if (!result) throw new Error(`Model not found with id: ${id}`);
|
|
392
613
|
return result;
|
|
393
614
|
}
|
|
394
615
|
|
|
616
|
+
async firstOrFail() {
|
|
617
|
+
const result = await this.first();
|
|
618
|
+
if (!result) throw new Error('No records found.');
|
|
619
|
+
return result;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
async doesntExist() {
|
|
623
|
+
return !(await this.exists());
|
|
624
|
+
}
|
|
625
|
+
|
|
395
626
|
async pluck(column) {
|
|
396
|
-
return await this.
|
|
627
|
+
return await this._softQuery().pluck(column);
|
|
397
628
|
}
|
|
398
629
|
|
|
399
630
|
async exists() {
|
|
400
|
-
const result = await this.
|
|
631
|
+
const result = await this._softQuery().select(Database.raw('1')).first();
|
|
401
632
|
return !!result;
|
|
402
633
|
}
|
|
403
634
|
|
|
404
635
|
// Pagination
|
|
405
636
|
async paginate(page = 1, perPage = 15) {
|
|
406
|
-
// Get total count
|
|
637
|
+
// Get total count — use _softQuery() to respect soft-delete scope
|
|
407
638
|
const countQuery = new QueryBuilder(this.query._single.table, this.modelClass, this.connectionName);
|
|
408
|
-
countQuery.query = this.
|
|
409
|
-
|
|
639
|
+
countQuery.query = this._softQuery();
|
|
640
|
+
countQuery._includeTrashed = this._includeTrashed;
|
|
641
|
+
countQuery._onlyTrashed = this._onlyTrashed;
|
|
642
|
+
const total = await countQuery.query.count('* as count').then(r => parseInt(r[0]?.count || r[0]?.['count(*)'] || 0));
|
|
410
643
|
|
|
411
644
|
// Get the actual data with limit and offset
|
|
412
645
|
const results = await this.clone().offset((page - 1) * perPage).limit(perPage).get();
|
|
@@ -472,7 +705,7 @@ class QueryBuilder {
|
|
|
472
705
|
let results;
|
|
473
706
|
|
|
474
707
|
do {
|
|
475
|
-
results = await this.offset((page - 1) * size).limit(size).get();
|
|
708
|
+
results = await this.clone().offset((page - 1) * size).limit(size).get();
|
|
476
709
|
if (results.length > 0) {
|
|
477
710
|
await callback(results);
|
|
478
711
|
}
|
|
@@ -570,6 +803,121 @@ class QueryBuilder {
|
|
|
570
803
|
throw new Error(`Invalid related class for relation '${relation}'. Make sure the model is properly defined and registered.`);
|
|
571
804
|
}
|
|
572
805
|
|
|
806
|
+
// HasManyThrough requires a JOIN through the intermediate table
|
|
807
|
+
if (relationInstance.constructor.name === 'HasManyThrough') {
|
|
808
|
+
const rel = relationInstance;
|
|
809
|
+
const throughClass = ModelRegistry.has(rel.through) ? ModelRegistry.get(rel.through) : null;
|
|
810
|
+
const throughTable = throughClass ? throughClass.getTableName() : (typeof rel.through === 'string' ? rel.through : rel.through.getTableName());
|
|
811
|
+
const relatedTable = relatedClass.getTableName();
|
|
812
|
+
const parentTable = models[0].constructor.getTableName();
|
|
813
|
+
|
|
814
|
+
let hmtQuery = new QueryBuilder(relatedTable, relatedClass, relatedClass.getConnectionName());
|
|
815
|
+
hmtQuery.query = hmtQuery.query
|
|
816
|
+
.select(`${relatedTable}.*`, `${throughTable}.${rel.firstKey} as _hmt_parent_id`)
|
|
817
|
+
.join(throughTable, `${relatedTable}.${rel.secondKey}`, `${throughTable}.${rel.secondLocalKey}`)
|
|
818
|
+
.whereIn(`${throughTable}.${rel.firstKey}`, localValues);
|
|
819
|
+
|
|
820
|
+
if (this.eagerLoadConstraints[relationName]) this.eagerLoadConstraints[relationName](hmtQuery);
|
|
821
|
+
if (nested) hmtQuery = hmtQuery.with(nested);
|
|
822
|
+
|
|
823
|
+
const hmtResults = await hmtQuery.get();
|
|
824
|
+
const hmtArray = Array.isArray(hmtResults) ? hmtResults : [...hmtResults];
|
|
825
|
+
|
|
826
|
+
const grouped = {};
|
|
827
|
+
for (const m of hmtArray) {
|
|
828
|
+
const pid = m.attributes['_hmt_parent_id'];
|
|
829
|
+
delete m.attributes['_hmt_parent_id'];
|
|
830
|
+
if (!grouped[pid]) grouped[pid] = [];
|
|
831
|
+
grouped[pid].push(m);
|
|
832
|
+
}
|
|
833
|
+
for (const model of models) {
|
|
834
|
+
model.relations[relation] = grouped[model.getAttribute(rel.localKey)] || [];
|
|
835
|
+
}
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// MorphTo: polymorphic parent — group by type and batch-load each model class
|
|
840
|
+
if (relationInstance.constructor.name === 'MorphTo') {
|
|
841
|
+
const rel = relationInstance;
|
|
842
|
+
const typeGroups = {};
|
|
843
|
+
for (const model of models) {
|
|
844
|
+
const morphType = model.getAttribute(rel.morphType);
|
|
845
|
+
const morphId = model.getAttribute(rel.morphId);
|
|
846
|
+
if (!morphType || morphId == null) continue;
|
|
847
|
+
if (!typeGroups[morphType]) typeGroups[morphType] = [];
|
|
848
|
+
typeGroups[morphType].push(morphId);
|
|
849
|
+
}
|
|
850
|
+
const resolved = {};
|
|
851
|
+
for (const [typeName, ids] of Object.entries(typeGroups)) {
|
|
852
|
+
const TypeClass = ModelRegistry.get(typeName);
|
|
853
|
+
if (!TypeClass) continue;
|
|
854
|
+
const rows = await new QueryBuilder(TypeClass.getTableName(), TypeClass, TypeClass.getConnectionName())
|
|
855
|
+
.whereIn(TypeClass.getPrimaryKey(), ids).get();
|
|
856
|
+
for (const row of (Array.isArray(rows) ? rows : [...rows])) {
|
|
857
|
+
resolved[`${typeName}:${row.getAttribute(TypeClass.getPrimaryKey())}`] = row;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
for (const model of models) {
|
|
861
|
+
const morphType = model.getAttribute(rel.morphType);
|
|
862
|
+
const morphId = model.getAttribute(rel.morphId);
|
|
863
|
+
model.relations[relation] = resolved[`${morphType}:${morphId}`] || null;
|
|
864
|
+
}
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// BelongsToMany requires a join through the pivot table
|
|
869
|
+
if (relationInstance.constructor.name === 'BelongsToMany') {
|
|
870
|
+
const rel = relationInstance;
|
|
871
|
+
const parentPivotKey = rel.parentPivotKey;
|
|
872
|
+
const relatedPivotKey = rel.relatedPivotKey;
|
|
873
|
+
const pivotTable = rel.pivotTable;
|
|
874
|
+
const relatedTable = relatedClass.getTableName();
|
|
875
|
+
const parentKey = rel.parentKey || 'id';
|
|
876
|
+
const relatedKey = rel.relatedKey || relatedClass.getPrimaryKey();
|
|
877
|
+
|
|
878
|
+
const parentIds = models.map(m => m.getAttribute(parentKey)).filter(v => v != null);
|
|
879
|
+
const selectColumns = [
|
|
880
|
+
`${relatedTable}.*`,
|
|
881
|
+
`${pivotTable}.${parentPivotKey} as _pivot_parent_id`,
|
|
882
|
+
...(rel.pivotColumns || []).map(c => `${pivotTable}.${c} as pivot_${c}`)
|
|
883
|
+
];
|
|
884
|
+
|
|
885
|
+
let pivotQuery = new QueryBuilder(relatedTable, relatedClass, relatedClass.getConnectionName());
|
|
886
|
+
pivotQuery.query = pivotQuery.query
|
|
887
|
+
.select(selectColumns)
|
|
888
|
+
.join(pivotTable, `${relatedTable}.${relatedKey}`, `${pivotTable}.${relatedPivotKey}`)
|
|
889
|
+
.whereIn(`${pivotTable}.${parentPivotKey}`, parentIds);
|
|
890
|
+
|
|
891
|
+
if (this.eagerLoadConstraints[relationName]) {
|
|
892
|
+
this.eagerLoadConstraints[relationName](pivotQuery);
|
|
893
|
+
}
|
|
894
|
+
if (nested) pivotQuery = pivotQuery.with(nested);
|
|
895
|
+
|
|
896
|
+
const pivotResults = await pivotQuery.get();
|
|
897
|
+
const pivotArray = Array.isArray(pivotResults) ? pivotResults : [...pivotResults];
|
|
898
|
+
|
|
899
|
+
const grouped = {};
|
|
900
|
+
for (const m of pivotArray) {
|
|
901
|
+
const parentId = m.attributes['_pivot_parent_id'];
|
|
902
|
+
delete m.attributes['_pivot_parent_id'];
|
|
903
|
+
// Extract pivot columns
|
|
904
|
+
if (rel.pivotColumns && rel.pivotColumns.length) {
|
|
905
|
+
m.pivot = {};
|
|
906
|
+
for (const c of rel.pivotColumns) {
|
|
907
|
+
m.pivot[c] = m.attributes[`pivot_${c}`];
|
|
908
|
+
delete m.attributes[`pivot_${c}`];
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
if (!grouped[parentId]) grouped[parentId] = [];
|
|
912
|
+
grouped[parentId].push(m);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
for (const model of models) {
|
|
916
|
+
model.relations[relation] = grouped[model.getAttribute(parentKey)] || [];
|
|
917
|
+
}
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
|
|
573
921
|
let relationQuery = new QueryBuilder(
|
|
574
922
|
relatedClass.getTableName(),
|
|
575
923
|
relatedClass,
|
|
@@ -580,6 +928,11 @@ class QueryBuilder {
|
|
|
580
928
|
if (relationInstance.constructor.name === 'BelongsTo') {
|
|
581
929
|
const foreignValues = models.map(model => model.getAttribute(foreignKey)).filter(Boolean);
|
|
582
930
|
relationQuery = relationQuery.whereIn(relatedClass.getPrimaryKey(), foreignValues);
|
|
931
|
+
} else if (relationInstance.constructor.name === 'MorphMany' || relationInstance.constructor.name === 'MorphOne') {
|
|
932
|
+
const rel = relationInstance;
|
|
933
|
+
relationQuery = relationQuery
|
|
934
|
+
.where(rel.morphType, rel.morphClass)
|
|
935
|
+
.whereIn(rel.morphId, localValues);
|
|
583
936
|
} else {
|
|
584
937
|
relationQuery = relationQuery.whereIn(foreignKey, localValues);
|
|
585
938
|
}
|
|
@@ -597,24 +950,31 @@ class QueryBuilder {
|
|
|
597
950
|
// Convert Collection to array if needed
|
|
598
951
|
const relatedArray = Array.isArray(relatedModels) ? relatedModels : (relatedModels && relatedModels.length !== undefined ? [...relatedModels] : []);
|
|
599
952
|
|
|
953
|
+
const isBelongsTo = relationInstance.constructor.name === 'BelongsTo';
|
|
954
|
+
const isMorphMany = relationInstance.constructor.name === 'MorphMany';
|
|
955
|
+
const isMorphOne = relationInstance.constructor.name === 'MorphOne';
|
|
956
|
+
|
|
600
957
|
const grouped = {};
|
|
601
|
-
for (
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
958
|
+
for (const relatedModel of relatedArray) {
|
|
959
|
+
let groupKey;
|
|
960
|
+
if (isBelongsTo) {
|
|
961
|
+
groupKey = relatedModel.getAttribute(relatedClass.getPrimaryKey());
|
|
962
|
+
} else if (isMorphMany || isMorphOne) {
|
|
963
|
+
groupKey = relatedModel.getAttribute(relationInstance.morphId);
|
|
964
|
+
} else {
|
|
965
|
+
groupKey = relatedModel.getAttribute(foreignKey);
|
|
966
|
+
}
|
|
967
|
+
if (!grouped[groupKey]) grouped[groupKey] = [];
|
|
968
|
+
grouped[groupKey].push(relatedModel);
|
|
606
969
|
}
|
|
607
970
|
|
|
608
971
|
for (const model of models) {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
} else {
|
|
613
|
-
matchValue = model.getAttribute(localKey);
|
|
614
|
-
}
|
|
972
|
+
const matchValue = isBelongsTo
|
|
973
|
+
? model.getAttribute(foreignKey)
|
|
974
|
+
: model.getAttribute(localKey);
|
|
615
975
|
const related = grouped[matchValue] || [];
|
|
616
976
|
|
|
617
|
-
if (relationInstance.constructor.name === 'HasOne' ||
|
|
977
|
+
if (relationInstance.constructor.name === 'HasOne' || isBelongsTo || isMorphOne) {
|
|
618
978
|
model.relations[relation] = related[0] || null;
|
|
619
979
|
} else {
|
|
620
980
|
model.relations[relation] = related;
|
|
@@ -647,6 +1007,8 @@ class QueryBuilder {
|
|
|
647
1007
|
cloned.query = this.query.clone();
|
|
648
1008
|
cloned.eagerLoad = [...this.eagerLoad];
|
|
649
1009
|
cloned.eagerLoadConstraints = { ...this.eagerLoadConstraints };
|
|
1010
|
+
if (this._includeTrashed) cloned._includeTrashed = true;
|
|
1011
|
+
if (this._onlyTrashed) cloned._onlyTrashed = true;
|
|
650
1012
|
return cloned;
|
|
651
1013
|
}
|
|
652
1014
|
|