outlet-orm 2.5.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/LICENSE +21 -0
- package/README.md +705 -0
- package/bin/convert.js +679 -0
- package/bin/init.js +190 -0
- package/bin/migrate.js +442 -0
- package/lib/Database/DatabaseConnection.js +4 -0
- package/lib/Migrations/Migration.js +48 -0
- package/lib/Migrations/MigrationManager.js +326 -0
- package/lib/Schema/Schema.js +790 -0
- package/package.json +75 -0
- package/src/DatabaseConnection.js +697 -0
- package/src/Model.js +659 -0
- package/src/QueryBuilder.js +710 -0
- package/src/Relations/BelongsToManyRelation.js +466 -0
- package/src/Relations/BelongsToRelation.js +127 -0
- package/src/Relations/HasManyRelation.js +125 -0
- package/src/Relations/HasManyThroughRelation.js +112 -0
- package/src/Relations/HasOneRelation.js +114 -0
- package/src/Relations/HasOneThroughRelation.js +105 -0
- package/src/Relations/MorphManyRelation.js +69 -0
- package/src/Relations/MorphOneRelation.js +68 -0
- package/src/Relations/MorphToRelation.js +110 -0
- package/src/Relations/Relation.js +31 -0
- package/src/index.js +23 -0
- package/types/index.d.ts +272 -0
|
@@ -0,0 +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;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const Relation = require('./Relation');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Belongs To Relation
|
|
5
|
+
* Represents an inverse one-to-one or many relationship
|
|
6
|
+
*/
|
|
7
|
+
class BelongsToRelation extends Relation {
|
|
8
|
+
constructor(child, related, foreignKey, ownerKey) {
|
|
9
|
+
super(child, related, foreignKey, ownerKey);
|
|
10
|
+
this.child = child;
|
|
11
|
+
this.ownerKey = ownerKey;
|
|
12
|
+
this.defaultValue = null;
|
|
13
|
+
this.touchesParent = false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the related model
|
|
18
|
+
* @returns {Promise<Model|null>}
|
|
19
|
+
*/
|
|
20
|
+
async get() {
|
|
21
|
+
const foreignKeyValue = this.child.getAttribute(this.foreignKey);
|
|
22
|
+
|
|
23
|
+
if (!foreignKeyValue) {
|
|
24
|
+
return this.getDefault();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const result = await this.related
|
|
28
|
+
.where(this.ownerKey, foreignKeyValue)
|
|
29
|
+
.first();
|
|
30
|
+
|
|
31
|
+
return result || this.getDefault();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the default value
|
|
36
|
+
* @returns {Model|null}
|
|
37
|
+
*/
|
|
38
|
+
getDefault() {
|
|
39
|
+
if (this.defaultValue === null) return null;
|
|
40
|
+
if (typeof this.defaultValue === 'function') return this.defaultValue();
|
|
41
|
+
return this.defaultValue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Eager load the relationship for a collection of child models
|
|
46
|
+
* @param {Array<Model>} models
|
|
47
|
+
* @param {string} relationName
|
|
48
|
+
* @returns {Promise<void>}
|
|
49
|
+
*/
|
|
50
|
+
async eagerLoad(models, relationName, constraint) {
|
|
51
|
+
const keys = models
|
|
52
|
+
.map(model => model.getAttribute(this.foreignKey))
|
|
53
|
+
.filter(key => key !== null && key !== undefined);
|
|
54
|
+
|
|
55
|
+
if (keys.length === 0) return;
|
|
56
|
+
|
|
57
|
+
const qb = this.related.whereIn(this.ownerKey, keys);
|
|
58
|
+
if (typeof constraint === 'function') constraint(qb);
|
|
59
|
+
const relatedModels = await qb.get();
|
|
60
|
+
|
|
61
|
+
const relatedMap = {};
|
|
62
|
+
relatedModels.forEach(model => {
|
|
63
|
+
const ownerKeyValue = model.getAttribute(this.ownerKey);
|
|
64
|
+
relatedMap[ownerKeyValue] = model;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
models.forEach(model => {
|
|
68
|
+
const foreignKeyValue = model.getAttribute(this.foreignKey);
|
|
69
|
+
model.relations[relationName] = relatedMap[foreignKeyValue] || this.getDefault();
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Add a where clause to the relation query
|
|
75
|
+
* @param {string} column
|
|
76
|
+
* @param {string|any} operator
|
|
77
|
+
* @param {any} value
|
|
78
|
+
* @returns {QueryBuilder}
|
|
79
|
+
*/
|
|
80
|
+
where(column, operator, value) {
|
|
81
|
+
return this.related
|
|
82
|
+
.where(this.ownerKey, this.child.getAttribute(this.foreignKey))
|
|
83
|
+
.where(column, operator, value);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Set a default value for the relation
|
|
88
|
+
* @param {Model|function} value
|
|
89
|
+
* @returns {BelongsToRelation}
|
|
90
|
+
*/
|
|
91
|
+
withDefault(value) {
|
|
92
|
+
this.defaultValue = value;
|
|
93
|
+
return this;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Associate the model with this relation
|
|
98
|
+
* @param {Model|number} modelOrId
|
|
99
|
+
* @returns {BelongsToRelation}
|
|
100
|
+
*/
|
|
101
|
+
associate(modelOrId) {
|
|
102
|
+
const id = modelOrId instanceof this.related ? modelOrId.getAttribute(this.ownerKey) : modelOrId;
|
|
103
|
+
this.child.setAttribute(this.foreignKey, id);
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Dissociate the model from this relation
|
|
109
|
+
* @returns {BelongsToRelation}
|
|
110
|
+
*/
|
|
111
|
+
dissociate() {
|
|
112
|
+
this.child.setAttribute(this.foreignKey, null);
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Enable touching the parent model's timestamp when this model is saved
|
|
118
|
+
* @returns {BelongsToRelation}
|
|
119
|
+
*/
|
|
120
|
+
touches() {
|
|
121
|
+
this.touchesParent = true;
|
|
122
|
+
this.child.touches.push(this);
|
|
123
|
+
return this;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = BelongsToRelation;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const Relation = require('./Relation');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Has Many Relation
|
|
5
|
+
* Represents a one-to-many relationship
|
|
6
|
+
*/
|
|
7
|
+
class HasManyRelation extends Relation {
|
|
8
|
+
/**
|
|
9
|
+
* Get the related models
|
|
10
|
+
* @returns {Promise<Array<Model>>}
|
|
11
|
+
*/
|
|
12
|
+
async get() {
|
|
13
|
+
return this.related
|
|
14
|
+
.where(this.foreignKey, this.parent.getAttribute(this.localKey))
|
|
15
|
+
.get();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Eager load the relationship for a collection of parent models
|
|
20
|
+
* @param {Array<Model>} models
|
|
21
|
+
* @param {string} relationName
|
|
22
|
+
* @returns {Promise<void>}
|
|
23
|
+
*/
|
|
24
|
+
async eagerLoad(models, relationName, constraint) {
|
|
25
|
+
const keys = models
|
|
26
|
+
.map(model => model.getAttribute(this.localKey))
|
|
27
|
+
.filter(key => key !== null && key !== undefined);
|
|
28
|
+
|
|
29
|
+
if (keys.length === 0) return;
|
|
30
|
+
|
|
31
|
+
const qb = this.related.whereIn(this.foreignKey, keys);
|
|
32
|
+
if (typeof constraint === 'function') constraint(qb);
|
|
33
|
+
const relatedModels = await qb.get();
|
|
34
|
+
|
|
35
|
+
const relatedMap = {};
|
|
36
|
+
relatedModels.forEach(model => {
|
|
37
|
+
const foreignKeyValue = model.getAttribute(this.foreignKey);
|
|
38
|
+
if (!relatedMap[foreignKeyValue]) {
|
|
39
|
+
relatedMap[foreignKeyValue] = [];
|
|
40
|
+
}
|
|
41
|
+
relatedMap[foreignKeyValue].push(model);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
models.forEach(model => {
|
|
45
|
+
const localKeyValue = model.getAttribute(this.localKey);
|
|
46
|
+
model.relations[relationName] = relatedMap[localKeyValue] || [];
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Add a where clause to the relation query
|
|
52
|
+
* @param {string} column
|
|
53
|
+
* @param {string|any} operator
|
|
54
|
+
* @param {any} value
|
|
55
|
+
* @returns {QueryBuilder}
|
|
56
|
+
*/
|
|
57
|
+
where(column, operator, value) {
|
|
58
|
+
return this.related
|
|
59
|
+
.where(this.foreignKey, this.parent.getAttribute(this.localKey))
|
|
60
|
+
.where(column, operator, value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Count the related models
|
|
65
|
+
* @returns {Promise<number>}
|
|
66
|
+
*/
|
|
67
|
+
async count() {
|
|
68
|
+
return this.related
|
|
69
|
+
.where(this.foreignKey, this.parent.getAttribute(this.localKey))
|
|
70
|
+
.count();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create a new related model and associate it
|
|
75
|
+
* @param {Object} attributes
|
|
76
|
+
* @returns {Promise<Model>}
|
|
77
|
+
*/
|
|
78
|
+
async create(attributes = {}) {
|
|
79
|
+
const model = new this.related.model(attributes);
|
|
80
|
+
model.setAttribute(this.foreignKey, this.parent.getAttribute(this.localKey));
|
|
81
|
+
await model.save();
|
|
82
|
+
return model;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Save an existing model and associate it
|
|
87
|
+
* @param {Model} model
|
|
88
|
+
* @returns {Promise<Model>}
|
|
89
|
+
*/
|
|
90
|
+
async save(model) {
|
|
91
|
+
model.setAttribute(this.foreignKey, this.parent.getAttribute(this.localKey));
|
|
92
|
+
await model.save();
|
|
93
|
+
return model;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create multiple related models and associate them
|
|
98
|
+
* @param {Array<Object>} attributesArray
|
|
99
|
+
* @returns {Promise<Array<Model>>}
|
|
100
|
+
*/
|
|
101
|
+
async createMany(attributesArray) {
|
|
102
|
+
const models = [];
|
|
103
|
+
for (const attributes of attributesArray) {
|
|
104
|
+
const model = await this.create(attributes);
|
|
105
|
+
models.push(model);
|
|
106
|
+
}
|
|
107
|
+
return models;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Save multiple existing models and associate them
|
|
112
|
+
* @param {Array<Model>} models
|
|
113
|
+
* @returns {Promise<Array<Model>>}
|
|
114
|
+
*/
|
|
115
|
+
async saveMany(models) {
|
|
116
|
+
const savedModels = [];
|
|
117
|
+
for (const model of models) {
|
|
118
|
+
const saved = await this.save(model);
|
|
119
|
+
savedModels.push(saved);
|
|
120
|
+
}
|
|
121
|
+
return savedModels;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = HasManyRelation;
|