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,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
|
+
};
|