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