@arikajs/database 0.0.4 → 0.0.5
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 +392 -25
- package/dist/Connections/MongoDBConnection.d.ts +81 -0
- package/dist/Connections/MongoDBConnection.d.ts.map +1 -0
- package/dist/Connections/MongoDBConnection.js +203 -0
- package/dist/Connections/MongoDBConnection.js.map +1 -0
- package/dist/Connections/MySQLConnection.d.ts +1 -0
- package/dist/Connections/MySQLConnection.d.ts.map +1 -1
- package/dist/Connections/MySQLConnection.js +47 -10
- package/dist/Connections/MySQLConnection.js.map +1 -1
- package/dist/Connections/PostgreSQLConnection.d.ts +1 -0
- package/dist/Connections/PostgreSQLConnection.d.ts.map +1 -1
- package/dist/Connections/PostgreSQLConnection.js +43 -9
- package/dist/Connections/PostgreSQLConnection.js.map +1 -1
- package/dist/Connections/SQLiteConnection.d.ts +1 -0
- package/dist/Connections/SQLiteConnection.d.ts.map +1 -1
- package/dist/Connections/SQLiteConnection.js +38 -7
- package/dist/Connections/SQLiteConnection.js.map +1 -1
- package/dist/Contracts/Database.d.ts +71 -4
- package/dist/Contracts/Database.d.ts.map +1 -1
- package/dist/Contracts/Schema.d.ts +4 -0
- package/dist/Contracts/Schema.d.ts.map +1 -1
- package/dist/Database.d.ts +30 -3
- package/dist/Database.d.ts.map +1 -1
- package/dist/Database.js +39 -1
- package/dist/Database.js.map +1 -1
- package/dist/DatabaseManager.d.ts +17 -3
- package/dist/DatabaseManager.d.ts.map +1 -1
- package/dist/DatabaseManager.js +27 -11
- package/dist/DatabaseManager.js.map +1 -1
- package/dist/Migrations/Migrator.d.ts.map +1 -1
- package/dist/Migrations/Migrator.js +35 -3
- package/dist/Migrations/Migrator.js.map +1 -1
- package/dist/Model/GlobalScope.d.ts +44 -0
- package/dist/Model/GlobalScope.d.ts.map +1 -0
- package/dist/Model/GlobalScope.js +64 -0
- package/dist/Model/GlobalScope.js.map +1 -0
- package/dist/Model/Model.d.ts +168 -4
- package/dist/Model/Model.d.ts.map +1 -1
- package/dist/Model/Model.js +476 -16
- package/dist/Model/Model.js.map +1 -1
- package/dist/Model/Observer.d.ts +39 -0
- package/dist/Model/Observer.d.ts.map +1 -0
- package/dist/Model/Observer.js +48 -0
- package/dist/Model/Observer.js.map +1 -0
- package/dist/Model/Relations.d.ts +201 -10
- package/dist/Model/Relations.d.ts.map +1 -1
- package/dist/Model/Relations.js +472 -27
- package/dist/Model/Relations.js.map +1 -1
- package/dist/Query/Expression.d.ts +16 -0
- package/dist/Query/Expression.d.ts.map +1 -0
- package/dist/Query/Expression.js +25 -0
- package/dist/Query/Expression.js.map +1 -0
- package/dist/Query/QueryBuilder.d.ts +64 -6
- package/dist/Query/QueryBuilder.d.ts.map +1 -1
- package/dist/Query/QueryBuilder.js +234 -15
- package/dist/Query/QueryBuilder.js.map +1 -1
- package/dist/Query/QueryLogger.d.ts +55 -0
- package/dist/Query/QueryLogger.d.ts.map +1 -0
- package/dist/Query/QueryLogger.js +82 -0
- package/dist/Query/QueryLogger.js.map +1 -0
- package/dist/Schema/Grammars/Grammar.d.ts +5 -0
- package/dist/Schema/Grammars/Grammar.d.ts.map +1 -1
- package/dist/Schema/Grammars/Grammar.js.map +1 -1
- package/dist/Schema/Grammars/MySQLGrammar.d.ts +1 -0
- package/dist/Schema/Grammars/MySQLGrammar.d.ts.map +1 -1
- package/dist/Schema/Grammars/MySQLGrammar.js +42 -0
- package/dist/Schema/Grammars/MySQLGrammar.js.map +1 -1
- package/dist/Schema/Grammars/PostgreSQLGrammar.d.ts +1 -0
- package/dist/Schema/Grammars/PostgreSQLGrammar.d.ts.map +1 -1
- package/dist/Schema/Grammars/PostgreSQLGrammar.js +46 -0
- package/dist/Schema/Grammars/PostgreSQLGrammar.js.map +1 -1
- package/dist/Schema/Grammars/SQLiteGrammar.d.ts +1 -0
- package/dist/Schema/Grammars/SQLiteGrammar.d.ts.map +1 -1
- package/dist/Schema/Grammars/SQLiteGrammar.js +31 -0
- package/dist/Schema/Grammars/SQLiteGrammar.js.map +1 -1
- package/dist/Schema/Schema.d.ts +6 -0
- package/dist/Schema/Schema.d.ts.map +1 -1
- package/dist/Schema/Schema.js +10 -0
- package/dist/Schema/Schema.js.map +1 -1
- package/dist/Schema/SchemaBuilder.d.ts +4 -0
- package/dist/Schema/SchemaBuilder.d.ts.map +1 -1
- package/dist/Schema/SchemaBuilder.js +15 -0
- package/dist/Schema/SchemaBuilder.js.map +1 -1
- package/dist/Transactions/TransactionManager.d.ts +28 -0
- package/dist/Transactions/TransactionManager.d.ts.map +1 -0
- package/dist/Transactions/TransactionManager.js +68 -0
- package/dist/Transactions/TransactionManager.js.map +1 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -1
- package/dist/index.js.map +1 -1
- package/package.json +10 -6
package/dist/Model/Model.js
CHANGED
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ModelQueryBuilder = exports.Model = void 0;
|
|
4
4
|
const Database_1 = require("../Database");
|
|
5
5
|
const Relations_1 = require("./Relations");
|
|
6
|
+
const Observer_1 = require("./Observer");
|
|
7
|
+
const GlobalScope_1 = require("./GlobalScope");
|
|
6
8
|
/**
|
|
7
9
|
* Base Model class for Active Record pattern
|
|
8
10
|
*/
|
|
@@ -32,11 +34,79 @@ class Model {
|
|
|
32
34
|
* Loaded relationships
|
|
33
35
|
*/
|
|
34
36
|
this.relations = {};
|
|
37
|
+
/**
|
|
38
|
+
* The attributes that should be hidden for serialization.
|
|
39
|
+
*/
|
|
40
|
+
this.hidden = [];
|
|
41
|
+
/**
|
|
42
|
+
* The attributes that should be visible for serialization.
|
|
43
|
+
*/
|
|
44
|
+
this.visible = [];
|
|
45
|
+
/**
|
|
46
|
+
* The attributes that should be cast to native types.
|
|
47
|
+
*/
|
|
48
|
+
this.casts = {};
|
|
49
|
+
return new Proxy(this, {
|
|
50
|
+
get(target, prop, receiver) {
|
|
51
|
+
if (Reflect.has(target, prop)) {
|
|
52
|
+
return Reflect.get(target, prop, receiver);
|
|
53
|
+
}
|
|
54
|
+
if (typeof prop === 'string') {
|
|
55
|
+
// Check if it's a loaded relationship first
|
|
56
|
+
if (target.relations && prop in target.relations) {
|
|
57
|
+
return target.relations[prop];
|
|
58
|
+
}
|
|
59
|
+
return target.getAttribute(prop);
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
},
|
|
63
|
+
set(target, prop, value, receiver) {
|
|
64
|
+
if (Reflect.has(target, prop)) {
|
|
65
|
+
return Reflect.set(target, prop, value, receiver);
|
|
66
|
+
}
|
|
67
|
+
if (typeof prop === 'string') {
|
|
68
|
+
target.setAttribute(prop, value);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
35
74
|
}
|
|
36
75
|
/**
|
|
37
76
|
* Create a new query builder for the model
|
|
38
77
|
*/
|
|
39
78
|
static query() {
|
|
79
|
+
const queryBuilder = Database_1.Database.table(this.getTableName(), this.getConnectionName());
|
|
80
|
+
const mqb = new ModelQueryBuilder(queryBuilder, this);
|
|
81
|
+
// Apply all registered global scopes
|
|
82
|
+
GlobalScope_1.GlobalScopeRegistry.applyAll(this.name, mqb, this);
|
|
83
|
+
return mqb;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Register an observer for this model
|
|
87
|
+
*/
|
|
88
|
+
static observe(observer) {
|
|
89
|
+
Observer_1.ObserverRegistry.register(this.name, observer);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Add a global scope to this model
|
|
93
|
+
*/
|
|
94
|
+
static addGlobalScope(name, scope) {
|
|
95
|
+
const resolvedScope = typeof scope === 'function'
|
|
96
|
+
? new GlobalScope_1.CallbackGlobalScope(scope)
|
|
97
|
+
: scope;
|
|
98
|
+
GlobalScope_1.GlobalScopeRegistry.register(this.name, name, resolvedScope);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Remove a named global scope from this model
|
|
102
|
+
*/
|
|
103
|
+
static removeGlobalScope(name) {
|
|
104
|
+
GlobalScope_1.GlobalScopeRegistry.remove(this.name, name);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Run a query without any global scopes
|
|
108
|
+
*/
|
|
109
|
+
static withoutGlobalScopes() {
|
|
40
110
|
const queryBuilder = Database_1.Database.table(this.getTableName(), this.getConnectionName());
|
|
41
111
|
return new ModelQueryBuilder(queryBuilder, this);
|
|
42
112
|
}
|
|
@@ -122,6 +192,12 @@ class Model {
|
|
|
122
192
|
static async create(data) {
|
|
123
193
|
return this.query().create(data);
|
|
124
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Insert a record or multiple records (Bulk Insert)
|
|
197
|
+
*/
|
|
198
|
+
static async insert(data) {
|
|
199
|
+
return this.query().insert(data);
|
|
200
|
+
}
|
|
125
201
|
/**
|
|
126
202
|
* Get the first record
|
|
127
203
|
*/
|
|
@@ -134,12 +210,24 @@ class Model {
|
|
|
134
210
|
static async get() {
|
|
135
211
|
return this.query().get();
|
|
136
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Paginate the models
|
|
215
|
+
*/
|
|
216
|
+
static async paginate(page = 1, perPage = 15) {
|
|
217
|
+
return this.query().paginate(page, perPage);
|
|
218
|
+
}
|
|
137
219
|
/**
|
|
138
220
|
* Get the count of records
|
|
139
221
|
*/
|
|
140
222
|
static async count(column) {
|
|
141
223
|
return this.query().count(column);
|
|
142
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Cache the query results
|
|
227
|
+
*/
|
|
228
|
+
static cache(ttl, key) {
|
|
229
|
+
return this.query().cache(ttl, key);
|
|
230
|
+
}
|
|
143
231
|
/**
|
|
144
232
|
* Eager load relationships
|
|
145
233
|
*/
|
|
@@ -194,18 +282,109 @@ class Model {
|
|
|
194
282
|
const constructor = this.constructor;
|
|
195
283
|
return constructor.getPrimaryKeyName();
|
|
196
284
|
}
|
|
285
|
+
// ==================== Casts & Mutators ====================
|
|
286
|
+
/**
|
|
287
|
+
* Convert string into studly case (e.g. first_name -> FirstName)
|
|
288
|
+
*/
|
|
289
|
+
studly(value) {
|
|
290
|
+
return value.replace(/(?:^|_)(.)/g, (_, c) => c.toUpperCase());
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Cast an attribute to a native type.
|
|
294
|
+
*/
|
|
295
|
+
castAttribute(key, value) {
|
|
296
|
+
if (value === null || value === undefined) {
|
|
297
|
+
return value;
|
|
298
|
+
}
|
|
299
|
+
const castType = this.casts[key];
|
|
300
|
+
if (!castType) {
|
|
301
|
+
return value;
|
|
302
|
+
}
|
|
303
|
+
switch (castType) {
|
|
304
|
+
case 'int':
|
|
305
|
+
case 'integer':
|
|
306
|
+
return parseInt(value, 10);
|
|
307
|
+
case 'real':
|
|
308
|
+
case 'float':
|
|
309
|
+
case 'double':
|
|
310
|
+
return parseFloat(value);
|
|
311
|
+
case 'string':
|
|
312
|
+
return String(value);
|
|
313
|
+
case 'bool':
|
|
314
|
+
case 'boolean':
|
|
315
|
+
return value === 'true' || value === '1' || value === 1 || value === true;
|
|
316
|
+
case 'object':
|
|
317
|
+
case 'array':
|
|
318
|
+
case 'json':
|
|
319
|
+
return typeof value === 'string' ? JSON.parse(value) : value;
|
|
320
|
+
case 'date':
|
|
321
|
+
case 'datetime':
|
|
322
|
+
case 'timestamp':
|
|
323
|
+
return new Date(value);
|
|
324
|
+
default:
|
|
325
|
+
return value;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Cast an attribute to a native type for DB.
|
|
330
|
+
*/
|
|
331
|
+
castAttributeForSave(key, value) {
|
|
332
|
+
if (value === null || value === undefined) {
|
|
333
|
+
return value;
|
|
334
|
+
}
|
|
335
|
+
const castType = this.casts[key];
|
|
336
|
+
// Always format dates even if not explicitly in casts for timestamps
|
|
337
|
+
if (value instanceof Date) {
|
|
338
|
+
return this.formatDate(value);
|
|
339
|
+
}
|
|
340
|
+
if (!castType) {
|
|
341
|
+
return value;
|
|
342
|
+
}
|
|
343
|
+
switch (castType) {
|
|
344
|
+
case 'object':
|
|
345
|
+
case 'array':
|
|
346
|
+
case 'json':
|
|
347
|
+
return typeof value === 'object' ? JSON.stringify(value) : value;
|
|
348
|
+
case 'date':
|
|
349
|
+
case 'datetime':
|
|
350
|
+
case 'timestamp':
|
|
351
|
+
return value instanceof Date ? this.formatDate(value) : value;
|
|
352
|
+
default:
|
|
353
|
+
return value;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Format a date for database and JSON serialization
|
|
358
|
+
*/
|
|
359
|
+
formatDate(date) {
|
|
360
|
+
if (this.constructor.serializeDateAsUtc) {
|
|
361
|
+
return date.toISOString();
|
|
362
|
+
}
|
|
363
|
+
const pad = (n) => n.toString().padStart(2, '0');
|
|
364
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +
|
|
365
|
+
`${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
366
|
+
}
|
|
197
367
|
// ==================== Instance Methods ====================
|
|
198
368
|
/**
|
|
199
369
|
* Get an attribute value
|
|
200
370
|
*/
|
|
201
371
|
getAttribute(key) {
|
|
202
|
-
|
|
372
|
+
const accessorMethod = `get${this.studly(key)}Attribute`;
|
|
373
|
+
if (typeof this[accessorMethod] === 'function') {
|
|
374
|
+
return this[accessorMethod](this.attributes[key]);
|
|
375
|
+
}
|
|
376
|
+
return this.castAttribute(key, this.attributes[key]);
|
|
203
377
|
}
|
|
204
378
|
/**
|
|
205
379
|
* Set an attribute value
|
|
206
380
|
*/
|
|
207
381
|
setAttribute(key, value) {
|
|
208
|
-
this.
|
|
382
|
+
const mutatorMethod = `set${this.studly(key)}Attribute`;
|
|
383
|
+
if (typeof this[mutatorMethod] === 'function') {
|
|
384
|
+
this[mutatorMethod](value);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
this.attributes[key] = this.castAttributeForSave(key, value);
|
|
209
388
|
}
|
|
210
389
|
/**
|
|
211
390
|
* Fill the model with an array of attributes
|
|
@@ -220,39 +399,67 @@ class Model {
|
|
|
220
399
|
* Save the model to the database
|
|
221
400
|
*/
|
|
222
401
|
async save() {
|
|
402
|
+
const modelName = this.constructor.name;
|
|
223
403
|
const query = Database_1.Database.table(this.getTable(), this.getConnection());
|
|
224
404
|
if (this.exists) {
|
|
225
|
-
//
|
|
405
|
+
// Fire saving + updating events
|
|
406
|
+
if (await Observer_1.ObserverRegistry.fire(modelName, 'saving', this) === false)
|
|
407
|
+
return false;
|
|
408
|
+
if (await Observer_1.ObserverRegistry.fire(modelName, 'updating', this) === false)
|
|
409
|
+
return false;
|
|
226
410
|
const primaryKey = this.getPrimaryKey();
|
|
227
411
|
const id = this.attributes[primaryKey];
|
|
228
412
|
if (!id) {
|
|
229
413
|
throw new Error('Cannot update model without primary key value');
|
|
230
414
|
}
|
|
415
|
+
if (this.usesTimestamps()) {
|
|
416
|
+
this.updateTimestamps();
|
|
417
|
+
}
|
|
231
418
|
const dirty = this.getDirty();
|
|
232
419
|
if (Object.keys(dirty).length === 0) {
|
|
233
420
|
return true; // No changes to save
|
|
234
421
|
}
|
|
235
422
|
await query.where(primaryKey, id).update(dirty);
|
|
236
423
|
this.syncOriginal();
|
|
424
|
+
// Fire saved + updated events
|
|
425
|
+
await Observer_1.ObserverRegistry.fire(modelName, 'updated', this);
|
|
426
|
+
await Observer_1.ObserverRegistry.fire(modelName, 'saved', this);
|
|
237
427
|
return true;
|
|
238
428
|
}
|
|
239
429
|
else {
|
|
240
|
-
//
|
|
430
|
+
// Fire saving + creating events
|
|
431
|
+
if (await Observer_1.ObserverRegistry.fire(modelName, 'saving', this) === false)
|
|
432
|
+
return false;
|
|
433
|
+
if (await Observer_1.ObserverRegistry.fire(modelName, 'creating', this) === false)
|
|
434
|
+
return false;
|
|
435
|
+
if (this.usesTimestamps()) {
|
|
436
|
+
this.updateTimestamps();
|
|
437
|
+
}
|
|
241
438
|
const result = await query.insert(this.attributes);
|
|
242
|
-
// Set the primary key if it was auto-generated
|
|
243
439
|
const primaryKey = this.getPrimaryKey();
|
|
244
440
|
if (result && result.insertId && !this.attributes[primaryKey]) {
|
|
245
441
|
this.attributes[primaryKey] = result.insertId;
|
|
246
442
|
}
|
|
247
443
|
this.exists = true;
|
|
248
444
|
this.syncOriginal();
|
|
445
|
+
// Fire created + saved events
|
|
446
|
+
await Observer_1.ObserverRegistry.fire(modelName, 'created', this);
|
|
447
|
+
await Observer_1.ObserverRegistry.fire(modelName, 'saved', this);
|
|
249
448
|
return true;
|
|
250
449
|
}
|
|
251
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* Update the model with an array of attributes
|
|
453
|
+
*/
|
|
454
|
+
async update(attributes) {
|
|
455
|
+
this.fill(attributes);
|
|
456
|
+
return await this.save();
|
|
457
|
+
}
|
|
252
458
|
/**
|
|
253
459
|
* Delete the model from the database
|
|
254
460
|
*/
|
|
255
|
-
async
|
|
461
|
+
async delete() {
|
|
462
|
+
const modelName = this.constructor.name;
|
|
256
463
|
if (!this.exists) {
|
|
257
464
|
return false;
|
|
258
465
|
}
|
|
@@ -261,9 +468,14 @@ class Model {
|
|
|
261
468
|
if (!id) {
|
|
262
469
|
throw new Error('Cannot delete model without primary key value');
|
|
263
470
|
}
|
|
471
|
+
// Fire deleting event
|
|
472
|
+
if (await Observer_1.ObserverRegistry.fire(modelName, 'deleting', this) === false)
|
|
473
|
+
return false;
|
|
264
474
|
const query = Database_1.Database.table(this.getTable(), this.getConnection());
|
|
265
475
|
await query.where(primaryKey, id).delete();
|
|
266
476
|
this.exists = false;
|
|
477
|
+
// Fire deleted event
|
|
478
|
+
await Observer_1.ObserverRegistry.fire(modelName, 'deleted', this);
|
|
267
479
|
return true;
|
|
268
480
|
}
|
|
269
481
|
/**
|
|
@@ -312,6 +524,24 @@ class Model {
|
|
|
312
524
|
syncOriginal() {
|
|
313
525
|
this.original = { ...this.attributes };
|
|
314
526
|
}
|
|
527
|
+
/**
|
|
528
|
+
* Check if the model uses timestamps
|
|
529
|
+
*/
|
|
530
|
+
usesTimestamps() {
|
|
531
|
+
return this.constructor.timestamps;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Update the model's timestamps
|
|
535
|
+
*/
|
|
536
|
+
updateTimestamps() {
|
|
537
|
+
const now = new Date();
|
|
538
|
+
if (this.usesTimestamps()) {
|
|
539
|
+
this.setAttribute('updated_at', now);
|
|
540
|
+
if (!this.exists && !this.getAttribute('created_at')) {
|
|
541
|
+
this.setAttribute('created_at', now);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
315
545
|
/**
|
|
316
546
|
* Mark the model as existing
|
|
317
547
|
*/
|
|
@@ -323,10 +553,42 @@ class Model {
|
|
|
323
553
|
* Convert the model to a plain object
|
|
324
554
|
*/
|
|
325
555
|
toJSON() {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
556
|
+
let attributes = { ...this.attributes };
|
|
557
|
+
// Handle date formatting in attributes
|
|
558
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
559
|
+
if (value instanceof Date) {
|
|
560
|
+
attributes[key] = this.formatDate(value);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
let relations = { ...this.relations };
|
|
564
|
+
// Handle date formatting in relations (recursive for nested models)
|
|
565
|
+
for (const [key, value] of Object.entries(relations)) {
|
|
566
|
+
if (value instanceof Model) {
|
|
567
|
+
relations[key] = value.toJSON();
|
|
568
|
+
}
|
|
569
|
+
else if (Array.isArray(value)) {
|
|
570
|
+
relations[key] = value.map(item => item instanceof Model ? item.toJSON() : item);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
let result = {
|
|
574
|
+
...attributes,
|
|
575
|
+
...relations,
|
|
329
576
|
};
|
|
577
|
+
// Filter hidden/visible attributes
|
|
578
|
+
if (this.visible.length > 0) {
|
|
579
|
+
result = Object.keys(result)
|
|
580
|
+
.filter(key => this.visible.includes(key))
|
|
581
|
+
.reduce((obj, key) => {
|
|
582
|
+
obj[key] = result[key];
|
|
583
|
+
return obj;
|
|
584
|
+
}, {});
|
|
585
|
+
}
|
|
586
|
+
else if (this.hidden.length > 0) {
|
|
587
|
+
this.hidden.forEach(key => {
|
|
588
|
+
delete result[key];
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
return result;
|
|
330
592
|
}
|
|
331
593
|
// ==================== Relationship Methods ====================
|
|
332
594
|
/**
|
|
@@ -367,6 +629,52 @@ class Model {
|
|
|
367
629
|
const rk = relatedKey || 'id';
|
|
368
630
|
return new Relations_1.BelongsToMany(related, this, pivot, fpk, rpk, pk, rk);
|
|
369
631
|
}
|
|
632
|
+
/**
|
|
633
|
+
* Define a has-one-through relationship
|
|
634
|
+
*
|
|
635
|
+
* Example: Country → hasOneThrough(Capital, Province, 'country_id', 'province_id')
|
|
636
|
+
*/
|
|
637
|
+
hasOneThrough(related, through, firstKey, secondKey, localKey = 'id', secondLocalKey = 'id') {
|
|
638
|
+
const throughTable = through.table;
|
|
639
|
+
const fk = firstKey || `${this.getTable()}_id`;
|
|
640
|
+
const sk = secondKey || `${throughTable}_id`;
|
|
641
|
+
return new Relations_1.HasOneThrough(related, this, through, fk, sk, localKey, secondLocalKey);
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Define a has-many-through relationship
|
|
645
|
+
*
|
|
646
|
+
* Example: Country → hasManyThrough(Post, User, 'country_id', 'user_id')
|
|
647
|
+
*/
|
|
648
|
+
hasManyThrough(related, through, firstKey, secondKey, localKey = 'id', secondLocalKey = 'id') {
|
|
649
|
+
const throughTable = through.table;
|
|
650
|
+
const fk = firstKey || `${this.getTable()}_id`;
|
|
651
|
+
const sk = secondKey || `${throughTable}_id`;
|
|
652
|
+
return new Relations_1.HasManyThrough(related, this, through, fk, sk, localKey, secondLocalKey);
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Define a polymorphic has-one relationship
|
|
656
|
+
*
|
|
657
|
+
* Example: Post → morphOne(Image, 'imageable')
|
|
658
|
+
*/
|
|
659
|
+
morphOne(related, morphName, localKey = 'id') {
|
|
660
|
+
return new Relations_1.MorphOne(related, this, morphName, `${morphName}_type`, `${morphName}_id`, localKey);
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Define a polymorphic has-many relationship
|
|
664
|
+
*
|
|
665
|
+
* Example: Post → morphMany(Comment, 'commentable')
|
|
666
|
+
*/
|
|
667
|
+
morphMany(related, morphName, localKey = 'id') {
|
|
668
|
+
return new Relations_1.MorphMany(related, this, morphName, `${morphName}_type`, `${morphName}_id`, localKey);
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Define the inverse of a polymorphic relationship
|
|
672
|
+
*
|
|
673
|
+
* Example: Comment → morphTo('commentable')
|
|
674
|
+
*/
|
|
675
|
+
morphTo(morphName, morphMap = {}) {
|
|
676
|
+
return new Relations_1.MorphTo(this, morphName, `${morphName}_type`, `${morphName}_id`, morphMap);
|
|
677
|
+
}
|
|
370
678
|
/**
|
|
371
679
|
* Get a relationship value
|
|
372
680
|
*/
|
|
@@ -381,13 +689,25 @@ class Model {
|
|
|
381
689
|
return this;
|
|
382
690
|
}
|
|
383
691
|
/**
|
|
384
|
-
*
|
|
692
|
+
* Eagerly load a relationship on this instance
|
|
693
|
+
*/
|
|
694
|
+
async load(...relationNames) {
|
|
695
|
+
for (const relation of relationNames) {
|
|
696
|
+
if (typeof this[relation] === 'function') {
|
|
697
|
+
const relationInstance = this[relation]();
|
|
698
|
+
const result = await relationInstance.get();
|
|
699
|
+
this.setRelation(relation, result);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return this;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Load relationships only if they haven't been loaded yet
|
|
385
706
|
*/
|
|
386
|
-
async
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
this.setRelation(relation, result);
|
|
707
|
+
async loadMissing(...relationNames) {
|
|
708
|
+
const toLoad = relationNames.filter(name => !(name in this.relations));
|
|
709
|
+
if (toLoad.length > 0) {
|
|
710
|
+
await this.load(...toLoad);
|
|
391
711
|
}
|
|
392
712
|
return this;
|
|
393
713
|
}
|
|
@@ -401,6 +721,14 @@ Model.table = '';
|
|
|
401
721
|
* The primary key for the model
|
|
402
722
|
*/
|
|
403
723
|
Model.primaryKey = 'id';
|
|
724
|
+
/**
|
|
725
|
+
* Indicates if the model should use timestamps
|
|
726
|
+
*/
|
|
727
|
+
Model.timestamps = true;
|
|
728
|
+
/**
|
|
729
|
+
* Indicates if dates should be serialized to UTC (ISO string) or Local string
|
|
730
|
+
*/
|
|
731
|
+
Model.serializeDateAsUtc = false;
|
|
404
732
|
/**
|
|
405
733
|
* Model query builder wrapper
|
|
406
734
|
*/
|
|
@@ -437,8 +765,14 @@ class ModelQueryBuilder {
|
|
|
437
765
|
* Create a new record
|
|
438
766
|
*/
|
|
439
767
|
async create(data) {
|
|
768
|
+
let instance = new this.modelClass();
|
|
769
|
+
if (instance.usesTimestamps()) {
|
|
770
|
+
const now = new Date();
|
|
771
|
+
data['created_at'] = data['created_at'] || now;
|
|
772
|
+
data['updated_at'] = data['updated_at'] || now;
|
|
773
|
+
}
|
|
440
774
|
const result = await this.queryBuilder.insert(data);
|
|
441
|
-
|
|
775
|
+
instance = this.hydrate(data);
|
|
442
776
|
// Set the primary key if it was auto-generated
|
|
443
777
|
const primaryKey = instance['getPrimaryKey']();
|
|
444
778
|
if (result && result.insertId && !data[primaryKey]) {
|
|
@@ -448,6 +782,12 @@ class ModelQueryBuilder {
|
|
|
448
782
|
instance['syncOriginal']();
|
|
449
783
|
return instance;
|
|
450
784
|
}
|
|
785
|
+
/**
|
|
786
|
+
* Insert a record or multiple records natively (Bulk Insert)
|
|
787
|
+
*/
|
|
788
|
+
async insert(data) {
|
|
789
|
+
return await this.queryBuilder.insert(data);
|
|
790
|
+
}
|
|
451
791
|
/**
|
|
452
792
|
* Add a where clause
|
|
453
793
|
*/
|
|
@@ -553,6 +893,13 @@ class ModelQueryBuilder {
|
|
|
553
893
|
async count(column) {
|
|
554
894
|
return await this.queryBuilder.count(column);
|
|
555
895
|
}
|
|
896
|
+
/**
|
|
897
|
+
* Cache the query results
|
|
898
|
+
*/
|
|
899
|
+
cache(ttl, key) {
|
|
900
|
+
this.queryBuilder.cache(ttl, key);
|
|
901
|
+
return this;
|
|
902
|
+
}
|
|
556
903
|
/**
|
|
557
904
|
* Eager load relationships
|
|
558
905
|
*/
|
|
@@ -570,6 +917,119 @@ class ModelQueryBuilder {
|
|
|
570
917
|
}
|
|
571
918
|
return this;
|
|
572
919
|
}
|
|
920
|
+
/**
|
|
921
|
+
* Include subqueries to count given relationships
|
|
922
|
+
*/
|
|
923
|
+
withCount(relations) {
|
|
924
|
+
const relationNames = Array.isArray(relations) ? relations : [relations];
|
|
925
|
+
const dummy = new this.modelClass();
|
|
926
|
+
const outerTable = dummy.getTable();
|
|
927
|
+
for (const rel of relationNames) {
|
|
928
|
+
if (typeof dummy[rel] !== 'function') {
|
|
929
|
+
throw new Error(`Relation ${rel} does not exist on model ${dummy.constructor.name}`);
|
|
930
|
+
}
|
|
931
|
+
const relationInstance = dummy[rel]();
|
|
932
|
+
if (typeof relationInstance.getRelationCountQuery !== 'function') {
|
|
933
|
+
throw new Error(`Relation ${rel} does not support withCount natively`);
|
|
934
|
+
}
|
|
935
|
+
const { sql, bindings } = relationInstance.getRelationCountQuery(outerTable);
|
|
936
|
+
this.queryBuilder.selectRaw(`${sql} AS ${rel}_count`, bindings);
|
|
937
|
+
}
|
|
938
|
+
return this;
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Add a basic where clause using a related model (WHERE EXISTS)
|
|
942
|
+
*/
|
|
943
|
+
whereHas(relation, callback) {
|
|
944
|
+
const dummy = new this.modelClass();
|
|
945
|
+
const outerTable = dummy.getTable();
|
|
946
|
+
if (typeof dummy[relation] !== 'function') {
|
|
947
|
+
throw new Error(`Relation ${relation} does not exist on model ${dummy.constructor.name}`);
|
|
948
|
+
}
|
|
949
|
+
const relationInstance = dummy[relation]();
|
|
950
|
+
this.queryBuilder.whereExists((q) => {
|
|
951
|
+
const { sql, bindings } = relationInstance.getRelationCountQuery(outerTable);
|
|
952
|
+
// Replace SELECT COUNT(*) with SELECT 1 for EXISTS optimization
|
|
953
|
+
const existsSql = sql.replace(/^\(SELECT COUNT\(\*\)/i, '(SELECT 1');
|
|
954
|
+
q.selectRaw(existsSql.slice(1, -1), bindings); // remove outer parens because whereExists adds them
|
|
955
|
+
if (callback) {
|
|
956
|
+
callback(q);
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
return this;
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Add a basic where clause for absence of a related model (WHERE NOT EXISTS)
|
|
963
|
+
*/
|
|
964
|
+
whereDoesntHave(relation, callback) {
|
|
965
|
+
const dummy = new this.modelClass();
|
|
966
|
+
const outerTable = dummy.getTable();
|
|
967
|
+
if (typeof dummy[relation] !== 'function') {
|
|
968
|
+
throw new Error(`Relation ${relation} does not exist on model ${dummy.constructor.name}`);
|
|
969
|
+
}
|
|
970
|
+
const relationInstance = dummy[relation]();
|
|
971
|
+
this.queryBuilder.whereNotExists((q) => {
|
|
972
|
+
const { sql, bindings } = relationInstance.getRelationCountQuery(outerTable);
|
|
973
|
+
const existsSql = sql.replace(/^\(SELECT COUNT\(\*\)/i, '(SELECT 1');
|
|
974
|
+
q.selectRaw(existsSql.slice(1, -1), bindings);
|
|
975
|
+
if (callback) {
|
|
976
|
+
callback(q);
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
return this;
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Paginate the query results
|
|
983
|
+
*/
|
|
984
|
+
async paginate(page = 1, perPage = 15, path = '/') {
|
|
985
|
+
const paginatedResult = await this.queryBuilder.paginate(page, perPage, path);
|
|
986
|
+
// Map and load relations
|
|
987
|
+
const models = paginatedResult.data.map(data => this.hydrate(data));
|
|
988
|
+
const finalModels = await this.loadRelations(models);
|
|
989
|
+
return {
|
|
990
|
+
data: finalModels,
|
|
991
|
+
meta: paginatedResult.meta,
|
|
992
|
+
links: paginatedResult.links
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Paginate the query results without counting total pages
|
|
997
|
+
*/
|
|
998
|
+
async simplePaginate(page = 1, perPage = 15, path = '/') {
|
|
999
|
+
const paginatedResult = await this.queryBuilder.simplePaginate(page, perPage, path);
|
|
1000
|
+
// Map and load relations
|
|
1001
|
+
const models = paginatedResult.data.map(data => this.hydrate(data));
|
|
1002
|
+
const finalModels = await this.loadRelations(models);
|
|
1003
|
+
return {
|
|
1004
|
+
data: finalModels,
|
|
1005
|
+
meta: paginatedResult.meta,
|
|
1006
|
+
links: paginatedResult.links
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Cursor paginate for high performance
|
|
1011
|
+
*/
|
|
1012
|
+
async cursorPaginate(cursor = null, perPage = 15, cursorColumn = 'id', path = '/') {
|
|
1013
|
+
const paginatedResult = await this.queryBuilder.cursorPaginate(cursor, perPage, cursorColumn, path);
|
|
1014
|
+
// Map and load relations
|
|
1015
|
+
const models = paginatedResult.data.map(data => this.hydrate(data));
|
|
1016
|
+
const finalModels = await this.loadRelations(models);
|
|
1017
|
+
return {
|
|
1018
|
+
data: finalModels,
|
|
1019
|
+
meta: paginatedResult.meta,
|
|
1020
|
+
links: paginatedResult.links
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Chunk the query results
|
|
1025
|
+
*/
|
|
1026
|
+
async chunk(count, callback) {
|
|
1027
|
+
return this.queryBuilder.chunk(count, async (results, page) => {
|
|
1028
|
+
const models = results.map(data => this.hydrate(data));
|
|
1029
|
+
const finalModels = await this.loadRelations(models);
|
|
1030
|
+
return callback(finalModels, page);
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
573
1033
|
/**
|
|
574
1034
|
* Hydrate a model instance from data
|
|
575
1035
|
*/
|