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.
@@ -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.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;
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;