ilana-orm 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/README.md +4763 -0
- package/cli/ilana.js +928 -0
- package/database/DB.js +85 -0
- package/database/connection.js +114 -0
- package/database/schema-builder.js +219 -0
- package/ilana.config.js +61 -0
- package/ilana.png +0 -0
- package/index.js +34 -0
- package/orm/Collection.js +283 -0
- package/orm/CustomCasts.js +75 -0
- package/orm/Factory.js +372 -0
- package/orm/MigrationRunner.js +458 -0
- package/orm/Model.js +607 -0
- package/orm/ModelRegistry.js +26 -0
- package/orm/QueryBuilder.js +680 -0
- package/orm/Relation.js +299 -0
- package/orm/Seeder.js +153 -0
- package/package.json +71 -0
- package/test-role.js +26 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
const Database = require('../database/connection');
|
|
2
|
+
const Collection = require('./Collection');
|
|
3
|
+
|
|
4
|
+
class QueryBuilder {
|
|
5
|
+
constructor(tableName, modelClass, connectionName) {
|
|
6
|
+
this.query = Database.table(tableName, connectionName);
|
|
7
|
+
this.modelClass = modelClass;
|
|
8
|
+
this.connectionName = connectionName;
|
|
9
|
+
this.eagerLoad = [];
|
|
10
|
+
this.eagerLoadConstraints = {};
|
|
11
|
+
this._transaction = null;
|
|
12
|
+
|
|
13
|
+
// Return proxy to handle scope methods
|
|
14
|
+
return new Proxy(this, {
|
|
15
|
+
get(target, prop) {
|
|
16
|
+
if (prop in target) {
|
|
17
|
+
return target[prop];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Check if it's a scope method
|
|
21
|
+
const scopeMethod = `scope${prop.charAt(0).toUpperCase()}${prop.slice(1)}`;
|
|
22
|
+
if (target.modelClass && typeof target.modelClass[scopeMethod] === 'function') {
|
|
23
|
+
return function(...args) {
|
|
24
|
+
target.modelClass[scopeMethod](target, ...args);
|
|
25
|
+
return target;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return target[prop];
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Where clauses
|
|
35
|
+
where(column, operator, value) {
|
|
36
|
+
if (arguments.length === 2) {
|
|
37
|
+
this.query.where(column, operator);
|
|
38
|
+
} else {
|
|
39
|
+
this.query.where(column, operator, value);
|
|
40
|
+
}
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
orWhere(column, operator, value) {
|
|
45
|
+
if (arguments.length === 2) {
|
|
46
|
+
this.query.orWhere(column, operator);
|
|
47
|
+
} else {
|
|
48
|
+
this.query.orWhere(column, operator, value);
|
|
49
|
+
}
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
whereIn(column, values) {
|
|
54
|
+
this.query.whereIn(column, values);
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
whereNotIn(column, values) {
|
|
59
|
+
this.query.whereNotIn(column, values);
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
whereNull(column) {
|
|
64
|
+
this.query.whereNull(column);
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
whereNotNull(column) {
|
|
69
|
+
this.query.whereNotNull(column);
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
whereBetween(column, range) {
|
|
74
|
+
this.query.whereBetween(column, range);
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
whereJsonContains(column, value) {
|
|
79
|
+
const client = this.query.client.config.client;
|
|
80
|
+
if (client === 'pg') {
|
|
81
|
+
this.query.whereRaw(`${column} @> ?`, [JSON.stringify(value)]);
|
|
82
|
+
} else if (client === 'mysql2') {
|
|
83
|
+
this.query.whereRaw(`JSON_CONTAINS(${column}, ?)`, [JSON.stringify(value)]);
|
|
84
|
+
} else {
|
|
85
|
+
this.query.whereJsonObject(column, value);
|
|
86
|
+
}
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
whereJsonLength(column, operator, value) {
|
|
91
|
+
const client = this.query.client.config.client;
|
|
92
|
+
if (client === 'pg') {
|
|
93
|
+
this.query.whereRaw(`jsonb_array_length(${column}) ${operator} ?`, [value]);
|
|
94
|
+
} else if (client === 'mysql2') {
|
|
95
|
+
this.query.whereRaw(`JSON_LENGTH(${column}) ${operator} ?`, [value]);
|
|
96
|
+
} else {
|
|
97
|
+
this.query.whereJsonPath(column, '$.length()', operator, value);
|
|
98
|
+
}
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
whereDate(column, operatorOrValue, value) {
|
|
103
|
+
if (value === undefined) {
|
|
104
|
+
this.query.whereRaw(`DATE(${column}) = ?`, [operatorOrValue]);
|
|
105
|
+
} else {
|
|
106
|
+
this.query.whereRaw(`DATE(${column}) ${operatorOrValue} ?`, [value]);
|
|
107
|
+
}
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
whereMonth(column, month) {
|
|
112
|
+
this.query.whereRaw(`MONTH(${column}) = ?`, [month]);
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
whereYear(column, year) {
|
|
117
|
+
this.query.whereRaw(`YEAR(${column}) = ?`, [year]);
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
whereExists(callback) {
|
|
122
|
+
this.query.whereExists((builder) => {
|
|
123
|
+
const subQuery = new QueryBuilder('', this.modelClass, this.connectionName);
|
|
124
|
+
subQuery.query = builder;
|
|
125
|
+
callback(subQuery);
|
|
126
|
+
});
|
|
127
|
+
return this;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
when(condition, callback, otherwise) {
|
|
131
|
+
if (condition) {
|
|
132
|
+
callback(this, condition);
|
|
133
|
+
} else if (otherwise) {
|
|
134
|
+
otherwise(this);
|
|
135
|
+
}
|
|
136
|
+
return this;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Joins
|
|
140
|
+
join(table, first, operator, second) {
|
|
141
|
+
this.query.join(table, first, operator, second);
|
|
142
|
+
return this;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
leftJoin(table, first, operator, second) {
|
|
146
|
+
this.query.leftJoin(table, first, operator, second);
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
rightJoin(table, first, operator, second) {
|
|
151
|
+
this.query.rightJoin(table, first, operator, second);
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Ordering and limits
|
|
156
|
+
orderBy(column, direction = 'asc') {
|
|
157
|
+
this.query.orderBy(column, direction);
|
|
158
|
+
return this;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
latest(column = 'created_at') {
|
|
162
|
+
return this.orderBy(column, 'desc');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
oldest(column = 'created_at') {
|
|
166
|
+
return this.orderBy(column, 'asc');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
limit(count) {
|
|
170
|
+
this.query.limit(count);
|
|
171
|
+
return this;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
offset(count) {
|
|
175
|
+
this.query.offset(count);
|
|
176
|
+
return this;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
take(count) {
|
|
180
|
+
return this.limit(count);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
skip(count) {
|
|
184
|
+
return this.offset(count);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Grouping
|
|
188
|
+
groupBy(...columns) {
|
|
189
|
+
this.query.groupBy(...columns);
|
|
190
|
+
return this;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
having(column, operator, value) {
|
|
194
|
+
if (arguments.length === 1) {
|
|
195
|
+
this.query.havingRaw(column);
|
|
196
|
+
} else if (arguments.length === 3) {
|
|
197
|
+
this.query.having(column, operator, value);
|
|
198
|
+
} else {
|
|
199
|
+
throw new Error('Invalid arguments for having(): expected 1 or 3 arguments.');
|
|
200
|
+
}
|
|
201
|
+
return this;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Locking
|
|
205
|
+
lockForUpdate() {
|
|
206
|
+
this.query.forUpdate();
|
|
207
|
+
return this;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
sharedLock() {
|
|
211
|
+
this.query.forShare();
|
|
212
|
+
return this;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
skipLocked() {
|
|
216
|
+
this.query.skipLocked();
|
|
217
|
+
return this;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
noWait() {
|
|
221
|
+
this.query.noWait();
|
|
222
|
+
return this;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Selection
|
|
226
|
+
select(...columns) {
|
|
227
|
+
this.query.select(...columns);
|
|
228
|
+
return this;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
distinct() {
|
|
232
|
+
this.query.distinct();
|
|
233
|
+
return this;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Raw queries
|
|
237
|
+
whereRaw(sql, bindings) {
|
|
238
|
+
this.query.whereRaw(sql, bindings);
|
|
239
|
+
return this;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
selectRaw(sql, bindings) {
|
|
243
|
+
this.query.select(Database.raw(sql, bindings));
|
|
244
|
+
return this;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Aggregates
|
|
248
|
+
async count(column = '*') {
|
|
249
|
+
const result = await this.query.count(column);
|
|
250
|
+
// Handle different database result formats
|
|
251
|
+
const countValue = result[0]['count(*)'] || result[0].count || result[0]['COUNT(*)'] || result[0]['COUNT'] || 0;
|
|
252
|
+
return parseInt(countValue) || 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async sum(column) {
|
|
256
|
+
const result = await this.query.sum(column);
|
|
257
|
+
return parseFloat(result[0][`sum(\`${column}\`)`] || result[0].sum);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async avg(column) {
|
|
261
|
+
const result = await this.query.avg(column);
|
|
262
|
+
return parseFloat(result[0][`avg(\`${column}\`)`] || result[0].avg);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async min(column) {
|
|
266
|
+
const result = await this.query.min(column);
|
|
267
|
+
return result[0][`min(\`${column}\`)`] || result[0].min;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async max(column) {
|
|
271
|
+
const result = await this.query.max(column);
|
|
272
|
+
return result[0][`max(\`${column}\`)`] || result[0].max;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Eager loading
|
|
276
|
+
with(...relations) {
|
|
277
|
+
this.eagerLoad.push(...relations);
|
|
278
|
+
return this;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
withConstraints(relation, callback) {
|
|
282
|
+
if (typeof relation === 'string' && callback) {
|
|
283
|
+
this.eagerLoadConstraints[relation] = callback;
|
|
284
|
+
this.with(relation);
|
|
285
|
+
} else if (typeof relation === 'object') {
|
|
286
|
+
for (const [rel, cb] of Object.entries(relation)) {
|
|
287
|
+
this.eagerLoadConstraints[rel] = cb;
|
|
288
|
+
this.with(rel);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return this;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
withCount(...relations) {
|
|
295
|
+
for (const relation of relations) {
|
|
296
|
+
this.eagerLoad.push(`${relation}_count`);
|
|
297
|
+
}
|
|
298
|
+
return this;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
whereHas(relation, callback) {
|
|
302
|
+
if (callback) {
|
|
303
|
+
const subQuery = new QueryBuilder('', this.modelClass, this.connectionName);
|
|
304
|
+
callback(subQuery);
|
|
305
|
+
}
|
|
306
|
+
return this;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Execution methods
|
|
310
|
+
async get() {
|
|
311
|
+
let query = this.query;
|
|
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
|
+
|
|
322
|
+
const rows = await query;
|
|
323
|
+
const models = rows.map(row => {
|
|
324
|
+
const model = new this.modelClass(row);
|
|
325
|
+
model.exists = true;
|
|
326
|
+
model._initialize();
|
|
327
|
+
return model;
|
|
328
|
+
});
|
|
329
|
+
if (this.eagerLoad.length) {
|
|
330
|
+
await this.loadRelations(models);
|
|
331
|
+
}
|
|
332
|
+
return new Collection(models);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async first() {
|
|
336
|
+
let query = this.query;
|
|
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();
|
|
348
|
+
if (!row) return null;
|
|
349
|
+
const model = new this.modelClass(row);
|
|
350
|
+
model.exists = true;
|
|
351
|
+
model._initialize();
|
|
352
|
+
if (this.eagerLoad.length) {
|
|
353
|
+
await this.loadRelations([model]);
|
|
354
|
+
}
|
|
355
|
+
return model;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
async find(id) {
|
|
360
|
+
let query = this.query.where('id', id);
|
|
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();
|
|
372
|
+
if (!result) return null;
|
|
373
|
+
|
|
374
|
+
const model = this.modelClass ? new this.modelClass(result) : result;
|
|
375
|
+
if (this.modelClass) {
|
|
376
|
+
model.exists = true;
|
|
377
|
+
model._initialize();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (this.eagerLoad.length > 0 && this.modelClass) {
|
|
381
|
+
await this.loadRelations([model]);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return model;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async findOrFail(id) {
|
|
388
|
+
const result = await this.find(id);
|
|
389
|
+
if (!result) {
|
|
390
|
+
throw new Error(`Model not found with id: ${id}`);
|
|
391
|
+
}
|
|
392
|
+
return result;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async pluck(column) {
|
|
396
|
+
return await this.query.pluck(column);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async exists() {
|
|
400
|
+
const result = await this.query.select(Database.raw('1')).first();
|
|
401
|
+
return !!result;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Pagination
|
|
405
|
+
async paginate(page = 1, perPage = 15) {
|
|
406
|
+
// Get total count first with a fresh query
|
|
407
|
+
const countQuery = new QueryBuilder(this.query._single.table, this.modelClass, this.connectionName);
|
|
408
|
+
countQuery.query = this.query.clone();
|
|
409
|
+
const total = await countQuery.count();
|
|
410
|
+
|
|
411
|
+
// Get the actual data with limit and offset
|
|
412
|
+
const results = await this.clone().offset((page - 1) * perPage).limit(perPage).get();
|
|
413
|
+
|
|
414
|
+
const lastPage = Math.ceil(total / perPage) || 1;
|
|
415
|
+
const hasData = results.length > 0;
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
data: results,
|
|
419
|
+
total,
|
|
420
|
+
perPage,
|
|
421
|
+
currentPage: page,
|
|
422
|
+
lastPage,
|
|
423
|
+
from: hasData ? (page - 1) * perPage + 1 : null,
|
|
424
|
+
to: hasData ? Math.min((page - 1) * perPage + results.length, total) : null,
|
|
425
|
+
nextPage: page < lastPage ? page + 1 : null
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async simplePaginate(page = 1, perPage = 15) {
|
|
430
|
+
const results = await this.offset((page - 1) * perPage).limit(perPage + 1).get();
|
|
431
|
+
const hasMore = results.length > perPage;
|
|
432
|
+
|
|
433
|
+
if (hasMore) {
|
|
434
|
+
results.pop();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return { data: results, hasMore };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async cursorPaginate(perPage = 15, cursor, column = 'id', direction = 'asc') {
|
|
441
|
+
let query = this.clone().orderBy(column, direction);
|
|
442
|
+
|
|
443
|
+
if (cursor) {
|
|
444
|
+
const operator = direction === 'asc' ? '>' : '<';
|
|
445
|
+
query = query.where(column, operator, cursor);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const results = await query.limit(perPage + 1).get();
|
|
449
|
+
const hasNextPage = results.length > perPage;
|
|
450
|
+
|
|
451
|
+
if (hasNextPage) {
|
|
452
|
+
results.pop();
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const nextCursor = results.length > 0 ? results[results.length - 1][column] : undefined;
|
|
456
|
+
const prevCursor = results.length > 0 ? results[0][column] : undefined;
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
data: results,
|
|
460
|
+
nextCursor: hasNextPage ? String(nextCursor) : undefined,
|
|
461
|
+
prevCursor: cursor ? String(prevCursor) : undefined,
|
|
462
|
+
hasNextPage,
|
|
463
|
+
hasPrevPage: !!cursor,
|
|
464
|
+
path: '',
|
|
465
|
+
perPage
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Chunking
|
|
470
|
+
async chunk(size, callback) {
|
|
471
|
+
let page = 1;
|
|
472
|
+
let results;
|
|
473
|
+
|
|
474
|
+
do {
|
|
475
|
+
results = await this.offset((page - 1) * size).limit(size).get();
|
|
476
|
+
if (results.length > 0) {
|
|
477
|
+
await callback(results);
|
|
478
|
+
}
|
|
479
|
+
page++;
|
|
480
|
+
} while (results.length === size);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async *cursor(chunkSize = 1000) {
|
|
484
|
+
let offset = 0;
|
|
485
|
+
let hasMore = true;
|
|
486
|
+
|
|
487
|
+
while (hasMore) {
|
|
488
|
+
const results = await this.clone().offset(offset).limit(chunkSize).get();
|
|
489
|
+
hasMore = results.length === chunkSize;
|
|
490
|
+
|
|
491
|
+
for (const result of results) {
|
|
492
|
+
yield result;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
offset += chunkSize;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async *lazy(chunkSize = 1000) {
|
|
500
|
+
yield* this.cursor(chunkSize);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Insert/Update/Delete
|
|
504
|
+
async insert(data) {
|
|
505
|
+
return this.query.insert(data);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async insertGetId(data) {
|
|
509
|
+
// Use the second argument for 'returning'. Knex handles this across
|
|
510
|
+
// different dialects without issuing warnings for MySQL.
|
|
511
|
+
const result = await this.query.insert(data, this.modelClass.getPrimaryKey());
|
|
512
|
+
|
|
513
|
+
// The result format differs between DBs.
|
|
514
|
+
// - MySQL/SQLite: [123]
|
|
515
|
+
// - Postgres: [{id: 123}]
|
|
516
|
+
// This handles both cases.
|
|
517
|
+
if (result && result.length > 0) {
|
|
518
|
+
const firstItem = result[0];
|
|
519
|
+
return typeof firstItem === 'object' ? firstItem[this.modelClass.getPrimaryKey()] : firstItem;
|
|
520
|
+
}
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async update(data) {
|
|
525
|
+
return this.query.update(data);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async delete() {
|
|
529
|
+
return this.query.del();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async upsert(data, uniqueBy, update) {
|
|
533
|
+
return this.query.insert(data).onConflict(uniqueBy).merge(update);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Eager loading implementation
|
|
537
|
+
async loadRelations(models) {
|
|
538
|
+
if (models.length === 0) return;
|
|
539
|
+
|
|
540
|
+
for (const relationName of this.eagerLoad) {
|
|
541
|
+
await this.loadRelation(models, relationName);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
async loadRelation(models, relationName) {
|
|
546
|
+
const parts = relationName.split('.');
|
|
547
|
+
const relation = parts[0];
|
|
548
|
+
const nested = parts.slice(1).join('.');
|
|
549
|
+
|
|
550
|
+
const firstModel = models[0];
|
|
551
|
+
const relationMethod = firstModel[relation];
|
|
552
|
+
|
|
553
|
+
if (typeof relationMethod !== 'function') {
|
|
554
|
+
throw new Error(`Relation '${relation}' not found on model`);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const relationInstance = relationMethod.call(firstModel);
|
|
558
|
+
const foreignKey = relationInstance.foreignKey;
|
|
559
|
+
const localKey = relationInstance.localKey;
|
|
560
|
+
|
|
561
|
+
const localValues = models.map(model => model.getAttribute(localKey)).filter(Boolean);
|
|
562
|
+
|
|
563
|
+
if (localValues.length === 0) return;
|
|
564
|
+
|
|
565
|
+
// Get the related class - should now always be a string
|
|
566
|
+
const relatedClass = relationInstance.getRelatedClass();
|
|
567
|
+
|
|
568
|
+
// Safety check
|
|
569
|
+
if (!relatedClass || typeof relatedClass.getTableName !== 'function') {
|
|
570
|
+
throw new Error(`Invalid related class for relation '${relation}'. Make sure the model is properly defined and registered.`);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
let relationQuery = new QueryBuilder(
|
|
574
|
+
relatedClass.getTableName(),
|
|
575
|
+
relatedClass,
|
|
576
|
+
relatedClass.getConnectionName()
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
// For BelongsTo, we query the related table by its primary key using foreign key values
|
|
580
|
+
if (relationInstance.constructor.name === 'BelongsTo') {
|
|
581
|
+
const foreignValues = models.map(model => model.getAttribute(foreignKey)).filter(Boolean);
|
|
582
|
+
relationQuery = relationQuery.whereIn(relatedClass.getPrimaryKey(), foreignValues);
|
|
583
|
+
} else {
|
|
584
|
+
relationQuery = relationQuery.whereIn(foreignKey, localValues);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (this.eagerLoadConstraints[relationName]) {
|
|
588
|
+
this.eagerLoadConstraints[relationName](relationQuery);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (nested) {
|
|
592
|
+
relationQuery = relationQuery.with(nested);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const relatedModels = await relationQuery.get();
|
|
596
|
+
|
|
597
|
+
// Convert Collection to array if needed
|
|
598
|
+
const relatedArray = Array.isArray(relatedModels) ? relatedModels : (relatedModels && relatedModels.length !== undefined ? [...relatedModels] : []);
|
|
599
|
+
|
|
600
|
+
const grouped = {};
|
|
601
|
+
for (let i = 0; i < relatedArray.length; i++) {
|
|
602
|
+
const relatedModel = relatedArray[i];
|
|
603
|
+
const key = relatedModel.getAttribute(relatedClass.getPrimaryKey());
|
|
604
|
+
if (!grouped[key]) grouped[key] = [];
|
|
605
|
+
grouped[key].push(relatedModel);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
for (const model of models) {
|
|
609
|
+
let matchValue;
|
|
610
|
+
if (relationInstance.constructor.name === 'BelongsTo') {
|
|
611
|
+
matchValue = model.getAttribute(foreignKey);
|
|
612
|
+
} else {
|
|
613
|
+
matchValue = model.getAttribute(localKey);
|
|
614
|
+
}
|
|
615
|
+
const related = grouped[matchValue] || [];
|
|
616
|
+
|
|
617
|
+
if (relationInstance.constructor.name === 'HasOne' || relationInstance.constructor.name === 'BelongsTo') {
|
|
618
|
+
model.relations[relation] = related[0] || null;
|
|
619
|
+
} else {
|
|
620
|
+
model.relations[relation] = related;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Connection switching
|
|
626
|
+
connection(name) {
|
|
627
|
+
const newBuilder = new QueryBuilder(this.query._single.table, this.modelClass, name);
|
|
628
|
+
newBuilder.query = Database.table(this.query._single.table, name);
|
|
629
|
+
return newBuilder;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
on(connectionOrTrx) {
|
|
633
|
+
if (connectionOrTrx && typeof connectionOrTrx.raw === 'function') {
|
|
634
|
+
// It's a transaction object
|
|
635
|
+
const newBuilder = new QueryBuilder('', this.modelClass, null);
|
|
636
|
+
newBuilder.query = connectionOrTrx(this.query._single?.table || this.modelClass.getTableName());
|
|
637
|
+
newBuilder._transaction = connectionOrTrx;
|
|
638
|
+
return newBuilder;
|
|
639
|
+
}
|
|
640
|
+
// It's a connection name
|
|
641
|
+
return this.connection(connectionOrTrx);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Clone query
|
|
645
|
+
clone() {
|
|
646
|
+
const cloned = new QueryBuilder('', this.modelClass, this.connectionName);
|
|
647
|
+
cloned.query = this.query.clone();
|
|
648
|
+
cloned.eagerLoad = [...this.eagerLoad];
|
|
649
|
+
cloned.eagerLoadConstraints = { ...this.eagerLoadConstraints };
|
|
650
|
+
return cloned;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Get underlying Knex query
|
|
654
|
+
toKnex() {
|
|
655
|
+
return this.query;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Soft delete methods
|
|
659
|
+
withTrashed() {
|
|
660
|
+
this._includeTrashed = true;
|
|
661
|
+
return this;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
onlyTrashed() {
|
|
665
|
+
this._onlyTrashed = true;
|
|
666
|
+
return this;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
withoutTrashed() {
|
|
670
|
+
this._withoutTrashed = true;
|
|
671
|
+
return this;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Debug
|
|
675
|
+
toSql() {
|
|
676
|
+
return this.query.toSQL().sql;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
module.exports = QueryBuilder;
|