outlet-orm 2.5.1 → 3.2.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 +682 -325
- package/package.json +1 -1
- package/src/DatabaseConnection.js +464 -110
- package/src/Model.js +445 -10
- package/src/QueryBuilder.js +82 -0
- package/src/index.js +9 -1
- package/types/index.d.ts +126 -20
package/src/Model.js
CHANGED
|
@@ -12,6 +12,30 @@ class Model {
|
|
|
12
12
|
static casts = {};
|
|
13
13
|
static connection = null;
|
|
14
14
|
|
|
15
|
+
// Soft Deletes
|
|
16
|
+
static softDeletes = false;
|
|
17
|
+
static DELETED_AT = 'deleted_at';
|
|
18
|
+
|
|
19
|
+
// Scopes
|
|
20
|
+
static globalScopes = {};
|
|
21
|
+
|
|
22
|
+
// Events/Hooks
|
|
23
|
+
static eventListeners = {
|
|
24
|
+
creating: [],
|
|
25
|
+
created: [],
|
|
26
|
+
updating: [],
|
|
27
|
+
updating: [],
|
|
28
|
+
saving: [],
|
|
29
|
+
saved: [],
|
|
30
|
+
deleting: [],
|
|
31
|
+
deleted: [],
|
|
32
|
+
restoring: [],
|
|
33
|
+
restored: []
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Validation rules
|
|
37
|
+
static rules = {};
|
|
38
|
+
|
|
15
39
|
/**
|
|
16
40
|
* Ensure a default database connection exists.
|
|
17
41
|
* If none is set, it will be initialized from environment (.env) lazily.
|
|
@@ -24,6 +48,15 @@ class Model {
|
|
|
24
48
|
}
|
|
25
49
|
}
|
|
26
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Get the current database connection
|
|
53
|
+
* @returns {DatabaseConnection}
|
|
54
|
+
*/
|
|
55
|
+
static getConnection() {
|
|
56
|
+
this.ensureConnection();
|
|
57
|
+
return this.connection;
|
|
58
|
+
}
|
|
59
|
+
|
|
27
60
|
/**
|
|
28
61
|
* Set the default database connection for all models
|
|
29
62
|
* @param {DatabaseConnection} connection
|
|
@@ -49,9 +82,369 @@ class Model {
|
|
|
49
82
|
this.touches = [];
|
|
50
83
|
this.exists = false;
|
|
51
84
|
this._showHidden = false;
|
|
85
|
+
this._withTrashed = false;
|
|
86
|
+
this._onlyTrashed = false;
|
|
52
87
|
this.fill(attributes);
|
|
53
88
|
}
|
|
54
89
|
|
|
90
|
+
// ==================== Events/Hooks ====================
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Register an event listener
|
|
94
|
+
* @param {string} event - Event name (creating, created, updating, updated, saving, saved, deleting, deleted, restoring, restored)
|
|
95
|
+
* @param {Function} callback - Callback function
|
|
96
|
+
*/
|
|
97
|
+
static on(event, callback) {
|
|
98
|
+
if (!this.eventListeners[event]) {
|
|
99
|
+
this.eventListeners[event] = [];
|
|
100
|
+
}
|
|
101
|
+
this.eventListeners[event].push(callback);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Fire an event
|
|
106
|
+
* @param {string} event
|
|
107
|
+
* @param {Model} model
|
|
108
|
+
* @returns {Promise<boolean>} - Returns false if event should be cancelled
|
|
109
|
+
*/
|
|
110
|
+
static async fireEvent(event, model) {
|
|
111
|
+
const listeners = this.eventListeners[event] || [];
|
|
112
|
+
for (const listener of listeners) {
|
|
113
|
+
const result = await listener(model);
|
|
114
|
+
if (result === false) {
|
|
115
|
+
return false; // Cancel the operation
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Register a 'creating' event listener
|
|
123
|
+
* @param {Function} callback
|
|
124
|
+
*/
|
|
125
|
+
static creating(callback) {
|
|
126
|
+
this.on('creating', callback);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Register a 'created' event listener
|
|
131
|
+
* @param {Function} callback
|
|
132
|
+
*/
|
|
133
|
+
static created(callback) {
|
|
134
|
+
this.on('created', callback);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Register an 'updating' event listener
|
|
139
|
+
* @param {Function} callback
|
|
140
|
+
*/
|
|
141
|
+
static updating(callback) {
|
|
142
|
+
this.on('updating', callback);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Register an 'updated' event listener
|
|
147
|
+
* @param {Function} callback
|
|
148
|
+
*/
|
|
149
|
+
static updated(callback) {
|
|
150
|
+
this.on('updated', callback);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Register a 'saving' event listener (fires on both create and update)
|
|
155
|
+
* @param {Function} callback
|
|
156
|
+
*/
|
|
157
|
+
static saving(callback) {
|
|
158
|
+
this.on('saving', callback);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Register a 'saved' event listener (fires after both create and update)
|
|
163
|
+
* @param {Function} callback
|
|
164
|
+
*/
|
|
165
|
+
static saved(callback) {
|
|
166
|
+
this.on('saved', callback);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Register a 'deleting' event listener
|
|
171
|
+
* @param {Function} callback
|
|
172
|
+
*/
|
|
173
|
+
static deleting(callback) {
|
|
174
|
+
this.on('deleting', callback);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Register a 'deleted' event listener
|
|
179
|
+
* @param {Function} callback
|
|
180
|
+
*/
|
|
181
|
+
static deleted(callback) {
|
|
182
|
+
this.on('deleted', callback);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Register a 'restoring' event listener
|
|
187
|
+
* @param {Function} callback
|
|
188
|
+
*/
|
|
189
|
+
static restoring(callback) {
|
|
190
|
+
this.on('restoring', callback);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Register a 'restored' event listener
|
|
195
|
+
* @param {Function} callback
|
|
196
|
+
*/
|
|
197
|
+
static restored(callback) {
|
|
198
|
+
this.on('restored', callback);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ==================== Scopes ====================
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Add a global scope
|
|
205
|
+
* @param {string} name - Scope name
|
|
206
|
+
* @param {Function} callback - Function that modifies the query builder
|
|
207
|
+
*/
|
|
208
|
+
static addGlobalScope(name, callback) {
|
|
209
|
+
if (!this.globalScopes) {
|
|
210
|
+
this.globalScopes = {};
|
|
211
|
+
}
|
|
212
|
+
this.globalScopes[name] = callback;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Remove a global scope
|
|
217
|
+
* @param {string} name - Scope name
|
|
218
|
+
*/
|
|
219
|
+
static removeGlobalScope(name) {
|
|
220
|
+
if (this.globalScopes && this.globalScopes[name]) {
|
|
221
|
+
delete this.globalScopes[name];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Query without a specific global scope
|
|
227
|
+
* @param {string} name - Scope name to exclude
|
|
228
|
+
* @returns {QueryBuilder}
|
|
229
|
+
*/
|
|
230
|
+
static withoutGlobalScope(name) {
|
|
231
|
+
const query = this.query();
|
|
232
|
+
query._excludedScopes = query._excludedScopes || [];
|
|
233
|
+
query._excludedScopes.push(name);
|
|
234
|
+
return query;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Query without all global scopes
|
|
239
|
+
* @returns {QueryBuilder}
|
|
240
|
+
*/
|
|
241
|
+
static withoutGlobalScopes() {
|
|
242
|
+
const query = this.query();
|
|
243
|
+
query._excludeAllScopes = true;
|
|
244
|
+
return query;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ==================== Soft Deletes ====================
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Query including soft deleted models
|
|
251
|
+
* @returns {QueryBuilder}
|
|
252
|
+
*/
|
|
253
|
+
static withTrashed() {
|
|
254
|
+
const query = this.query();
|
|
255
|
+
query._withTrashed = true;
|
|
256
|
+
return query;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Query only soft deleted models
|
|
261
|
+
* @returns {QueryBuilder}
|
|
262
|
+
*/
|
|
263
|
+
static onlyTrashed() {
|
|
264
|
+
const query = this.query();
|
|
265
|
+
query._onlyTrashed = true;
|
|
266
|
+
return query;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Check if model is soft deleted
|
|
271
|
+
* @returns {boolean}
|
|
272
|
+
*/
|
|
273
|
+
trashed() {
|
|
274
|
+
return this.constructor.softDeletes && this.attributes[this.constructor.DELETED_AT] !== null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Restore a soft deleted model
|
|
279
|
+
* @returns {Promise<this>}
|
|
280
|
+
*/
|
|
281
|
+
async restore() {
|
|
282
|
+
if (!this.constructor.softDeletes) {
|
|
283
|
+
throw new Error('This model does not use soft deletes');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Fire restoring event
|
|
287
|
+
const shouldContinue = await this.constructor.fireEvent('restoring', this);
|
|
288
|
+
if (!shouldContinue) return this;
|
|
289
|
+
|
|
290
|
+
this.setAttribute(this.constructor.DELETED_AT, null);
|
|
291
|
+
|
|
292
|
+
await this.constructor.connection.update(
|
|
293
|
+
this.constructor.table,
|
|
294
|
+
{ [this.constructor.DELETED_AT]: null },
|
|
295
|
+
{ wheres: [{ type: 'basic', column: this.constructor.primaryKey, operator: '=', value: this.getAttribute(this.constructor.primaryKey) }] }
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
// Fire restored event
|
|
299
|
+
await this.constructor.fireEvent('restored', this);
|
|
300
|
+
|
|
301
|
+
return this;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Force delete a soft deleted model (permanent delete)
|
|
306
|
+
* @returns {Promise<boolean>}
|
|
307
|
+
*/
|
|
308
|
+
async forceDelete() {
|
|
309
|
+
// Fire deleting event
|
|
310
|
+
const shouldContinue = await this.constructor.fireEvent('deleting', this);
|
|
311
|
+
if (!shouldContinue) return false;
|
|
312
|
+
|
|
313
|
+
await this.constructor.connection.delete(
|
|
314
|
+
this.constructor.table,
|
|
315
|
+
{ wheres: [{ type: 'basic', column: this.constructor.primaryKey, operator: '=', value: this.getAttribute(this.constructor.primaryKey) }] }
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
this.exists = false;
|
|
319
|
+
|
|
320
|
+
// Fire deleted event
|
|
321
|
+
await this.constructor.fireEvent('deleted', this);
|
|
322
|
+
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ==================== Validation ====================
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Validate the model attributes
|
|
330
|
+
* @returns {Object} - { valid: boolean, errors: Object }
|
|
331
|
+
*/
|
|
332
|
+
validate() {
|
|
333
|
+
const rules = this.constructor.rules;
|
|
334
|
+
const errors = {};
|
|
335
|
+
let valid = true;
|
|
336
|
+
|
|
337
|
+
for (const [field, ruleString] of Object.entries(rules)) {
|
|
338
|
+
const fieldRules = typeof ruleString === 'string' ? ruleString.split('|') : ruleString;
|
|
339
|
+
const value = this.attributes[field];
|
|
340
|
+
|
|
341
|
+
for (const rule of fieldRules) {
|
|
342
|
+
const [ruleName, ruleParam] = rule.split(':');
|
|
343
|
+
const error = this._validateRule(field, value, ruleName, ruleParam);
|
|
344
|
+
if (error) {
|
|
345
|
+
if (!errors[field]) errors[field] = [];
|
|
346
|
+
errors[field].push(error);
|
|
347
|
+
valid = false;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return { valid, errors };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Validate a single rule
|
|
357
|
+
* @private
|
|
358
|
+
*/
|
|
359
|
+
_validateRule(field, value, ruleName, ruleParam) {
|
|
360
|
+
switch (ruleName) {
|
|
361
|
+
case 'required':
|
|
362
|
+
if (value === undefined || value === null || value === '') {
|
|
363
|
+
return `${field} is required`;
|
|
364
|
+
}
|
|
365
|
+
break;
|
|
366
|
+
|
|
367
|
+
case 'string':
|
|
368
|
+
if (value !== undefined && value !== null && typeof value !== 'string') {
|
|
369
|
+
return `${field} must be a string`;
|
|
370
|
+
}
|
|
371
|
+
break;
|
|
372
|
+
|
|
373
|
+
case 'number':
|
|
374
|
+
case 'numeric':
|
|
375
|
+
if (value !== undefined && value !== null && typeof value !== 'number' && isNaN(Number(value))) {
|
|
376
|
+
return `${field} must be a number`;
|
|
377
|
+
}
|
|
378
|
+
break;
|
|
379
|
+
|
|
380
|
+
case 'email':
|
|
381
|
+
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
382
|
+
return `${field} must be a valid email`;
|
|
383
|
+
}
|
|
384
|
+
break;
|
|
385
|
+
|
|
386
|
+
case 'min':
|
|
387
|
+
if (typeof value === 'string' && value.length < parseInt(ruleParam, 10)) {
|
|
388
|
+
return `${field} must be at least ${ruleParam} characters`;
|
|
389
|
+
}
|
|
390
|
+
if (typeof value === 'number' && value < parseInt(ruleParam, 10)) {
|
|
391
|
+
return `${field} must be at least ${ruleParam}`;
|
|
392
|
+
}
|
|
393
|
+
break;
|
|
394
|
+
|
|
395
|
+
case 'max':
|
|
396
|
+
if (typeof value === 'string' && value.length > parseInt(ruleParam, 10)) {
|
|
397
|
+
return `${field} must not exceed ${ruleParam} characters`;
|
|
398
|
+
}
|
|
399
|
+
if (typeof value === 'number' && value > parseInt(ruleParam, 10)) {
|
|
400
|
+
return `${field} must not exceed ${ruleParam}`;
|
|
401
|
+
}
|
|
402
|
+
break;
|
|
403
|
+
|
|
404
|
+
case 'in':
|
|
405
|
+
if (value !== undefined && value !== null) {
|
|
406
|
+
const allowed = ruleParam.split(',');
|
|
407
|
+
if (!allowed.includes(String(value))) {
|
|
408
|
+
return `${field} must be one of: ${ruleParam}`;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
break;
|
|
412
|
+
|
|
413
|
+
case 'boolean':
|
|
414
|
+
if (value !== undefined && value !== null && typeof value !== 'boolean') {
|
|
415
|
+
return `${field} must be a boolean`;
|
|
416
|
+
}
|
|
417
|
+
break;
|
|
418
|
+
|
|
419
|
+
case 'date':
|
|
420
|
+
if (value !== undefined && value !== null && isNaN(Date.parse(value))) {
|
|
421
|
+
return `${field} must be a valid date`;
|
|
422
|
+
}
|
|
423
|
+
break;
|
|
424
|
+
|
|
425
|
+
case 'regex':
|
|
426
|
+
if (value && !new RegExp(ruleParam).test(value)) {
|
|
427
|
+
return `${field} format is invalid`;
|
|
428
|
+
}
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Validate and throw if invalid
|
|
437
|
+
* @throws {Error}
|
|
438
|
+
*/
|
|
439
|
+
validateOrFail() {
|
|
440
|
+
const { valid, errors } = this.validate();
|
|
441
|
+
if (!valid) {
|
|
442
|
+
const error = new Error('Validation failed');
|
|
443
|
+
error.errors = errors;
|
|
444
|
+
throw error;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
55
448
|
// ==================== Query Builder ====================
|
|
56
449
|
|
|
57
450
|
/**
|
|
@@ -354,10 +747,21 @@ class Model {
|
|
|
354
747
|
* @returns {Promise<this>}
|
|
355
748
|
*/
|
|
356
749
|
async save() {
|
|
750
|
+
// Fire saving event
|
|
751
|
+
const shouldContinue = await this.constructor.fireEvent('saving', this);
|
|
752
|
+
if (!shouldContinue) return this;
|
|
753
|
+
|
|
754
|
+
let result;
|
|
357
755
|
if (this.exists) {
|
|
358
|
-
|
|
756
|
+
result = await this.performUpdate();
|
|
757
|
+
} else {
|
|
758
|
+
result = await this.performInsert();
|
|
359
759
|
}
|
|
360
|
-
|
|
760
|
+
|
|
761
|
+
// Fire saved event
|
|
762
|
+
await this.constructor.fireEvent('saved', this);
|
|
763
|
+
|
|
764
|
+
return result;
|
|
361
765
|
}
|
|
362
766
|
|
|
363
767
|
/**
|
|
@@ -365,6 +769,10 @@ class Model {
|
|
|
365
769
|
* @returns {Promise<this>}
|
|
366
770
|
*/
|
|
367
771
|
async performInsert() {
|
|
772
|
+
// Fire creating event
|
|
773
|
+
const shouldContinue = await this.constructor.fireEvent('creating', this);
|
|
774
|
+
if (!shouldContinue) return this;
|
|
775
|
+
|
|
368
776
|
if (this.constructor.timestamps) {
|
|
369
777
|
const now = new Date();
|
|
370
778
|
this.setAttribute('created_at', now);
|
|
@@ -380,6 +788,9 @@ class Model {
|
|
|
380
788
|
|
|
381
789
|
await this.touchParents();
|
|
382
790
|
|
|
791
|
+
// Fire created event
|
|
792
|
+
await this.constructor.fireEvent('created', this);
|
|
793
|
+
|
|
383
794
|
return this;
|
|
384
795
|
}
|
|
385
796
|
|
|
@@ -388,6 +799,10 @@ class Model {
|
|
|
388
799
|
* @returns {Promise<this>}
|
|
389
800
|
*/
|
|
390
801
|
async performUpdate() {
|
|
802
|
+
// Fire updating event
|
|
803
|
+
const shouldContinue = await this.constructor.fireEvent('updating', this);
|
|
804
|
+
if (!shouldContinue) return this;
|
|
805
|
+
|
|
391
806
|
if (this.constructor.timestamps) {
|
|
392
807
|
this.setAttribute('updated_at', new Date());
|
|
393
808
|
}
|
|
@@ -400,13 +815,16 @@ class Model {
|
|
|
400
815
|
await this.constructor.connection.update(
|
|
401
816
|
this.constructor.table,
|
|
402
817
|
dirty,
|
|
403
|
-
{ [this.constructor.primaryKey
|
|
818
|
+
{ wheres: [{ type: 'basic', column: this.constructor.primaryKey, operator: '=', value: this.getAttribute(this.constructor.primaryKey) }] }
|
|
404
819
|
);
|
|
405
820
|
|
|
406
821
|
this.original = { ...this.attributes };
|
|
407
822
|
|
|
408
823
|
await this.touchParents();
|
|
409
824
|
|
|
825
|
+
// Fire updated event
|
|
826
|
+
await this.constructor.fireEvent('updated', this);
|
|
827
|
+
|
|
410
828
|
return this;
|
|
411
829
|
}
|
|
412
830
|
|
|
@@ -422,7 +840,7 @@ class Model {
|
|
|
422
840
|
await this.constructor.connection.update(
|
|
423
841
|
relation.related.table,
|
|
424
842
|
{ updated_at: new Date() },
|
|
425
|
-
{ [relation.ownerKey
|
|
843
|
+
{ wheres: [{ type: 'basic', column: relation.ownerKey, operator: '=', value: foreignKeyValue }] }
|
|
426
844
|
);
|
|
427
845
|
}
|
|
428
846
|
}
|
|
@@ -430,7 +848,7 @@ class Model {
|
|
|
430
848
|
}
|
|
431
849
|
|
|
432
850
|
/**
|
|
433
|
-
* Delete the model
|
|
851
|
+
* Delete the model (soft delete if enabled)
|
|
434
852
|
* @returns {Promise<boolean>}
|
|
435
853
|
*/
|
|
436
854
|
async destroy() {
|
|
@@ -438,12 +856,29 @@ class Model {
|
|
|
438
856
|
return false;
|
|
439
857
|
}
|
|
440
858
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
859
|
+
// Fire deleting event
|
|
860
|
+
const shouldContinue = await this.constructor.fireEvent('deleting', this);
|
|
861
|
+
if (!shouldContinue) return false;
|
|
862
|
+
|
|
863
|
+
// Soft delete if enabled
|
|
864
|
+
if (this.constructor.softDeletes) {
|
|
865
|
+
this.setAttribute(this.constructor.DELETED_AT, new Date());
|
|
866
|
+
await this.constructor.connection.update(
|
|
867
|
+
this.constructor.table,
|
|
868
|
+
{ [this.constructor.DELETED_AT]: new Date() },
|
|
869
|
+
{ wheres: [{ type: 'basic', column: this.constructor.primaryKey, operator: '=', value: this.getAttribute(this.constructor.primaryKey) }] }
|
|
870
|
+
);
|
|
871
|
+
} else {
|
|
872
|
+
await this.constructor.connection.delete(
|
|
873
|
+
this.constructor.table,
|
|
874
|
+
{ wheres: [{ type: 'basic', column: this.constructor.primaryKey, operator: '=', value: this.getAttribute(this.constructor.primaryKey) }] }
|
|
875
|
+
);
|
|
876
|
+
this.exists = false;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Fire deleted event
|
|
880
|
+
await this.constructor.fireEvent('deleted', this);
|
|
445
881
|
|
|
446
|
-
this.exists = false;
|
|
447
882
|
return true;
|
|
448
883
|
}
|
|
449
884
|
|
package/src/QueryBuilder.js
CHANGED
|
@@ -16,6 +16,76 @@ class QueryBuilder {
|
|
|
16
16
|
this.groupBys = [];
|
|
17
17
|
this.havings = [];
|
|
18
18
|
this._showHidden = false;
|
|
19
|
+
this._withTrashed = false;
|
|
20
|
+
this._onlyTrashed = false;
|
|
21
|
+
this._excludedScopes = [];
|
|
22
|
+
this._excludeAllScopes = false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Apply global scopes to the query
|
|
27
|
+
* @private
|
|
28
|
+
*/
|
|
29
|
+
_applyGlobalScopes() {
|
|
30
|
+
if (this._excludeAllScopes) return;
|
|
31
|
+
|
|
32
|
+
const scopes = this.model.globalScopes || {};
|
|
33
|
+
for (const [name, scopeFn] of Object.entries(scopes)) {
|
|
34
|
+
if (!this._excludedScopes.includes(name)) {
|
|
35
|
+
scopeFn(this);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Apply soft delete constraints
|
|
42
|
+
* @private
|
|
43
|
+
*/
|
|
44
|
+
_applySoftDeleteConstraints() {
|
|
45
|
+
if (!this.model.softDeletes) return;
|
|
46
|
+
|
|
47
|
+
if (this._onlyTrashed) {
|
|
48
|
+
this.whereNotNull(this.model.DELETED_AT);
|
|
49
|
+
} else if (!this._withTrashed) {
|
|
50
|
+
this.whereNull(this.model.DELETED_AT);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Include soft deleted records
|
|
56
|
+
* @returns {this}
|
|
57
|
+
*/
|
|
58
|
+
withTrashed() {
|
|
59
|
+
this._withTrashed = true;
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Only get soft deleted records
|
|
65
|
+
* @returns {this}
|
|
66
|
+
*/
|
|
67
|
+
onlyTrashed() {
|
|
68
|
+
this._onlyTrashed = true;
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Query without a specific global scope
|
|
74
|
+
* @param {string} name
|
|
75
|
+
* @returns {this}
|
|
76
|
+
*/
|
|
77
|
+
withoutGlobalScope(name) {
|
|
78
|
+
this._excludedScopes.push(name);
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Query without all global scopes
|
|
84
|
+
* @returns {this}
|
|
85
|
+
*/
|
|
86
|
+
withoutGlobalScopes() {
|
|
87
|
+
this._excludeAllScopes = true;
|
|
88
|
+
return this;
|
|
19
89
|
}
|
|
20
90
|
|
|
21
91
|
/**
|
|
@@ -441,6 +511,10 @@ class QueryBuilder {
|
|
|
441
511
|
* @returns {Promise<Array>}
|
|
442
512
|
*/
|
|
443
513
|
async get() {
|
|
514
|
+
// Apply global scopes and soft delete constraints
|
|
515
|
+
this._applyGlobalScopes();
|
|
516
|
+
this._applySoftDeleteConstraints();
|
|
517
|
+
|
|
444
518
|
const rows = await this.model.connection.select(
|
|
445
519
|
this.model.table,
|
|
446
520
|
this.buildQuery()
|
|
@@ -486,6 +560,10 @@ class QueryBuilder {
|
|
|
486
560
|
async paginate(page = 1, perPage = 15) {
|
|
487
561
|
const offset = (page - 1) * perPage;
|
|
488
562
|
|
|
563
|
+
// Apply scopes for count
|
|
564
|
+
this._applyGlobalScopes();
|
|
565
|
+
this._applySoftDeleteConstraints();
|
|
566
|
+
|
|
489
567
|
const total = await this.count();
|
|
490
568
|
const data = await this.offset(offset).limit(perPage).get();
|
|
491
569
|
|
|
@@ -505,6 +583,10 @@ class QueryBuilder {
|
|
|
505
583
|
* @returns {Promise<number>}
|
|
506
584
|
*/
|
|
507
585
|
async count() {
|
|
586
|
+
// Apply scopes for count
|
|
587
|
+
this._applyGlobalScopes();
|
|
588
|
+
this._applySoftDeleteConstraints();
|
|
589
|
+
|
|
508
590
|
const result = await this.model.connection.count(
|
|
509
591
|
this.model.table,
|
|
510
592
|
this.buildQuery()
|
package/src/index.js
CHANGED
|
@@ -9,6 +9,10 @@ const HasManyRelation = require('./Relations/HasManyRelation');
|
|
|
9
9
|
const BelongsToRelation = require('./Relations/BelongsToRelation');
|
|
10
10
|
const BelongsToManyRelation = require('./Relations/BelongsToManyRelation');
|
|
11
11
|
const HasManyThroughRelation = require('./Relations/HasManyThroughRelation');
|
|
12
|
+
const HasOneThroughRelation = require('./Relations/HasOneThroughRelation');
|
|
13
|
+
const MorphOneRelation = require('./Relations/MorphOneRelation');
|
|
14
|
+
const MorphManyRelation = require('./Relations/MorphManyRelation');
|
|
15
|
+
const MorphToRelation = require('./Relations/MorphToRelation');
|
|
12
16
|
|
|
13
17
|
module.exports = {
|
|
14
18
|
Model,
|
|
@@ -19,5 +23,9 @@ module.exports = {
|
|
|
19
23
|
HasManyRelation,
|
|
20
24
|
BelongsToRelation,
|
|
21
25
|
BelongsToManyRelation,
|
|
22
|
-
HasManyThroughRelation
|
|
26
|
+
HasManyThroughRelation,
|
|
27
|
+
HasOneThroughRelation,
|
|
28
|
+
MorphOneRelation,
|
|
29
|
+
MorphManyRelation,
|
|
30
|
+
MorphToRelation
|
|
23
31
|
};
|