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,112 @@
1
+ const Relation = require('./Relation');
2
+
3
+ /**
4
+ * Has Many Through Relation
5
+ * parent -> through -> related (final)
6
+ */
7
+ class HasManyThroughRelation extends Relation {
8
+ /**
9
+ * @param {import('../Model')} parent
10
+ * @param {typeof import('../Model')} relatedFinal
11
+ * @param {typeof import('../Model')} through
12
+ * @param {string} [foreignKeyOnThrough] - FK on through referencing parent
13
+ * @param {string} [throughKeyOnFinal] - FK on final referencing through
14
+ * @param {string} [localKey] - PK on parent
15
+ * @param {string} [throughLocalKey] - PK on through
16
+ */
17
+ constructor(parent, relatedFinal, through, foreignKeyOnThrough, throughKeyOnFinal, localKey, throughLocalKey) {
18
+ super(parent, relatedFinal, foreignKeyOnThrough, localKey);
19
+ this.through = through;
20
+ // Defaults based on naming conventions
21
+ this.localKey = localKey || parent.constructor.primaryKey || 'id';
22
+ this.throughLocalKey = throughLocalKey || through.primaryKey || 'id';
23
+ this.foreignKeyOnThrough = foreignKeyOnThrough || `${parent.constructor.table.slice(0, -1)}_id`;
24
+ this.throughKeyOnFinal = throughKeyOnFinal || `${through.table.slice(0, -1)}_id`;
25
+ }
26
+
27
+ /**
28
+ * Get final related models
29
+ * @returns {Promise<Array>}
30
+ */
31
+ async get() {
32
+ const parentKeyValue = this.parent.getAttribute(this.localKey);
33
+ if (parentKeyValue === undefined || parentKeyValue === null) return [];
34
+
35
+ const throughRows = await this.through
36
+ .where(this.foreignKeyOnThrough, parentKeyValue)
37
+ .columns([this.throughLocalKey])
38
+ .get();
39
+
40
+ const throughIds = throughRows.map(r => r.getAttribute(this.throughLocalKey));
41
+ if (throughIds.length === 0) return [];
42
+
43
+ const results = await this.related
44
+ .whereIn(this.throughKeyOnFinal, throughIds)
45
+ .get();
46
+ return results;
47
+ }
48
+
49
+ /**
50
+ * Eager load hasManyThrough for a batch of parents
51
+ * @param {Array} models
52
+ * @param {string} relationName
53
+ * @param {(qb: any) => void} [constraint]
54
+ */
55
+ async eagerLoad(models, relationName, constraint) {
56
+ const parentKeys = models
57
+ .map(m => m.getAttribute(this.localKey))
58
+ .filter(v => v !== undefined && v !== null);
59
+
60
+ if (parentKeys.length === 0) {
61
+ models.forEach(m => { m.relations[relationName] = []; });
62
+ return;
63
+ }
64
+
65
+ // Fetch through rows for all parent keys
66
+ const throughRows = await this.through
67
+ .whereIn(this.foreignKeyOnThrough, parentKeys)
68
+ .get();
69
+
70
+ if (throughRows.length === 0) {
71
+ models.forEach(m => { m.relations[relationName] = []; });
72
+ return;
73
+ }
74
+
75
+ // Map parentKey -> array of throughIds
76
+ const parentToThroughIds = {};
77
+ for (const row of throughRows) {
78
+ const pKey = row.getAttribute(this.foreignKeyOnThrough);
79
+ const tId = row.getAttribute(this.throughLocalKey);
80
+ if (!parentToThroughIds[pKey]) parentToThroughIds[pKey] = [];
81
+ parentToThroughIds[pKey].push(tId);
82
+ }
83
+
84
+ const allThroughIds = [...new Set(throughRows.map(r => r.getAttribute(this.throughLocalKey)))];
85
+
86
+ // Fetch finals in one query (with optional constraint)
87
+ const qb = this.related.whereIn(this.throughKeyOnFinal, allThroughIds);
88
+ if (typeof constraint === 'function') constraint(qb);
89
+ const finals = await qb.get();
90
+
91
+ // Group finals by through id
92
+ const finalsByThrough = {};
93
+ for (const f of finals) {
94
+ const key = f.getAttribute(this.throughKeyOnFinal);
95
+ if (!finalsByThrough[key]) finalsByThrough[key] = [];
96
+ finalsByThrough[key].push(f);
97
+ }
98
+
99
+ // Assign to each parent
100
+ for (const m of models) {
101
+ const pKey = m.getAttribute(this.localKey);
102
+ const tIds = parentToThroughIds[pKey] || [];
103
+ const collected = [];
104
+ for (const tId of tIds) {
105
+ if (finalsByThrough[tId]) collected.push(...finalsByThrough[tId]);
106
+ }
107
+ m.relations[relationName] = collected;
108
+ }
109
+ }
110
+ }
111
+
112
+ module.exports = HasManyThroughRelation;
@@ -0,0 +1,114 @@
1
+ const Relation = require('./Relation');
2
+
3
+ /**
4
+ * Has One Relation
5
+ * Represents a one-to-one relationship
6
+ */
7
+ class HasOneRelation extends Relation {
8
+ /**
9
+ * Get the related model
10
+ * @returns {Promise<Model|null>}
11
+ */
12
+ async get() {
13
+ const result = await this.related
14
+ .where(this.foreignKey, this.parent.getAttribute(this.localKey))
15
+ .first();
16
+
17
+ return result;
18
+ }
19
+
20
+ /**
21
+ * Eager load the relationship for a collection of parent models
22
+ * @param {Array<Model>} models
23
+ * @param {string} relationName
24
+ * @returns {Promise<void>}
25
+ */
26
+ async eagerLoad(models, relationName, constraint) {
27
+ const keys = models
28
+ .map(model => model.getAttribute(this.localKey))
29
+ .filter(key => key !== null && key !== undefined);
30
+
31
+ if (keys.length === 0) return;
32
+
33
+ const qb = this.related.whereIn(this.foreignKey, keys);
34
+ if (typeof constraint === 'function') constraint(qb);
35
+ const relatedModels = await qb.get();
36
+
37
+ const relatedMap = {};
38
+ relatedModels.forEach(model => {
39
+ const foreignKeyValue = model.getAttribute(this.foreignKey);
40
+ relatedMap[foreignKeyValue] = model;
41
+ });
42
+
43
+ models.forEach(model => {
44
+ const localKeyValue = model.getAttribute(this.localKey);
45
+ model.relations[relationName] = relatedMap[localKeyValue] || null;
46
+ });
47
+ }
48
+
49
+ /**
50
+ * Add a where clause to the relation query
51
+ * @param {string} column
52
+ * @param {string|any} operator
53
+ * @param {any} value
54
+ * @returns {QueryBuilder}
55
+ */
56
+ where(column, operator, value) {
57
+ return this.related
58
+ .where(this.foreignKey, this.parent.getAttribute(this.localKey))
59
+ .where(column, operator, value);
60
+ }
61
+
62
+ /**
63
+ * Create a new related model and associate it
64
+ * @param {Object} attributes
65
+ * @returns {Promise<Model>}
66
+ */
67
+ async create(attributes = {}) {
68
+ const model = new this.related.model(attributes);
69
+ model.setAttribute(this.foreignKey, this.parent.getAttribute(this.localKey));
70
+ await model.save();
71
+ return model;
72
+ }
73
+
74
+ /**
75
+ * Save an existing model and associate it
76
+ * @param {Model} model
77
+ * @returns {Promise<Model>}
78
+ */
79
+ async save(model) {
80
+ model.setAttribute(this.foreignKey, this.parent.getAttribute(this.localKey));
81
+ await model.save();
82
+ return model;
83
+ }
84
+
85
+ /**
86
+ * Create multiple related models and associate them
87
+ * @param {Array<Object>} attributesArray
88
+ * @returns {Promise<Array<Model>>}
89
+ */
90
+ async createMany(attributesArray) {
91
+ const models = [];
92
+ for (const attributes of attributesArray) {
93
+ const model = await this.create(attributes);
94
+ models.push(model);
95
+ }
96
+ return models;
97
+ }
98
+
99
+ /**
100
+ * Save multiple existing models and associate them
101
+ * @param {Array<Model>} models
102
+ * @returns {Promise<Array<Model>>}
103
+ */
104
+ async saveMany(models) {
105
+ const savedModels = [];
106
+ for (const model of models) {
107
+ const saved = await this.save(model);
108
+ savedModels.push(saved);
109
+ }
110
+ return savedModels;
111
+ }
112
+ }
113
+
114
+ module.exports = HasOneRelation;
@@ -0,0 +1,105 @@
1
+ const Relation = require('./Relation');
2
+
3
+ /**
4
+ * Has One Through Relation
5
+ * parent -> through -> related (final) - returns single model
6
+ */
7
+ class HasOneThroughRelation extends Relation {
8
+ /**
9
+ * @param {import('../Model')} parent
10
+ * @param {typeof import('../Model')} relatedFinal
11
+ * @param {typeof import('../Model')} through
12
+ * @param {string} [foreignKeyOnThrough] - FK on through referencing parent
13
+ * @param {string} [throughKeyOnFinal] - FK on final referencing through
14
+ * @param {string} [localKey] - PK on parent
15
+ * @param {string} [throughLocalKey] - PK on through
16
+ */
17
+ constructor(parent, relatedFinal, through, foreignKeyOnThrough, throughKeyOnFinal, localKey, throughLocalKey) {
18
+ super(parent, relatedFinal, foreignKeyOnThrough, localKey);
19
+ this.through = through;
20
+ // Defaults based on naming conventions
21
+ this.localKey = localKey || parent.constructor.primaryKey || 'id';
22
+ this.throughLocalKey = throughLocalKey || through.primaryKey || 'id';
23
+ this.foreignKeyOnThrough = foreignKeyOnThrough || `${parent.constructor.table.slice(0, -1)}_id`;
24
+ this.throughKeyOnFinal = throughKeyOnFinal || `${through.table.slice(0, -1)}_id`;
25
+ }
26
+
27
+ /**
28
+ * Get final related model (single)
29
+ * @returns {Promise<import('../Model')|null>}
30
+ */
31
+ async get() {
32
+ const parentKeyValue = this.parent.getAttribute(this.localKey);
33
+ if (parentKeyValue === undefined || parentKeyValue === null) return null;
34
+
35
+ const throughRow = await this.through
36
+ .where(this.foreignKeyOnThrough, parentKeyValue)
37
+ .first();
38
+
39
+ if (!throughRow) return null;
40
+
41
+ const throughId = throughRow.getAttribute(this.throughLocalKey);
42
+ const result = await this.related
43
+ .where(this.throughKeyOnFinal, throughId)
44
+ .first();
45
+ return result;
46
+ }
47
+
48
+ /**
49
+ * Eager load hasOneThrough for a batch of parents
50
+ * @param {Array} models
51
+ * @param {string} relationName
52
+ * @param {(qb: any) => void} [constraint]
53
+ */
54
+ async eagerLoad(models, relationName, constraint) {
55
+ const parentKeys = models
56
+ .map(m => m.getAttribute(this.localKey))
57
+ .filter(v => v !== undefined && v !== null);
58
+
59
+ if (parentKeys.length === 0) {
60
+ models.forEach(m => { m.relations[relationName] = null; });
61
+ return;
62
+ }
63
+
64
+ // Fetch through rows for all parent keys
65
+ const throughRows = await this.through
66
+ .whereIn(this.foreignKeyOnThrough, parentKeys)
67
+ .get();
68
+
69
+ if (throughRows.length === 0) {
70
+ models.forEach(m => { m.relations[relationName] = null; });
71
+ return;
72
+ }
73
+
74
+ // Map parentKey -> throughId (assuming one through per parent)
75
+ const parentToThroughId = {};
76
+ for (const row of throughRows) {
77
+ const pKey = row.getAttribute(this.foreignKeyOnThrough);
78
+ const tId = row.getAttribute(this.throughLocalKey);
79
+ parentToThroughId[pKey] = tId; // overwrite if multiple, take last
80
+ }
81
+
82
+ const allThroughIds = Object.values(parentToThroughId);
83
+
84
+ // Fetch finals in one query (with optional constraint)
85
+ const qb = this.related.whereIn(this.throughKeyOnFinal, allThroughIds);
86
+ if (typeof constraint === 'function') constraint(qb);
87
+ const finals = await qb.get();
88
+
89
+ // Map through id to final
90
+ const finalsByThrough = {};
91
+ for (const f of finals) {
92
+ const key = f.getAttribute(this.throughKeyOnFinal);
93
+ finalsByThrough[key] = f; // assume one per through
94
+ }
95
+
96
+ // Assign to each parent
97
+ for (const m of models) {
98
+ const pKey = m.getAttribute(this.localKey);
99
+ const tId = parentToThroughId[pKey];
100
+ m.relations[relationName] = tId ? finalsByThrough[tId] || null : null;
101
+ }
102
+ }
103
+ }
104
+
105
+ module.exports = HasOneThroughRelation;
@@ -0,0 +1,69 @@
1
+ const Relation = require('./Relation');
2
+
3
+ /**
4
+ * Morph Many Relation
5
+ * Represents a polymorphic one-to-many relationship
6
+ */
7
+ class MorphManyRelation extends Relation {
8
+ constructor(parent, related, morphType, foreignKey, localKey) {
9
+ super(parent, related, foreignKey, localKey);
10
+ this.parent = parent;
11
+ this.morphType = morphType;
12
+ }
13
+
14
+ /**
15
+ * Get the related models
16
+ * @returns {Promise<Array<Model>>}
17
+ */
18
+ async get() {
19
+ return this.related
20
+ .where(this.foreignKey, this.parent.getAttribute(this.localKey))
21
+ .where(`${this.morphType}_type`, this.parent.constructor.table)
22
+ .get();
23
+ }
24
+
25
+ /**
26
+ * Eager load the relationship for a collection of parent models
27
+ * @param {Array<Model>} models
28
+ * @param {string} relationName
29
+ * @returns {Promise<void>}
30
+ */
31
+ async eagerLoad(models, relationName, constraint) {
32
+ const keys = models.map(model => model.getAttribute(this.localKey));
33
+
34
+ const qb = this.related
35
+ .whereIn(this.foreignKey, keys)
36
+ .where(`${this.morphType}_type`, models[0].constructor.table);
37
+
38
+ if (typeof constraint === 'function') constraint(qb);
39
+ const relatedModels = await qb.get();
40
+
41
+ const relatedMap = {};
42
+ for (const model of relatedModels) {
43
+ const fk = model.getAttribute(this.foreignKey);
44
+ if (!relatedMap[fk]) relatedMap[fk] = [];
45
+ relatedMap[fk].push(model);
46
+ }
47
+
48
+ for (const model of models) {
49
+ const key = model.getAttribute(this.localKey);
50
+ model.relations[relationName] = relatedMap[key] || [];
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Add a where clause to the relation query
56
+ * @param {string} column
57
+ * @param {string|any} operator
58
+ * @param {any} value
59
+ * @returns {QueryBuilder}
60
+ */
61
+ where(column, operator, value) {
62
+ return this.related
63
+ .where(this.foreignKey, this.parent.getAttribute(this.localKey))
64
+ .where(`${this.morphType}_type`, this.parent.constructor.table)
65
+ .where(column, operator, value);
66
+ }
67
+ }
68
+
69
+ module.exports = MorphManyRelation;
@@ -0,0 +1,68 @@
1
+ const Relation = require('./Relation');
2
+
3
+ /**
4
+ * Morph One Relation
5
+ * Represents a polymorphic one-to-one relationship
6
+ */
7
+ class MorphOneRelation extends Relation {
8
+ constructor(parent, related, morphType, foreignKey, localKey) {
9
+ super(parent, related, foreignKey, localKey);
10
+ this.parent = parent;
11
+ this.morphType = morphType;
12
+ }
13
+
14
+ /**
15
+ * Get the related model
16
+ * @returns {Promise<Model|null>}
17
+ */
18
+ async get() {
19
+ return this.related
20
+ .where(this.foreignKey, this.parent.getAttribute(this.localKey))
21
+ .where(`${this.morphType}_type`, this.parent.constructor.table)
22
+ .first();
23
+ }
24
+
25
+ /**
26
+ * Eager load the relationship for a collection of parent models
27
+ * @param {Array<Model>} models
28
+ * @param {string} relationName
29
+ * @returns {Promise<void>}
30
+ */
31
+ async eagerLoad(models, relationName, constraint) {
32
+ const keys = models.map(model => model.getAttribute(this.localKey));
33
+
34
+ const qb = this.related
35
+ .whereIn(this.foreignKey, keys)
36
+ .where(`${this.morphType}_type`, models[0].constructor.table);
37
+
38
+ if (typeof constraint === 'function') constraint(qb);
39
+ const relatedModels = await qb.get();
40
+
41
+ const relatedMap = {};
42
+ for (const model of relatedModels) {
43
+ const fk = model.getAttribute(this.foreignKey);
44
+ relatedMap[fk] = model;
45
+ }
46
+
47
+ for (const model of models) {
48
+ const key = model.getAttribute(this.localKey);
49
+ model.relations[relationName] = relatedMap[key] || null;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Add a where clause to the relation query
55
+ * @param {string} column
56
+ * @param {string|any} operator
57
+ * @param {any} value
58
+ * @returns {QueryBuilder}
59
+ */
60
+ where(column, operator, value) {
61
+ return this.related
62
+ .where(this.foreignKey, this.parent.getAttribute(this.localKey))
63
+ .where(`${this.morphType}_type`, this.parent.constructor.table)
64
+ .where(column, operator, value);
65
+ }
66
+ }
67
+
68
+ module.exports = MorphOneRelation;
@@ -0,0 +1,110 @@
1
+ const Relation = require('./Relation');
2
+
3
+ /**
4
+ * Morph To Relation
5
+ * Represents a polymorphic inverse relationship
6
+ */
7
+ class MorphToRelation extends Relation {
8
+ constructor(child, name, typeColumn = null, idColumn = null) {
9
+ super(child, null, null, null); // related is dynamic
10
+ this.child = child;
11
+ this.name = name;
12
+ this.typeColumn = typeColumn || `${name}_type`;
13
+ this.idColumn = idColumn || `${name}_id`;
14
+ }
15
+
16
+ /**
17
+ * Get the related model
18
+ * @returns {Promise<Model|null>}
19
+ */
20
+ async get() {
21
+ const morphType = this.child.getAttribute(this.typeColumn);
22
+ const morphId = this.child.getAttribute(this.idColumn);
23
+
24
+ if (!morphType || !morphId) {
25
+ return null;
26
+ }
27
+
28
+ // Resolve the model class from type
29
+ const relatedClass = this.resolveMorphClass(morphType);
30
+ if (!relatedClass) {
31
+ return null;
32
+ }
33
+
34
+ return relatedClass.where(relatedClass.primaryKey, morphId).first();
35
+ }
36
+
37
+ /**
38
+ * Resolve the model class from morph type
39
+ * @param {string} morphType
40
+ * @returns {typeof Model|null}
41
+ */
42
+ resolveMorphClass(morphType) {
43
+ // Check morph map first
44
+ const morphMap = this.child.constructor.morphMap || {};
45
+ if (morphMap[morphType]) {
46
+ return morphMap[morphType];
47
+ }
48
+
49
+ // Fallback: assume morphType is the table name or class name
50
+ // For simplicity, return null if not mapped
51
+ return null;
52
+ }
53
+
54
+ /**
55
+ * Eager load the relationship for a collection of child models
56
+ * @param {Array<Model>} models
57
+ * @param {string} relationName
58
+ * @returns {Promise<void>}
59
+ */
60
+ async eagerLoad(models, relationName, constraint) {
61
+ // Group models by morph type
62
+ const grouped = {};
63
+ for (const model of models) {
64
+ const type = model.getAttribute(this.typeColumn);
65
+ const id = model.getAttribute(this.idColumn);
66
+ if (type && id) {
67
+ if (!grouped[type]) grouped[type] = { ids: [], models: [] };
68
+ grouped[type].ids.push(id);
69
+ grouped[type].models.push(model);
70
+ }
71
+ }
72
+
73
+ // Load each type separately
74
+ for (const [type, { ids, models: typeModels }] of Object.entries(grouped)) {
75
+ const relatedClass = this.resolveMorphClass(type);
76
+ if (!relatedClass) continue;
77
+
78
+ const qb = relatedClass.whereIn(relatedClass.primaryKey, ids);
79
+ if (typeof constraint === 'function') constraint(qb);
80
+ const relatedModels = await qb.get();
81
+
82
+ const relatedMap = {};
83
+ for (const model of relatedModels) {
84
+ const pk = model.getAttribute(relatedClass.primaryKey);
85
+ relatedMap[pk] = model;
86
+ }
87
+
88
+ for (const model of typeModels) {
89
+ const id = model.getAttribute(this.idColumn);
90
+ model.relations[relationName] = relatedMap[id] || null;
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Add a where clause to the relation query
97
+ * Note: Since related is dynamic, this is limited
98
+ * @param {string} _column
99
+ * @param {string|any} _operator
100
+ * @param {any} _value
101
+ * @returns {QueryBuilder}
102
+ */
103
+ where(_column, _operator, _value) {
104
+ // This is tricky since related is dynamic
105
+ // For now, throw error or handle basic case
106
+ throw new Error('where() on morphTo relation is not fully supported yet');
107
+ }
108
+ }
109
+
110
+ module.exports = MorphToRelation;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Base Relation class
3
+ */
4
+ class Relation {
5
+ constructor(parent, related, foreignKey, localKey) {
6
+ this.parent = parent;
7
+ this.related = related;
8
+ this.foreignKey = foreignKey;
9
+ this.localKey = localKey;
10
+ }
11
+
12
+ /**
13
+ * Get the results of the relationship
14
+ * @returns {Promise<Model|Array<Model>|null>}
15
+ */
16
+ async get() {
17
+ throw new Error('Method get() must be implemented by subclass');
18
+ }
19
+
20
+ /**
21
+ * Eager load the relationship for a collection of parent models
22
+ * @param {Array<Model>} models
23
+ * @param {string} relationName
24
+ * @returns {Promise<void>}
25
+ */
26
+ async eagerLoad(_models, _relationName) {
27
+ throw new Error('Method eagerLoad() must be implemented by subclass');
28
+ }
29
+ }
30
+
31
+ module.exports = Relation;
package/src/index.js ADDED
@@ -0,0 +1,23 @@
1
+ const Model = require('./Model');
2
+ const QueryBuilder = require('./QueryBuilder');
3
+ const DatabaseConnection = require('./DatabaseConnection');
4
+
5
+ // Relations
6
+ const Relation = require('./Relations/Relation');
7
+ const HasOneRelation = require('./Relations/HasOneRelation');
8
+ const HasManyRelation = require('./Relations/HasManyRelation');
9
+ const BelongsToRelation = require('./Relations/BelongsToRelation');
10
+ const BelongsToManyRelation = require('./Relations/BelongsToManyRelation');
11
+ const HasManyThroughRelation = require('./Relations/HasManyThroughRelation');
12
+
13
+ module.exports = {
14
+ Model,
15
+ QueryBuilder,
16
+ DatabaseConnection,
17
+ Relation,
18
+ HasOneRelation,
19
+ HasManyRelation,
20
+ BelongsToRelation,
21
+ BelongsToManyRelation,
22
+ HasManyThroughRelation
23
+ };