outlet-orm 5.0.0 → 5.5.2
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 +1325 -1312
- package/bin/init.js +397 -379
- package/bin/migrate.js +544 -440
- package/bin/reverse.js +602 -0
- package/package.json +88 -76
- package/src/DatabaseConnection.js +98 -46
- package/src/Migrations/MigrationManager.js +329 -326
- package/src/Model.js +1141 -1118
- package/src/QueryBuilder.js +134 -35
- package/src/RawExpression.js +11 -0
- package/src/Relations/BelongsToManyRelation.js +466 -466
- package/src/Schema/Schema.js +830 -790
- package/src/Seeders/Seeder.js +60 -0
- package/src/Seeders/SeederManager.js +105 -0
- package/src/index.js +55 -49
- package/types/index.d.ts +674 -660
|
@@ -1,466 +1,466 @@
|
|
|
1
|
-
const Relation = require('./Relation');
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Belongs To Many Relation
|
|
5
|
-
* Represents a many-to-many relationship through a pivot table
|
|
6
|
-
*/
|
|
7
|
-
class BelongsToManyRelation extends Relation {
|
|
8
|
-
constructor(parent, related, pivot, foreignPivotKey, relatedPivotKey, parentKey, relatedKey) {
|
|
9
|
-
super(parent, related, null, null);
|
|
10
|
-
this.pivot = pivot;
|
|
11
|
-
this.foreignPivotKey = foreignPivotKey || `${parent.constructor.table.slice(0, -1)}_id`;
|
|
12
|
-
this.relatedPivotKey = relatedPivotKey || `${related.table.slice(0, -1)}_id`;
|
|
13
|
-
this.parentKey = parentKey || parent.constructor.primaryKey;
|
|
14
|
-
this.relatedKey = relatedKey || related.primaryKey;
|
|
15
|
-
this.pivotColumns = [];
|
|
16
|
-
this.
|
|
17
|
-
this.pivotAlias = 'pivot';
|
|
18
|
-
this.wherePivotConditions = [];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Get the related models
|
|
23
|
-
* @returns {Promise<Array<Model>>}
|
|
24
|
-
*/
|
|
25
|
-
async get() {
|
|
26
|
-
const parentKeyValue = this.parent.getAttribute(this.parentKey);
|
|
27
|
-
|
|
28
|
-
// Columns to select from pivot
|
|
29
|
-
const pivotSelectColumns = [this.relatedPivotKey, ...this.pivotColumns];
|
|
30
|
-
if (this.
|
|
31
|
-
pivotSelectColumns.push('created_at', 'updated_at');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// First, get the related IDs and pivot data
|
|
35
|
-
const pivotRecords = await this.parent.constructor.connection.select(
|
|
36
|
-
this.pivot,
|
|
37
|
-
{
|
|
38
|
-
columns: pivotSelectColumns,
|
|
39
|
-
wheres: [
|
|
40
|
-
{
|
|
41
|
-
column: this.foreignPivotKey,
|
|
42
|
-
operator: '=',
|
|
43
|
-
value: parentKeyValue,
|
|
44
|
-
type: 'basic',
|
|
45
|
-
boolean: 'and'
|
|
46
|
-
},
|
|
47
|
-
...this.wherePivotConditions.map(cond => {
|
|
48
|
-
if (cond.type === 'in') {
|
|
49
|
-
return {
|
|
50
|
-
column: cond.column,
|
|
51
|
-
values: cond.values,
|
|
52
|
-
type: 'in',
|
|
53
|
-
boolean: 'and'
|
|
54
|
-
};
|
|
55
|
-
} else {
|
|
56
|
-
return {
|
|
57
|
-
column: cond.column,
|
|
58
|
-
operator: cond.operator || '=',
|
|
59
|
-
value: cond.value,
|
|
60
|
-
type: 'basic',
|
|
61
|
-
boolean: 'and'
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
})
|
|
65
|
-
],
|
|
66
|
-
orders: [],
|
|
67
|
-
limit: null,
|
|
68
|
-
offset: null
|
|
69
|
-
}
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
if (pivotRecords.length === 0) {
|
|
73
|
-
return [];
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const relatedIds = pivotRecords.map(record => record[this.relatedPivotKey]);
|
|
77
|
-
|
|
78
|
-
// Then get the related models
|
|
79
|
-
const relatedModels = await this.related
|
|
80
|
-
.whereIn(this.relatedKey, relatedIds)
|
|
81
|
-
.get();
|
|
82
|
-
|
|
83
|
-
// Attach pivot data
|
|
84
|
-
const pivotMap = {};
|
|
85
|
-
pivotRecords.forEach(record => {
|
|
86
|
-
const key = record[this.relatedPivotKey];
|
|
87
|
-
const pivotData = {};
|
|
88
|
-
this.pivotColumns.forEach(col => {
|
|
89
|
-
pivotData[col] = record[col];
|
|
90
|
-
});
|
|
91
|
-
if (this.
|
|
92
|
-
pivotData.created_at = record.created_at;
|
|
93
|
-
pivotData.updated_at = record.updated_at;
|
|
94
|
-
}
|
|
95
|
-
pivotMap[key] = pivotData;
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
relatedModels.forEach(model => {
|
|
99
|
-
const key = model.getAttribute(this.relatedKey);
|
|
100
|
-
model[this.pivotAlias] = pivotMap[key] || {};
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
return relatedModels;
|
|
104
|
-
} /**
|
|
105
|
-
* Eager load the relationship for a collection of parent models
|
|
106
|
-
* @param {Array<Model>} models
|
|
107
|
-
* @param {string} relationName
|
|
108
|
-
* @returns {Promise<void>}
|
|
109
|
-
*/
|
|
110
|
-
async eagerLoad(models, relationName, constraint) {
|
|
111
|
-
const parentKeys = models
|
|
112
|
-
.map(model => model.getAttribute(this.parentKey))
|
|
113
|
-
.filter(key => key !== null && key !== undefined);
|
|
114
|
-
|
|
115
|
-
if (parentKeys.length === 0) return;
|
|
116
|
-
|
|
117
|
-
// Columns to select from pivot
|
|
118
|
-
const pivotSelectColumns = [this.foreignPivotKey, this.relatedPivotKey, ...this.pivotColumns];
|
|
119
|
-
if (this.
|
|
120
|
-
pivotSelectColumns.push('created_at', 'updated_at');
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Get all pivot records
|
|
124
|
-
const pivotRecords = await this.parent.constructor.connection.select(
|
|
125
|
-
this.pivot,
|
|
126
|
-
{
|
|
127
|
-
columns: pivotSelectColumns,
|
|
128
|
-
wheres: [
|
|
129
|
-
{
|
|
130
|
-
column: this.foreignPivotKey,
|
|
131
|
-
values: parentKeys,
|
|
132
|
-
type: 'in',
|
|
133
|
-
boolean: 'and'
|
|
134
|
-
},
|
|
135
|
-
...this.wherePivotConditions.map(cond => {
|
|
136
|
-
if (cond.type === 'in') {
|
|
137
|
-
return {
|
|
138
|
-
column: cond.column,
|
|
139
|
-
values: cond.values,
|
|
140
|
-
type: 'in',
|
|
141
|
-
boolean: 'and'
|
|
142
|
-
};
|
|
143
|
-
} else {
|
|
144
|
-
return {
|
|
145
|
-
column: cond.column,
|
|
146
|
-
operator: cond.operator || '=',
|
|
147
|
-
value: cond.value,
|
|
148
|
-
type: 'basic',
|
|
149
|
-
boolean: 'and'
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
})
|
|
153
|
-
],
|
|
154
|
-
orders: [],
|
|
155
|
-
limit: null,
|
|
156
|
-
offset: null
|
|
157
|
-
}
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
if (pivotRecords.length === 0) {
|
|
161
|
-
models.forEach(model => {
|
|
162
|
-
model.relations[relationName] = [];
|
|
163
|
-
});
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Get all related IDs
|
|
168
|
-
const relatedIds = [...new Set(pivotRecords.map(record => record[this.relatedPivotKey]))];
|
|
169
|
-
|
|
170
|
-
// Get all related models
|
|
171
|
-
const qb = this.related.whereIn(this.relatedKey, relatedIds);
|
|
172
|
-
if (typeof constraint === 'function') constraint(qb);
|
|
173
|
-
const relatedModels = await qb.get();
|
|
174
|
-
|
|
175
|
-
// Create a map of related models by their key
|
|
176
|
-
const relatedMap = {};
|
|
177
|
-
relatedModels.forEach(model => {
|
|
178
|
-
const keyValue = model.getAttribute(this.relatedKey);
|
|
179
|
-
relatedMap[keyValue] = model;
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
// Create a map of parent key to related models with pivot
|
|
183
|
-
const parentToRelatedMap = {};
|
|
184
|
-
pivotRecords.forEach(pivotRecord => {
|
|
185
|
-
const parentKeyValue = pivotRecord[this.foreignPivotKey];
|
|
186
|
-
const relatedKeyValue = pivotRecord[this.relatedPivotKey];
|
|
187
|
-
|
|
188
|
-
if (!parentToRelatedMap[parentKeyValue]) {
|
|
189
|
-
parentToRelatedMap[parentKeyValue] = [];
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (relatedMap[relatedKeyValue]) {
|
|
193
|
-
const model = relatedMap[relatedKeyValue];
|
|
194
|
-
// Attach pivot data
|
|
195
|
-
const pivotData = {};
|
|
196
|
-
this.pivotColumns.forEach(col => {
|
|
197
|
-
pivotData[col] = pivotRecord[col];
|
|
198
|
-
});
|
|
199
|
-
if (this.
|
|
200
|
-
pivotData.created_at = pivotRecord.created_at;
|
|
201
|
-
pivotData.updated_at = pivotRecord.updated_at;
|
|
202
|
-
}
|
|
203
|
-
model[this.pivotAlias] = pivotData;
|
|
204
|
-
parentToRelatedMap[parentKeyValue].push(model);
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
// Assign relations to parent models
|
|
209
|
-
models.forEach(model => {
|
|
210
|
-
const parentKeyValue = model.getAttribute(this.parentKey);
|
|
211
|
-
model.relations[relationName] = parentToRelatedMap[parentKeyValue] || [];
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Attach a related model to the parent
|
|
217
|
-
* @param {number|Array<number>} ids
|
|
218
|
-
* @returns {Promise<void>}
|
|
219
|
-
*/
|
|
220
|
-
async attach(ids) {
|
|
221
|
-
const parentKeyValue = this.parent.getAttribute(this.parentKey);
|
|
222
|
-
const idsArray = Array.isArray(ids) ? ids : [ids];
|
|
223
|
-
|
|
224
|
-
const pivotData = idsArray.map(id => {
|
|
225
|
-
const data = {
|
|
226
|
-
[this.foreignPivotKey]: parentKeyValue,
|
|
227
|
-
[this.relatedPivotKey]: id
|
|
228
|
-
};
|
|
229
|
-
if (this.
|
|
230
|
-
const now = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
231
|
-
data.created_at = now;
|
|
232
|
-
data.updated_at = now;
|
|
233
|
-
}
|
|
234
|
-
return data;
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
await this.parent.constructor.connection.insertMany(this.pivot, pivotData);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Detach a related model from the parent
|
|
242
|
-
* @param {number|Array<number>|null} ids
|
|
243
|
-
* @returns {Promise<void>}
|
|
244
|
-
*/
|
|
245
|
-
async detach(ids = null) {
|
|
246
|
-
const parentKeyValue = this.parent.getAttribute(this.parentKey);
|
|
247
|
-
|
|
248
|
-
const wheres = [{
|
|
249
|
-
column: this.foreignPivotKey,
|
|
250
|
-
operator: '=',
|
|
251
|
-
value: parentKeyValue,
|
|
252
|
-
type: 'basic',
|
|
253
|
-
boolean: 'and'
|
|
254
|
-
}];
|
|
255
|
-
|
|
256
|
-
if (ids !== null) {
|
|
257
|
-
const idsArray = Array.isArray(ids) ? ids : [ids];
|
|
258
|
-
wheres.push({
|
|
259
|
-
column: this.relatedPivotKey,
|
|
260
|
-
values: idsArray,
|
|
261
|
-
type: 'in',
|
|
262
|
-
boolean: 'and'
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
await this.parent.constructor.connection.delete(this.pivot, { wheres });
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Sync the pivot table with the given IDs
|
|
271
|
-
* @param {Array<number>} ids
|
|
272
|
-
* @returns {Promise<void>}
|
|
273
|
-
*/
|
|
274
|
-
async sync(ids) {
|
|
275
|
-
await this.detach();
|
|
276
|
-
await this.attach(ids);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Specify additional columns to select from the pivot table
|
|
281
|
-
* @param {...string} columns
|
|
282
|
-
* @returns {BelongsToManyRelation}
|
|
283
|
-
*/
|
|
284
|
-
withPivot(...columns) {
|
|
285
|
-
this.pivotColumns = columns;
|
|
286
|
-
return this;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Include timestamps in the pivot table
|
|
291
|
-
* @returns {BelongsToManyRelation}
|
|
292
|
-
*/
|
|
293
|
-
withTimestamps() {
|
|
294
|
-
this.
|
|
295
|
-
return this;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Alias for the pivot attribute
|
|
300
|
-
* @param {string} alias
|
|
301
|
-
* @returns {BelongsToManyRelation}
|
|
302
|
-
*/
|
|
303
|
-
as(alias) {
|
|
304
|
-
this.pivotAlias = alias;
|
|
305
|
-
return this;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Add a where condition on the pivot table
|
|
310
|
-
* @param {string} column
|
|
311
|
-
* @param {string} operator
|
|
312
|
-
* @param {*} value
|
|
313
|
-
* @returns {BelongsToManyRelation}
|
|
314
|
-
*/
|
|
315
|
-
wherePivot(column, operator, value) {
|
|
316
|
-
this.wherePivotConditions.push({ column, operator, value });
|
|
317
|
-
return this;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Add a whereIn condition on the pivot table
|
|
322
|
-
* @param {string} column
|
|
323
|
-
* @param {Array} values
|
|
324
|
-
* @returns {BelongsToManyRelation}
|
|
325
|
-
*/
|
|
326
|
-
wherePivotIn(column, values) {
|
|
327
|
-
this.wherePivotConditions.push({ column, values, type: 'in' });
|
|
328
|
-
return this;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Toggle attachment of related models
|
|
333
|
-
* @param {number|Array<number>} ids
|
|
334
|
-
* @returns {Promise<void>}
|
|
335
|
-
*/
|
|
336
|
-
async toggle(ids) {
|
|
337
|
-
const parentKeyValue = this.parent.getAttribute(this.parentKey);
|
|
338
|
-
const idsArray = Array.isArray(ids) ? ids : [ids];
|
|
339
|
-
|
|
340
|
-
// Get currently attached
|
|
341
|
-
const attached = await this.parent.constructor.connection.select(
|
|
342
|
-
this.pivot,
|
|
343
|
-
{
|
|
344
|
-
columns: [this.relatedPivotKey],
|
|
345
|
-
wheres: [{
|
|
346
|
-
column: this.foreignPivotKey,
|
|
347
|
-
operator: '=',
|
|
348
|
-
value: parentKeyValue,
|
|
349
|
-
type: 'basic',
|
|
350
|
-
boolean: 'and'
|
|
351
|
-
}],
|
|
352
|
-
orders: [],
|
|
353
|
-
limit: null,
|
|
354
|
-
offset: null
|
|
355
|
-
}
|
|
356
|
-
);
|
|
357
|
-
|
|
358
|
-
const attachedIds = attached.map(r => r[this.relatedPivotKey]);
|
|
359
|
-
|
|
360
|
-
const toAttach = idsArray.filter(id => !attachedIds.includes(id));
|
|
361
|
-
const toDetach = attachedIds.filter(id => idsArray.includes(id));
|
|
362
|
-
|
|
363
|
-
if (toDetach.length > 0) await this.detach(toDetach);
|
|
364
|
-
if (toAttach.length > 0) await this.attach(toAttach);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* Sync without detaching existing
|
|
369
|
-
* @param {Array<number>} ids
|
|
370
|
-
* @returns {Promise<void>}
|
|
371
|
-
*/
|
|
372
|
-
async syncWithoutDetaching(ids) {
|
|
373
|
-
const parentKeyValue = this.parent.getAttribute(this.parentKey);
|
|
374
|
-
|
|
375
|
-
// Get currently attached
|
|
376
|
-
const attached = await this.parent.constructor.connection.select(
|
|
377
|
-
this.pivot,
|
|
378
|
-
{
|
|
379
|
-
columns: [this.relatedPivotKey],
|
|
380
|
-
wheres: [{
|
|
381
|
-
column: this.foreignPivotKey,
|
|
382
|
-
operator: '=',
|
|
383
|
-
value: parentKeyValue,
|
|
384
|
-
type: 'basic',
|
|
385
|
-
boolean: 'and'
|
|
386
|
-
}],
|
|
387
|
-
orders: [],
|
|
388
|
-
limit: null,
|
|
389
|
-
offset: null
|
|
390
|
-
}
|
|
391
|
-
);
|
|
392
|
-
|
|
393
|
-
const attachedIds = attached.map(r => r[this.relatedPivotKey]);
|
|
394
|
-
const toAttach = ids.filter(id => !attachedIds.includes(id));
|
|
395
|
-
|
|
396
|
-
if (toAttach.length > 0) await this.attach(toAttach);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* Update existing pivot record
|
|
401
|
-
* @param {number} id
|
|
402
|
-
* @param {Object} attributes
|
|
403
|
-
* @returns {Promise<void>}
|
|
404
|
-
*/
|
|
405
|
-
async updateExistingPivot(id, attributes) {
|
|
406
|
-
const parentKeyValue = this.parent.getAttribute(this.parentKey);
|
|
407
|
-
|
|
408
|
-
const wheres = [
|
|
409
|
-
{
|
|
410
|
-
column: this.foreignPivotKey,
|
|
411
|
-
operator: '=',
|
|
412
|
-
value: parentKeyValue,
|
|
413
|
-
type: 'basic',
|
|
414
|
-
boolean: 'and'
|
|
415
|
-
},
|
|
416
|
-
{
|
|
417
|
-
column: this.relatedPivotKey,
|
|
418
|
-
operator: '=',
|
|
419
|
-
value: id,
|
|
420
|
-
type: 'basic',
|
|
421
|
-
boolean: 'and'
|
|
422
|
-
}
|
|
423
|
-
];
|
|
424
|
-
|
|
425
|
-
await this.parent.constructor.connection.update(this.pivot, attributes, { wheres });
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Create a new related model and attach it
|
|
430
|
-
* @param {Object} attributes
|
|
431
|
-
* @param {Object} pivotAttributes
|
|
432
|
-
* @returns {Promise<Model>}
|
|
433
|
-
*/
|
|
434
|
-
async create(attributes = {}, pivotAttributes = {}) {
|
|
435
|
-
const model = new this.related.model(attributes);
|
|
436
|
-
await model.save();
|
|
437
|
-
const id = model.getAttribute(this.relatedKey);
|
|
438
|
-
await this.attach(id);
|
|
439
|
-
// If pivot attributes, update the pivot
|
|
440
|
-
if (Object.keys(pivotAttributes).length > 0) {
|
|
441
|
-
await this.updateExistingPivot(id, pivotAttributes);
|
|
442
|
-
}
|
|
443
|
-
return model;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
/**
|
|
447
|
-
* Create multiple related models and attach them
|
|
448
|
-
* @param {Array<Object>} attributesArray
|
|
449
|
-
* @param {Array<Object>} pivotAttributesArray
|
|
450
|
-
* @returns {Promise<Array<Model>>}
|
|
451
|
-
*/
|
|
452
|
-
async createMany(attributesArray, pivotAttributesArray = []) {
|
|
453
|
-
const models = [];
|
|
454
|
-
const ids = [];
|
|
455
|
-
for (let i = 0; i < attributesArray.length; i++) {
|
|
456
|
-
const attributes = attributesArray[i];
|
|
457
|
-
const pivotAttributes = pivotAttributesArray[i] || {};
|
|
458
|
-
const model = await this.create(attributes, pivotAttributes);
|
|
459
|
-
models.push(model);
|
|
460
|
-
ids.push(model.getAttribute(this.relatedKey));
|
|
461
|
-
}
|
|
462
|
-
return models;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
module.exports = BelongsToManyRelation;
|
|
1
|
+
const Relation = require('./Relation');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Belongs To Many Relation
|
|
5
|
+
* Represents a many-to-many relationship through a pivot table
|
|
6
|
+
*/
|
|
7
|
+
class BelongsToManyRelation extends Relation {
|
|
8
|
+
constructor(parent, related, pivot, foreignPivotKey, relatedPivotKey, parentKey, relatedKey) {
|
|
9
|
+
super(parent, related, null, null);
|
|
10
|
+
this.pivot = pivot;
|
|
11
|
+
this.foreignPivotKey = foreignPivotKey || `${parent.constructor.table.slice(0, -1)}_id`;
|
|
12
|
+
this.relatedPivotKey = relatedPivotKey || `${related.table.slice(0, -1)}_id`;
|
|
13
|
+
this.parentKey = parentKey || parent.constructor.primaryKey;
|
|
14
|
+
this.relatedKey = relatedKey || related.primaryKey;
|
|
15
|
+
this.pivotColumns = [];
|
|
16
|
+
this._withTimestamps = false;
|
|
17
|
+
this.pivotAlias = 'pivot';
|
|
18
|
+
this.wherePivotConditions = [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the related models
|
|
23
|
+
* @returns {Promise<Array<Model>>}
|
|
24
|
+
*/
|
|
25
|
+
async get() {
|
|
26
|
+
const parentKeyValue = this.parent.getAttribute(this.parentKey);
|
|
27
|
+
|
|
28
|
+
// Columns to select from pivot
|
|
29
|
+
const pivotSelectColumns = [this.relatedPivotKey, ...this.pivotColumns];
|
|
30
|
+
if (this._withTimestamps) {
|
|
31
|
+
pivotSelectColumns.push('created_at', 'updated_at');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// First, get the related IDs and pivot data
|
|
35
|
+
const pivotRecords = await this.parent.constructor.connection.select(
|
|
36
|
+
this.pivot,
|
|
37
|
+
{
|
|
38
|
+
columns: pivotSelectColumns,
|
|
39
|
+
wheres: [
|
|
40
|
+
{
|
|
41
|
+
column: this.foreignPivotKey,
|
|
42
|
+
operator: '=',
|
|
43
|
+
value: parentKeyValue,
|
|
44
|
+
type: 'basic',
|
|
45
|
+
boolean: 'and'
|
|
46
|
+
},
|
|
47
|
+
...this.wherePivotConditions.map(cond => {
|
|
48
|
+
if (cond.type === 'in') {
|
|
49
|
+
return {
|
|
50
|
+
column: cond.column,
|
|
51
|
+
values: cond.values,
|
|
52
|
+
type: 'in',
|
|
53
|
+
boolean: 'and'
|
|
54
|
+
};
|
|
55
|
+
} else {
|
|
56
|
+
return {
|
|
57
|
+
column: cond.column,
|
|
58
|
+
operator: cond.operator || '=',
|
|
59
|
+
value: cond.value,
|
|
60
|
+
type: 'basic',
|
|
61
|
+
boolean: 'and'
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
],
|
|
66
|
+
orders: [],
|
|
67
|
+
limit: null,
|
|
68
|
+
offset: null
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (pivotRecords.length === 0) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const relatedIds = pivotRecords.map(record => record[this.relatedPivotKey]);
|
|
77
|
+
|
|
78
|
+
// Then get the related models
|
|
79
|
+
const relatedModels = await this.related
|
|
80
|
+
.whereIn(this.relatedKey, relatedIds)
|
|
81
|
+
.get();
|
|
82
|
+
|
|
83
|
+
// Attach pivot data
|
|
84
|
+
const pivotMap = {};
|
|
85
|
+
pivotRecords.forEach(record => {
|
|
86
|
+
const key = record[this.relatedPivotKey];
|
|
87
|
+
const pivotData = {};
|
|
88
|
+
this.pivotColumns.forEach(col => {
|
|
89
|
+
pivotData[col] = record[col];
|
|
90
|
+
});
|
|
91
|
+
if (this._withTimestamps) {
|
|
92
|
+
pivotData.created_at = record.created_at;
|
|
93
|
+
pivotData.updated_at = record.updated_at;
|
|
94
|
+
}
|
|
95
|
+
pivotMap[key] = pivotData;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
relatedModels.forEach(model => {
|
|
99
|
+
const key = model.getAttribute(this.relatedKey);
|
|
100
|
+
model[this.pivotAlias] = pivotMap[key] || {};
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return relatedModels;
|
|
104
|
+
} /**
|
|
105
|
+
* Eager load the relationship for a collection of parent models
|
|
106
|
+
* @param {Array<Model>} models
|
|
107
|
+
* @param {string} relationName
|
|
108
|
+
* @returns {Promise<void>}
|
|
109
|
+
*/
|
|
110
|
+
async eagerLoad(models, relationName, constraint) {
|
|
111
|
+
const parentKeys = models
|
|
112
|
+
.map(model => model.getAttribute(this.parentKey))
|
|
113
|
+
.filter(key => key !== null && key !== undefined);
|
|
114
|
+
|
|
115
|
+
if (parentKeys.length === 0) return;
|
|
116
|
+
|
|
117
|
+
// Columns to select from pivot
|
|
118
|
+
const pivotSelectColumns = [this.foreignPivotKey, this.relatedPivotKey, ...this.pivotColumns];
|
|
119
|
+
if (this._withTimestamps) {
|
|
120
|
+
pivotSelectColumns.push('created_at', 'updated_at');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Get all pivot records
|
|
124
|
+
const pivotRecords = await this.parent.constructor.connection.select(
|
|
125
|
+
this.pivot,
|
|
126
|
+
{
|
|
127
|
+
columns: pivotSelectColumns,
|
|
128
|
+
wheres: [
|
|
129
|
+
{
|
|
130
|
+
column: this.foreignPivotKey,
|
|
131
|
+
values: parentKeys,
|
|
132
|
+
type: 'in',
|
|
133
|
+
boolean: 'and'
|
|
134
|
+
},
|
|
135
|
+
...this.wherePivotConditions.map(cond => {
|
|
136
|
+
if (cond.type === 'in') {
|
|
137
|
+
return {
|
|
138
|
+
column: cond.column,
|
|
139
|
+
values: cond.values,
|
|
140
|
+
type: 'in',
|
|
141
|
+
boolean: 'and'
|
|
142
|
+
};
|
|
143
|
+
} else {
|
|
144
|
+
return {
|
|
145
|
+
column: cond.column,
|
|
146
|
+
operator: cond.operator || '=',
|
|
147
|
+
value: cond.value,
|
|
148
|
+
type: 'basic',
|
|
149
|
+
boolean: 'and'
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
],
|
|
154
|
+
orders: [],
|
|
155
|
+
limit: null,
|
|
156
|
+
offset: null
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
if (pivotRecords.length === 0) {
|
|
161
|
+
models.forEach(model => {
|
|
162
|
+
model.relations[relationName] = [];
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Get all related IDs
|
|
168
|
+
const relatedIds = [...new Set(pivotRecords.map(record => record[this.relatedPivotKey]))];
|
|
169
|
+
|
|
170
|
+
// Get all related models
|
|
171
|
+
const qb = this.related.whereIn(this.relatedKey, relatedIds);
|
|
172
|
+
if (typeof constraint === 'function') constraint(qb);
|
|
173
|
+
const relatedModels = await qb.get();
|
|
174
|
+
|
|
175
|
+
// Create a map of related models by their key
|
|
176
|
+
const relatedMap = {};
|
|
177
|
+
relatedModels.forEach(model => {
|
|
178
|
+
const keyValue = model.getAttribute(this.relatedKey);
|
|
179
|
+
relatedMap[keyValue] = model;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Create a map of parent key to related models with pivot
|
|
183
|
+
const parentToRelatedMap = {};
|
|
184
|
+
pivotRecords.forEach(pivotRecord => {
|
|
185
|
+
const parentKeyValue = pivotRecord[this.foreignPivotKey];
|
|
186
|
+
const relatedKeyValue = pivotRecord[this.relatedPivotKey];
|
|
187
|
+
|
|
188
|
+
if (!parentToRelatedMap[parentKeyValue]) {
|
|
189
|
+
parentToRelatedMap[parentKeyValue] = [];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (relatedMap[relatedKeyValue]) {
|
|
193
|
+
const model = relatedMap[relatedKeyValue];
|
|
194
|
+
// Attach pivot data
|
|
195
|
+
const pivotData = {};
|
|
196
|
+
this.pivotColumns.forEach(col => {
|
|
197
|
+
pivotData[col] = pivotRecord[col];
|
|
198
|
+
});
|
|
199
|
+
if (this._withTimestamps) {
|
|
200
|
+
pivotData.created_at = pivotRecord.created_at;
|
|
201
|
+
pivotData.updated_at = pivotRecord.updated_at;
|
|
202
|
+
}
|
|
203
|
+
model[this.pivotAlias] = pivotData;
|
|
204
|
+
parentToRelatedMap[parentKeyValue].push(model);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Assign relations to parent models
|
|
209
|
+
models.forEach(model => {
|
|
210
|
+
const parentKeyValue = model.getAttribute(this.parentKey);
|
|
211
|
+
model.relations[relationName] = parentToRelatedMap[parentKeyValue] || [];
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Attach a related model to the parent
|
|
217
|
+
* @param {number|Array<number>} ids
|
|
218
|
+
* @returns {Promise<void>}
|
|
219
|
+
*/
|
|
220
|
+
async attach(ids) {
|
|
221
|
+
const parentKeyValue = this.parent.getAttribute(this.parentKey);
|
|
222
|
+
const idsArray = Array.isArray(ids) ? ids : [ids];
|
|
223
|
+
|
|
224
|
+
const pivotData = idsArray.map(id => {
|
|
225
|
+
const data = {
|
|
226
|
+
[this.foreignPivotKey]: parentKeyValue,
|
|
227
|
+
[this.relatedPivotKey]: id
|
|
228
|
+
};
|
|
229
|
+
if (this._withTimestamps) {
|
|
230
|
+
const now = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
231
|
+
data.created_at = now;
|
|
232
|
+
data.updated_at = now;
|
|
233
|
+
}
|
|
234
|
+
return data;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await this.parent.constructor.connection.insertMany(this.pivot, pivotData);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Detach a related model from the parent
|
|
242
|
+
* @param {number|Array<number>|null} ids
|
|
243
|
+
* @returns {Promise<void>}
|
|
244
|
+
*/
|
|
245
|
+
async detach(ids = null) {
|
|
246
|
+
const parentKeyValue = this.parent.getAttribute(this.parentKey);
|
|
247
|
+
|
|
248
|
+
const wheres = [{
|
|
249
|
+
column: this.foreignPivotKey,
|
|
250
|
+
operator: '=',
|
|
251
|
+
value: parentKeyValue,
|
|
252
|
+
type: 'basic',
|
|
253
|
+
boolean: 'and'
|
|
254
|
+
}];
|
|
255
|
+
|
|
256
|
+
if (ids !== null) {
|
|
257
|
+
const idsArray = Array.isArray(ids) ? ids : [ids];
|
|
258
|
+
wheres.push({
|
|
259
|
+
column: this.relatedPivotKey,
|
|
260
|
+
values: idsArray,
|
|
261
|
+
type: 'in',
|
|
262
|
+
boolean: 'and'
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
await this.parent.constructor.connection.delete(this.pivot, { wheres });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Sync the pivot table with the given IDs
|
|
271
|
+
* @param {Array<number>} ids
|
|
272
|
+
* @returns {Promise<void>}
|
|
273
|
+
*/
|
|
274
|
+
async sync(ids) {
|
|
275
|
+
await this.detach();
|
|
276
|
+
await this.attach(ids);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Specify additional columns to select from the pivot table
|
|
281
|
+
* @param {...string} columns
|
|
282
|
+
* @returns {BelongsToManyRelation}
|
|
283
|
+
*/
|
|
284
|
+
withPivot(...columns) {
|
|
285
|
+
this.pivotColumns = columns;
|
|
286
|
+
return this;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Include timestamps in the pivot table
|
|
291
|
+
* @returns {BelongsToManyRelation}
|
|
292
|
+
*/
|
|
293
|
+
withTimestamps() {
|
|
294
|
+
this._withTimestamps = true;
|
|
295
|
+
return this;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Alias for the pivot attribute
|
|
300
|
+
* @param {string} alias
|
|
301
|
+
* @returns {BelongsToManyRelation}
|
|
302
|
+
*/
|
|
303
|
+
as(alias) {
|
|
304
|
+
this.pivotAlias = alias;
|
|
305
|
+
return this;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Add a where condition on the pivot table
|
|
310
|
+
* @param {string} column
|
|
311
|
+
* @param {string} operator
|
|
312
|
+
* @param {*} value
|
|
313
|
+
* @returns {BelongsToManyRelation}
|
|
314
|
+
*/
|
|
315
|
+
wherePivot(column, operator, value) {
|
|
316
|
+
this.wherePivotConditions.push({ column, operator, value });
|
|
317
|
+
return this;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Add a whereIn condition on the pivot table
|
|
322
|
+
* @param {string} column
|
|
323
|
+
* @param {Array} values
|
|
324
|
+
* @returns {BelongsToManyRelation}
|
|
325
|
+
*/
|
|
326
|
+
wherePivotIn(column, values) {
|
|
327
|
+
this.wherePivotConditions.push({ column, values, type: 'in' });
|
|
328
|
+
return this;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Toggle attachment of related models
|
|
333
|
+
* @param {number|Array<number>} ids
|
|
334
|
+
* @returns {Promise<void>}
|
|
335
|
+
*/
|
|
336
|
+
async toggle(ids) {
|
|
337
|
+
const parentKeyValue = this.parent.getAttribute(this.parentKey);
|
|
338
|
+
const idsArray = Array.isArray(ids) ? ids : [ids];
|
|
339
|
+
|
|
340
|
+
// Get currently attached
|
|
341
|
+
const attached = await this.parent.constructor.connection.select(
|
|
342
|
+
this.pivot,
|
|
343
|
+
{
|
|
344
|
+
columns: [this.relatedPivotKey],
|
|
345
|
+
wheres: [{
|
|
346
|
+
column: this.foreignPivotKey,
|
|
347
|
+
operator: '=',
|
|
348
|
+
value: parentKeyValue,
|
|
349
|
+
type: 'basic',
|
|
350
|
+
boolean: 'and'
|
|
351
|
+
}],
|
|
352
|
+
orders: [],
|
|
353
|
+
limit: null,
|
|
354
|
+
offset: null
|
|
355
|
+
}
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const attachedIds = attached.map(r => r[this.relatedPivotKey]);
|
|
359
|
+
|
|
360
|
+
const toAttach = idsArray.filter(id => !attachedIds.includes(id));
|
|
361
|
+
const toDetach = attachedIds.filter(id => idsArray.includes(id));
|
|
362
|
+
|
|
363
|
+
if (toDetach.length > 0) await this.detach(toDetach);
|
|
364
|
+
if (toAttach.length > 0) await this.attach(toAttach);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Sync without detaching existing
|
|
369
|
+
* @param {Array<number>} ids
|
|
370
|
+
* @returns {Promise<void>}
|
|
371
|
+
*/
|
|
372
|
+
async syncWithoutDetaching(ids) {
|
|
373
|
+
const parentKeyValue = this.parent.getAttribute(this.parentKey);
|
|
374
|
+
|
|
375
|
+
// Get currently attached
|
|
376
|
+
const attached = await this.parent.constructor.connection.select(
|
|
377
|
+
this.pivot,
|
|
378
|
+
{
|
|
379
|
+
columns: [this.relatedPivotKey],
|
|
380
|
+
wheres: [{
|
|
381
|
+
column: this.foreignPivotKey,
|
|
382
|
+
operator: '=',
|
|
383
|
+
value: parentKeyValue,
|
|
384
|
+
type: 'basic',
|
|
385
|
+
boolean: 'and'
|
|
386
|
+
}],
|
|
387
|
+
orders: [],
|
|
388
|
+
limit: null,
|
|
389
|
+
offset: null
|
|
390
|
+
}
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
const attachedIds = attached.map(r => r[this.relatedPivotKey]);
|
|
394
|
+
const toAttach = ids.filter(id => !attachedIds.includes(id));
|
|
395
|
+
|
|
396
|
+
if (toAttach.length > 0) await this.attach(toAttach);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Update existing pivot record
|
|
401
|
+
* @param {number} id
|
|
402
|
+
* @param {Object} attributes
|
|
403
|
+
* @returns {Promise<void>}
|
|
404
|
+
*/
|
|
405
|
+
async updateExistingPivot(id, attributes) {
|
|
406
|
+
const parentKeyValue = this.parent.getAttribute(this.parentKey);
|
|
407
|
+
|
|
408
|
+
const wheres = [
|
|
409
|
+
{
|
|
410
|
+
column: this.foreignPivotKey,
|
|
411
|
+
operator: '=',
|
|
412
|
+
value: parentKeyValue,
|
|
413
|
+
type: 'basic',
|
|
414
|
+
boolean: 'and'
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
column: this.relatedPivotKey,
|
|
418
|
+
operator: '=',
|
|
419
|
+
value: id,
|
|
420
|
+
type: 'basic',
|
|
421
|
+
boolean: 'and'
|
|
422
|
+
}
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
await this.parent.constructor.connection.update(this.pivot, attributes, { wheres });
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Create a new related model and attach it
|
|
430
|
+
* @param {Object} attributes
|
|
431
|
+
* @param {Object} pivotAttributes
|
|
432
|
+
* @returns {Promise<Model>}
|
|
433
|
+
*/
|
|
434
|
+
async create(attributes = {}, pivotAttributes = {}) {
|
|
435
|
+
const model = new this.related.model(attributes);
|
|
436
|
+
await model.save();
|
|
437
|
+
const id = model.getAttribute(this.relatedKey);
|
|
438
|
+
await this.attach(id);
|
|
439
|
+
// If pivot attributes, update the pivot
|
|
440
|
+
if (Object.keys(pivotAttributes).length > 0) {
|
|
441
|
+
await this.updateExistingPivot(id, pivotAttributes);
|
|
442
|
+
}
|
|
443
|
+
return model;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Create multiple related models and attach them
|
|
448
|
+
* @param {Array<Object>} attributesArray
|
|
449
|
+
* @param {Array<Object>} pivotAttributesArray
|
|
450
|
+
* @returns {Promise<Array<Model>>}
|
|
451
|
+
*/
|
|
452
|
+
async createMany(attributesArray, pivotAttributesArray = []) {
|
|
453
|
+
const models = [];
|
|
454
|
+
const ids = [];
|
|
455
|
+
for (let i = 0; i < attributesArray.length; i++) {
|
|
456
|
+
const attributes = attributesArray[i];
|
|
457
|
+
const pivotAttributes = pivotAttributesArray[i] || {};
|
|
458
|
+
const model = await this.create(attributes, pivotAttributes);
|
|
459
|
+
models.push(model);
|
|
460
|
+
ids.push(model.getAttribute(this.relatedKey));
|
|
461
|
+
}
|
|
462
|
+
return models;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
module.exports = BelongsToManyRelation;
|