mythix-orm 1.0.1
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/.eslintrc.js +94 -0
- package/.vscode/launch.json +34 -0
- package/.vscode/settings.json +10 -0
- package/LICENSE +21 -0
- package/README.md +15 -0
- package/lib/connection/connection-base.js +877 -0
- package/lib/connection/index.js +11 -0
- package/lib/connection/literals/average-literal.js +14 -0
- package/lib/connection/literals/count-literal.js +18 -0
- package/lib/connection/literals/distinct-literal.js +14 -0
- package/lib/connection/literals/index.js +23 -0
- package/lib/connection/literals/literal-base.js +62 -0
- package/lib/connection/literals/literal-field-base.js +45 -0
- package/lib/connection/literals/literal.js +11 -0
- package/lib/connection/literals/max-literal.js +14 -0
- package/lib/connection/literals/min-literal.js +14 -0
- package/lib/connection/literals/sum-literal.js +14 -0
- package/lib/connection/query-generator-base.js +864 -0
- package/lib/errors/base-error.js +14 -0
- package/lib/errors/connection/access-denied-error.js +13 -0
- package/lib/errors/connection/connection-acquire-timeout-error.js +13 -0
- package/lib/errors/connection/connection-refused-error.js +13 -0
- package/lib/errors/connection/connection-timed-out-error.js +13 -0
- package/lib/errors/connection/host-not-found-error.js +13 -0
- package/lib/errors/connection/host-not-reachable-error.js +13 -0
- package/lib/errors/connection/index.js +19 -0
- package/lib/errors/connection/invalid-connection-error.js +13 -0
- package/lib/errors/connection-base-error.js +13 -0
- package/lib/errors/database/exclusion-constraint-error.js +13 -0
- package/lib/errors/database/foreign-key-constraint-error.js +13 -0
- package/lib/errors/database/index.js +13 -0
- package/lib/errors/database/timeout-error.js +13 -0
- package/lib/errors/database/unknown-constraint-error.js +13 -0
- package/lib/errors/database-base-error.js +17 -0
- package/lib/errors/index.js +44 -0
- package/lib/field.js +18 -0
- package/lib/index.js +43 -0
- package/lib/model.js +1374 -0
- package/lib/proxy-class/index.js +3 -0
- package/lib/proxy-class/proxy-class.js +269 -0
- package/lib/query-engine/field-scope.js +99 -0
- package/lib/query-engine/index.js +13 -0
- package/lib/query-engine/model-scope.js +198 -0
- package/lib/query-engine/query-engine-base.js +325 -0
- package/lib/query-engine/query-engine.js +212 -0
- package/lib/types/concrete/bigint-type.js +62 -0
- package/lib/types/concrete/blob-type.js +46 -0
- package/lib/types/concrete/boolean-type.js +41 -0
- package/lib/types/concrete/char-type.js +39 -0
- package/lib/types/concrete/date-type.js +87 -0
- package/lib/types/concrete/datetime-type.js +92 -0
- package/lib/types/concrete/float-type.js +47 -0
- package/lib/types/concrete/foreign-key-type.js +208 -0
- package/lib/types/concrete/index.js +53 -0
- package/lib/types/concrete/integer-type.js +51 -0
- package/lib/types/concrete/string-type.js +44 -0
- package/lib/types/concrete/text-type.js +44 -0
- package/lib/types/concrete/uuid-base.js +77 -0
- package/lib/types/concrete/uuid-v1-type.js +99 -0
- package/lib/types/concrete/uuid-v3-type.js +108 -0
- package/lib/types/concrete/uuid-v4-type.js +90 -0
- package/lib/types/concrete/uuid-v5-type.js +108 -0
- package/lib/types/concrete/xid-type.js +58 -0
- package/lib/types/helpers/default-helpers.js +127 -0
- package/lib/types/helpers/index.js +35 -0
- package/lib/types/index.js +94 -0
- package/lib/types/type.js +244 -0
- package/lib/types/virtual/index.js +14 -0
- package/lib/types/virtual/model-type.js +141 -0
- package/lib/types/virtual/models-type.js +264 -0
- package/lib/types/virtual/relational-type-base.js +323 -0
- package/lib/utils/index.js +65 -0
- package/lib/utils/misc-utils.js +73 -0
- package/lib/utils/model-utils.js +704 -0
- package/lib/utils/query-utils.js +186 -0
- package/package.json +63 -0
- package/playground/test01.js +15 -0
- package/spec/connection/connection-base-spec.js +126 -0
- package/spec/connection/literals/average-literal-spec.js +45 -0
- package/spec/connection/literals/count-literal-spec.js +42 -0
- package/spec/connection/literals/distinct-literal-spec.js +42 -0
- package/spec/connection/literals/literal-spec.js +26 -0
- package/spec/connection/literals/max-literal-spec.js +42 -0
- package/spec/connection/literals/min-literal-spec.js +42 -0
- package/spec/connection/literals/sum-literal-spec.js +42 -0
- package/spec/helpers/default-helpers-spec.js +108 -0
- package/spec/model-spec.js +736 -0
- package/spec/proxy-class/proxy-class-spec.js +173 -0
- package/spec/query-engine/__snapshots__/QueryEngine-operations-query_operations_and_chaining-can_chain_query_conditions-001.snapshot +94 -0
- package/spec/query-engine/__snapshots__/QueryEngine-operations-query_operations_and_chaining-can_construct_a_query_context_with_a_model_call-001.snapshot +35 -0
- package/spec/query-engine/__snapshots__/QueryEngine-operations-query_operations_and_chaining-can_construct_a_query_context_with_a_model_sub-001.snapshot +35 -0
- package/spec/query-engine/__snapshots__/QueryEngine-operations-query_operations_and_chaining-can_set_a_default_scope_on_a_model-001.snapshot +57 -0
- package/spec/query-engine/__snapshots__/QueryEngine-operations-query_operations_and_chaining-can_unscope_default_scope_on_a_model-001.snapshot +35 -0
- package/spec/query-engine/query-engine-spec.js +99 -0
- package/spec/support/jasmine.json +13 -0
- package/spec/support/models/blob-test-model.js +19 -0
- package/spec/support/models/extended-user-model.js +38 -0
- package/spec/support/models/index.js +27 -0
- package/spec/support/models/number-model.js +24 -0
- package/spec/support/models/role-model.js +26 -0
- package/spec/support/models/role-thing-model.js +41 -0
- package/spec/support/models/scoped-user-model.js +13 -0
- package/spec/support/models/time-model.js +36 -0
- package/spec/support/models/user-model.js +70 -0
- package/spec/support/models/user-role-model.js +36 -0
- package/spec/support/models/user-thing-model.js +46 -0
- package/spec/support/models/validation-test-model.js +40 -0
- package/spec/support/snapshots.js +293 -0
- package/spec/support/test-helpers.js +13 -0
- package/spec/types/concrete/bigint-type-spec.js +84 -0
- package/spec/types/concrete/boolean-type-spec.js +83 -0
- package/spec/types/concrete/date-type-spec.js +85 -0
- package/spec/types/concrete/datetime-type-spec.js +87 -0
- package/spec/types/concrete/float-type-spec.js +71 -0
- package/spec/types/concrete/foreign-key-type-spec.js +64 -0
- package/spec/types/concrete/integer-type-spec.js +71 -0
- package/spec/types/concrete/string-type-spec.js +91 -0
- package/spec/types/concrete/uuid-v1-type-spec.js +73 -0
- package/spec/types/concrete/uuid-v4-type-spec.js +65 -0
- package/spec/types/type-spec.js +101 -0
- package/spec/types/virtual/model-types-spec.js +401 -0
- package/spec/utils/misc-utils-spec.js +61 -0
- package/spec/utils/model-utils-spec.js +55 -0
- package/spec/utils/query-utils-spec.js +105 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Nife = require('nife');
|
|
4
|
+
const Inflection = require('inflection');
|
|
5
|
+
const RelationalTypeBase = require('./relational-type-base');
|
|
6
|
+
const Utils = require('../../utils');
|
|
7
|
+
|
|
8
|
+
const NAMED_METHOD = false;
|
|
9
|
+
|
|
10
|
+
async function destroyRelatedModels(connection, field, type, TargetModel, storedModels, invertMatch, options, operation) {
|
|
11
|
+
// Destroy all relations not matching stored set
|
|
12
|
+
let thisMethodName = type.fieldNameToOperationName(field, operation, NAMED_METHOD);
|
|
13
|
+
let parentModelName = this.getModelName();
|
|
14
|
+
let idMap = Utils.getPrimaryKeysForModels(connection, TargetModel, storedModels, { includeRelations: true, skipRelations: [ field.Model.getModelName() ] });
|
|
15
|
+
let { relationalMap } = await Utils.getRelationalModelStatusForField(connection, this, field);
|
|
16
|
+
let sortedModelNames = Nife.subtractFromArray(Utils.sortModelNamesByCreationOrder(connection, Object.keys(relationalMap)), [ parentModelName ]);
|
|
17
|
+
let targetRelationName = sortedModelNames[0];
|
|
18
|
+
let destroyModelNames = sortedModelNames.slice(1).reverse();
|
|
19
|
+
let targetIDs = idMap[targetRelationName];
|
|
20
|
+
let targetPKFieldName = TargetModel.getPrimaryKeyFieldName();
|
|
21
|
+
|
|
22
|
+
if (destroyModelNames.length === 0) {
|
|
23
|
+
// No through-table... this is a one-to-many relation.
|
|
24
|
+
// We can only destroy if the TargetModel has a foreign key
|
|
25
|
+
// to the parent model.
|
|
26
|
+
|
|
27
|
+
// Set destroyModelNames to the first relation.
|
|
28
|
+
// The following code will ensure they only get
|
|
29
|
+
// deleted if there is a foreign key relationship
|
|
30
|
+
// to the parent model.
|
|
31
|
+
destroyModelNames = [ targetRelationName ];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (let i = 0, il = destroyModelNames.length; i < il; i++) {
|
|
35
|
+
let modelName = destroyModelNames[i];
|
|
36
|
+
let ids = idMap[modelName];
|
|
37
|
+
|
|
38
|
+
// If we have no ids for this model, then
|
|
39
|
+
// there is nothing we can destroy
|
|
40
|
+
if (Nife.isEmpty(ids))
|
|
41
|
+
continue;
|
|
42
|
+
|
|
43
|
+
let ThisModel = connection.getModel(modelName);
|
|
44
|
+
let thisModelPKFieldName = ThisModel.getPrimaryKeyFieldName();
|
|
45
|
+
if (!thisModelPKFieldName) {
|
|
46
|
+
// Not sure how this would happen... since we obviously
|
|
47
|
+
// have a list of ids already... but let's be safe anyway
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// We can only destroy models that have a foreign key
|
|
52
|
+
// to the parent model.
|
|
53
|
+
let foreignFieldNames = ThisModel.getForeignKeysTargetFieldNames(connection, parentModelName);
|
|
54
|
+
if (Nife.isEmpty(foreignFieldNames))
|
|
55
|
+
continue;
|
|
56
|
+
|
|
57
|
+
// Create query to destroy related models
|
|
58
|
+
let query = ThisModel.where(connection)[thisModelPKFieldName];
|
|
59
|
+
if (invertMatch)
|
|
60
|
+
query = query.EQ(ids);
|
|
61
|
+
else
|
|
62
|
+
query = query.NOT.EQ(ids);
|
|
63
|
+
|
|
64
|
+
for (let j = 0, jl = foreignFieldNames.length; j < jl; j++) {
|
|
65
|
+
let { targetFieldName, sourceFieldName } = foreignFieldNames[j];
|
|
66
|
+
let parentModelValue = this[targetFieldName];
|
|
67
|
+
|
|
68
|
+
// If this value is empty then something is wrong...
|
|
69
|
+
// so fail early to prevent unwanted data loss
|
|
70
|
+
if (parentModelValue == null)
|
|
71
|
+
throw new Error(`${parentModelName}::${thisMethodName}: Field "${parentModelName}.${targetFieldName}" can not be empty for this operation.`);
|
|
72
|
+
|
|
73
|
+
query = query.AND[sourceFieldName].EQ(parentModelValue);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// If there is a foreign field that points to the target
|
|
77
|
+
// relation, then also match against those
|
|
78
|
+
if (targetPKFieldName && Nife.isNotEmpty(targetIDs)) {
|
|
79
|
+
let targetForeignField = ThisModel.getForeignKeysTargetField(connection, targetRelationName, targetPKFieldName);
|
|
80
|
+
if (targetForeignField) {
|
|
81
|
+
if (invertMatch)
|
|
82
|
+
query = query.AND[modelName][targetForeignField.sourceFieldName].EQ(targetIDs);
|
|
83
|
+
else
|
|
84
|
+
query = query.AND[modelName][targetForeignField.sourceFieldName].NOT.EQ(targetIDs);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Now destroy relations to update set
|
|
89
|
+
await query.destroy(options);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// These get injected into the class as
|
|
94
|
+
// addTo{fieldName}, get{fieldName}, etc...
|
|
95
|
+
const TYPE_OPERATIONS = {
|
|
96
|
+
'queryFor': async function({ field, type }, userQuery, options, ...args) {
|
|
97
|
+
return await type.prepareQuery({ connection: null, self: this, field, userQuery, options }, args);
|
|
98
|
+
},
|
|
99
|
+
'addTo': async function({ field }, _models, options) {
|
|
100
|
+
let models = Nife.toArray(_models).filter(Boolean);
|
|
101
|
+
if (Nife.isEmpty(models))
|
|
102
|
+
return [];
|
|
103
|
+
|
|
104
|
+
// TODO: Needs to manage unique constraints
|
|
105
|
+
// for through tables. Even though the model
|
|
106
|
+
// might be persisted, the through table record
|
|
107
|
+
// might still be created, which might blow up
|
|
108
|
+
// if a constraint fails because the records
|
|
109
|
+
// already exist
|
|
110
|
+
return this.getConnection(options && options.connection).transaction(async (connection) => {
|
|
111
|
+
let currentModels = this[field.fieldName];
|
|
112
|
+
if (Nife.isEmpty(currentModels))
|
|
113
|
+
currentModels = [];
|
|
114
|
+
|
|
115
|
+
let storedModels = await Utils.createAndSaveAllRelatedModels(connection, this, field, models, options);
|
|
116
|
+
let allModels = currentModels.concat(storedModels);
|
|
117
|
+
|
|
118
|
+
this[field.fieldName] = allModels;
|
|
119
|
+
|
|
120
|
+
return storedModels;
|
|
121
|
+
}, options);
|
|
122
|
+
},
|
|
123
|
+
'get': function({ field, type }, userQuery, options, ...args) {
|
|
124
|
+
const doGet = async function*() {
|
|
125
|
+
let query = await type.prepareQuery({ connection: null, self: this, field, userQuery, options }, args);
|
|
126
|
+
let results = query.all(Object.assign({}, options, { stream: true }));
|
|
127
|
+
let primaryModelRelationalArray = [];
|
|
128
|
+
|
|
129
|
+
this[field.fieldName] = primaryModelRelationalArray;
|
|
130
|
+
|
|
131
|
+
for await (let item of results) {
|
|
132
|
+
primaryModelRelationalArray.push(item);
|
|
133
|
+
|
|
134
|
+
yield item;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
if (options && options.stream === true)
|
|
139
|
+
return doGet.call(this);
|
|
140
|
+
else
|
|
141
|
+
return Utils.collect(doGet.call(this));
|
|
142
|
+
},
|
|
143
|
+
'set': async function({ field, type, addTo }, models, options) {
|
|
144
|
+
return this.getConnection(options && options.connection).transaction(async (connection) => {
|
|
145
|
+
let TargetModel = type.getTargetModel(connection);
|
|
146
|
+
|
|
147
|
+
// Reset relation so we don't end up with
|
|
148
|
+
// current persisted model instances
|
|
149
|
+
this[field.fieldName] = [];
|
|
150
|
+
|
|
151
|
+
// First, create models in set
|
|
152
|
+
let storedModels = await addTo.call(this, models, Object.assign({}, options || {}, { connection }));
|
|
153
|
+
|
|
154
|
+
await destroyRelatedModels.call(this, connection, field, type, TargetModel, storedModels, false, options, 'set');
|
|
155
|
+
|
|
156
|
+
return storedModels;
|
|
157
|
+
}, options);
|
|
158
|
+
},
|
|
159
|
+
'removeFrom': async function({ field, type, get }, _models, _options) {
|
|
160
|
+
return this.getConnection(_options && _options.connection).transaction(async (connection) => {
|
|
161
|
+
let { relationalMap, TargetModel } = await Utils.getRelationalModelStatusForField(connection, this, field);
|
|
162
|
+
let primaryModelName = this.getModelName();
|
|
163
|
+
let targetModelName = TargetModel.getModelName();
|
|
164
|
+
let models = Nife.toArray(_models);
|
|
165
|
+
|
|
166
|
+
const needsLoad = async () => {
|
|
167
|
+
let sortedModelNames = Utils.sortModelNamesByCreationOrder(connection, Object.keys(relationalMap));
|
|
168
|
+
|
|
169
|
+
for (let i = 0, il = models.length; i < il; i++) {
|
|
170
|
+
let model = models[i];
|
|
171
|
+
|
|
172
|
+
if (!(model instanceof TargetModel))
|
|
173
|
+
return true;
|
|
174
|
+
|
|
175
|
+
if (!model.isPersisted())
|
|
176
|
+
return true;
|
|
177
|
+
|
|
178
|
+
for (let j = 0, jl = sortedModelNames.length; j < jl; j++) {
|
|
179
|
+
let modelName = sortedModelNames[j];
|
|
180
|
+
if (modelName === targetModelName || modelName === primaryModelName)
|
|
181
|
+
continue;
|
|
182
|
+
|
|
183
|
+
let relation = relationalMap[modelName];
|
|
184
|
+
let ThisModel = relation.Model;
|
|
185
|
+
let pluralModelName = ThisModel.getPluralModelName();
|
|
186
|
+
|
|
187
|
+
if (!model[pluralModelName])
|
|
188
|
+
return true;
|
|
189
|
+
|
|
190
|
+
let relationSet = model[pluralModelName];
|
|
191
|
+
if (Nife.isEmpty(relationSet))
|
|
192
|
+
return true;
|
|
193
|
+
|
|
194
|
+
if (!relationSet.every((relatedModel) => relatedModel.isPersisted()))
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return false;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
let options = Object.assign({}, _options || {}, { connection });
|
|
203
|
+
let storedModels;
|
|
204
|
+
|
|
205
|
+
if (await needsLoad()) {
|
|
206
|
+
let query = Utils.generateQueryFromFilter(connection, TargetModel, models);
|
|
207
|
+
if (!query)
|
|
208
|
+
throw new Error(`${this.getModelName()}::${type.fieldNameToOperationName(field, 'removeFrom', NAMED_METHOD)}: Data provided is insufficient to complete operation.`);
|
|
209
|
+
|
|
210
|
+
storedModels = await get.call(this, TargetModel.where(connection).AND(query), Object.assign({}, options, { stream: false, includeRelations: true, connection }));
|
|
211
|
+
} else {
|
|
212
|
+
storedModels = models;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Destroy target relations to removed
|
|
216
|
+
// requested models from set
|
|
217
|
+
await destroyRelatedModels.call(this, connection, field, type, TargetModel, storedModels, true, options, 'removeFrom');
|
|
218
|
+
|
|
219
|
+
return storedModels.length;
|
|
220
|
+
}, _options);
|
|
221
|
+
},
|
|
222
|
+
'destroy': async function({ field, type }, userQuery, options, ...args) {
|
|
223
|
+
let query = await type.prepareQuery({ connection: null, self: this, field, userQuery, options }, args);
|
|
224
|
+
return await query.destroy(options);
|
|
225
|
+
},
|
|
226
|
+
'count': async function({ field, type }, userQuery, options, ...args) {
|
|
227
|
+
let query = await type.prepareQuery({ connection: null, self: this, field, userQuery, options }, args);
|
|
228
|
+
return await query.count(null, options);
|
|
229
|
+
},
|
|
230
|
+
'has': async function({ count }, userQuery, options, ...args) {
|
|
231
|
+
let itemCount = await count.call(this, userQuery, options, ...args);
|
|
232
|
+
return (itemCount > 0);
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
class ModelsType extends RelationalTypeBase {
|
|
237
|
+
static exposeToModel() {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
isManyRelation() {
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
fieldNameToOperationName(field, operation, rootMethod) {
|
|
246
|
+
let fieldName = field.pluralName;
|
|
247
|
+
if (!fieldName)
|
|
248
|
+
fieldName = Nife.capitalize(Inflection.pluralize(field.fieldName));
|
|
249
|
+
|
|
250
|
+
if (rootMethod)
|
|
251
|
+
return `_${operation}${fieldName}`;
|
|
252
|
+
else
|
|
253
|
+
return `${operation}${fieldName}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
initialize(connection, self) {
|
|
257
|
+
return super.initialize(connection, self, TYPE_OPERATIONS);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = {
|
|
262
|
+
Models: RelationalTypeBase.wrapConstructor(ModelsType),
|
|
263
|
+
ModelsType,
|
|
264
|
+
};
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Nife = require('nife');
|
|
4
|
+
const Type = require('../type');
|
|
5
|
+
const Utils = require('../../utils');
|
|
6
|
+
const { QueryEngine } = require('../../query-engine');
|
|
7
|
+
|
|
8
|
+
const NAMED_METHOD = false;
|
|
9
|
+
const ROOT_METHOD = true;
|
|
10
|
+
|
|
11
|
+
class RelationalTypeBase extends Type {
|
|
12
|
+
static isVirtual() {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static isRelational() {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Model types work by specifying a "target"
|
|
21
|
+
// and a "value provider" (source).
|
|
22
|
+
// These are fully qualified names, meaning
|
|
23
|
+
// they also point to the model as well as the field.
|
|
24
|
+
// If no model is specified, then it always defaults to
|
|
25
|
+
// "this" model. If no field is specified, then it always
|
|
26
|
+
// defaults to "this PK" of the model.
|
|
27
|
+
// Mythix ORM will recursively walk all models and fields
|
|
28
|
+
// defined until it has the full relationships between
|
|
29
|
+
// all fields.
|
|
30
|
+
constructor(_targetModelName, queryFactory, _options) {
|
|
31
|
+
let options = _options || {};
|
|
32
|
+
|
|
33
|
+
super(_targetModelName, queryFactory, options);
|
|
34
|
+
|
|
35
|
+
let targetModelName = _targetModelName;
|
|
36
|
+
if (!targetModelName || !(Nife.instanceOf(targetModelName, 'string') || targetModelName._isMythixModel))
|
|
37
|
+
throw new TypeError(`${this.constructor.name}::constructor: First argument is required to be a model or a model name.`);
|
|
38
|
+
|
|
39
|
+
if (targetModelName._isMythixModel)
|
|
40
|
+
targetModelName = targetModelName.getModelName();
|
|
41
|
+
|
|
42
|
+
Object.defineProperties(this, {
|
|
43
|
+
'targetModelName': {
|
|
44
|
+
writable: true,
|
|
45
|
+
enumberable: false,
|
|
46
|
+
configurable: true,
|
|
47
|
+
value: targetModelName,
|
|
48
|
+
},
|
|
49
|
+
'queryFactory': {
|
|
50
|
+
writable: true,
|
|
51
|
+
enumberable: false,
|
|
52
|
+
configurable: true,
|
|
53
|
+
value: queryFactory,
|
|
54
|
+
},
|
|
55
|
+
'options': {
|
|
56
|
+
writable: true,
|
|
57
|
+
enumberable: false,
|
|
58
|
+
configurable: true,
|
|
59
|
+
value: options,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getOptions() {
|
|
65
|
+
return this.options;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
initialize(connection, self, methods) {
|
|
69
|
+
if (typeof this.queryFactory !== 'function') {
|
|
70
|
+
let debugScope = '';
|
|
71
|
+
|
|
72
|
+
let Model = this.getModel();
|
|
73
|
+
let Field = this.getField();
|
|
74
|
+
|
|
75
|
+
if (Model || Field) {
|
|
76
|
+
let modelName = (Model) ? Model.getModelName() : '<UnknownModel>';
|
|
77
|
+
let fieldName = (Field) ? Field.fieldName : '<UnknownField>';
|
|
78
|
+
|
|
79
|
+
debugScope = `"${modelName}:${fieldName}" `;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
throw new TypeError(`${this.constructor.name}::initialize: ${debugScope}Bad relation arguments. Expected a method that returns a query.`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
super.initialize(connection, self);
|
|
86
|
+
|
|
87
|
+
if (!connection)
|
|
88
|
+
throw new Error(`${this.constructor.name}::initialize: A connection is required to use the ModelType.`);
|
|
89
|
+
|
|
90
|
+
let field = this.getField();
|
|
91
|
+
let context = {};
|
|
92
|
+
let binding = Object.create(context);
|
|
93
|
+
let operationNames = Object.keys(methods);
|
|
94
|
+
|
|
95
|
+
for (let i = 0, il = operationNames.length; i < il; i++) {
|
|
96
|
+
let operation = operationNames[i];
|
|
97
|
+
let method = methods[operation];
|
|
98
|
+
let methodName = this.fieldNameToOperationName(field, operation, NAMED_METHOD);
|
|
99
|
+
let fullMethodName = this.fieldNameToOperationName(field, operation, ROOT_METHOD);
|
|
100
|
+
|
|
101
|
+
context[operation] = function(...args) {
|
|
102
|
+
return this[methodName].apply(this, args);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
context[`_${operation}`] = function(...args) {
|
|
106
|
+
return this[fullMethodName].apply(this, args);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
Object.assign(binding, { methodName, fullMethodName, field, type: this });
|
|
110
|
+
|
|
111
|
+
Utils.injectModelMethod(self, method.bind(self, binding), methodName, fullMethodName);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async walkQueryRelations(connection, callback, context, ...args) {
|
|
116
|
+
const isAlreadyVisited = (leftContext, rightContext) => {
|
|
117
|
+
// Model can't be related to itself
|
|
118
|
+
if (leftContext.modelName === rightContext.modelName)
|
|
119
|
+
return true;
|
|
120
|
+
|
|
121
|
+
let firstKey = `${leftContext.modelName}:${leftContext.fieldName}<->${rightContext.modelName}:${rightContext.fieldName}`;
|
|
122
|
+
let secondKey = `${rightContext.modelName}:${rightContext.fieldName}<->${leftContext.modelName}:${leftContext.fieldName}`;
|
|
123
|
+
if (alreadyVisited.has(firstKey))
|
|
124
|
+
return true;
|
|
125
|
+
|
|
126
|
+
if (alreadyVisited.has(secondKey))
|
|
127
|
+
return true;
|
|
128
|
+
|
|
129
|
+
alreadyVisited.add(firstKey);
|
|
130
|
+
alreadyVisited.add(secondKey);
|
|
131
|
+
|
|
132
|
+
return false;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const prepareSourceAndTarget = (leftContext, rightContext) => {
|
|
136
|
+
const voidFieldForNonRelational = (info) => {
|
|
137
|
+
let { target, source } = info;
|
|
138
|
+
|
|
139
|
+
if (target.field.type.isRelational() && !source.field.type.isRelational()) {
|
|
140
|
+
source.field = null;
|
|
141
|
+
source.fieldName = null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!target.field.type.isRelational() && source.field.type.isRelational()) {
|
|
145
|
+
target.field = null;
|
|
146
|
+
target.fieldName = null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return info;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
let target = {
|
|
153
|
+
Model: leftContext.Model,
|
|
154
|
+
modelName: leftContext.modelName,
|
|
155
|
+
field: leftContext.Field,
|
|
156
|
+
fieldName: leftContext.fieldName,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
let source = {
|
|
160
|
+
Model: rightContext.Model,
|
|
161
|
+
modelName: rightContext.modelName,
|
|
162
|
+
field: rightContext.Field,
|
|
163
|
+
fieldName: rightContext.fieldName,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// The primary model is always the target
|
|
167
|
+
// so swap the order if it is currently
|
|
168
|
+
// marked as the source
|
|
169
|
+
let swapRelation = (source.modelName === primaryModelName);
|
|
170
|
+
|
|
171
|
+
return voidFieldForNonRelational({
|
|
172
|
+
PrimaryModel,
|
|
173
|
+
TargetModel,
|
|
174
|
+
TargetField,
|
|
175
|
+
target: (swapRelation) ? source : target,
|
|
176
|
+
source: (swapRelation) ? target : source,
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
let PrimaryModel = (context.self && context.self.getModel());
|
|
181
|
+
let primaryModelName = PrimaryModel.getModelName();
|
|
182
|
+
let query = await this.prepareQuery(Object.assign({ connection }, context), args);
|
|
183
|
+
let queryParts = query._getRawQuery();
|
|
184
|
+
let queryContext = (queryParts[queryParts.length - 1] || {});
|
|
185
|
+
let TargetModel = queryContext.rootModel;
|
|
186
|
+
let TargetField = queryContext.rootField;
|
|
187
|
+
let alreadyVisited = new Set();
|
|
188
|
+
|
|
189
|
+
for (let i = 0, il = queryParts.length; i < il; i++) {
|
|
190
|
+
let leftQueryContext = queryParts[i];
|
|
191
|
+
|
|
192
|
+
if (!(Object.prototype.hasOwnProperty.call(leftQueryContext, 'condition') && leftQueryContext.condition))
|
|
193
|
+
continue;
|
|
194
|
+
|
|
195
|
+
let conditionValue = leftQueryContext.value;
|
|
196
|
+
let rightQueryContext = null;
|
|
197
|
+
|
|
198
|
+
if (QueryEngine.isQuery(conditionValue)) {
|
|
199
|
+
rightQueryContext = conditionValue._getRawQueryContext();
|
|
200
|
+
if (rightQueryContext.condition)
|
|
201
|
+
continue;
|
|
202
|
+
} else {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (isAlreadyVisited(leftQueryContext, rightQueryContext))
|
|
207
|
+
continue;
|
|
208
|
+
|
|
209
|
+
callback(prepareSourceAndTarget(leftQueryContext, rightQueryContext));
|
|
210
|
+
|
|
211
|
+
if (leftQueryContext.Field.type.isForeignKey()) {
|
|
212
|
+
let fkField = leftQueryContext.Field.type;
|
|
213
|
+
let targetModel = fkField.getTargetModel(connection);
|
|
214
|
+
let targetModelName = fkField.getTargetModelName(connection);
|
|
215
|
+
let targetField = fkField.getTargetField(connection);
|
|
216
|
+
let targetFieldName = fkField.getTargetFieldName(connection);
|
|
217
|
+
let fkContext = {
|
|
218
|
+
Model: targetModel,
|
|
219
|
+
modelName: targetModelName,
|
|
220
|
+
field: targetField,
|
|
221
|
+
fieldName: targetFieldName,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
if (!isAlreadyVisited(fkContext, rightQueryContext))
|
|
225
|
+
callback(prepareSourceAndTarget(fkContext, rightQueryContext));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (rightQueryContext.Field.type.isForeignKey()) {
|
|
229
|
+
let fkField = rightQueryContext.Field.type;
|
|
230
|
+
let targetModel = fkField.getTargetModel(connection);
|
|
231
|
+
let targetModelName = fkField.getTargetModelName(connection);
|
|
232
|
+
let targetField = fkField.getTargetField(connection);
|
|
233
|
+
let targetFieldName = fkField.getTargetFieldName(connection);
|
|
234
|
+
let fkContext = {
|
|
235
|
+
Model: targetModel,
|
|
236
|
+
modelName: targetModelName,
|
|
237
|
+
field: targetField,
|
|
238
|
+
fieldName: targetFieldName,
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
if (!isAlreadyVisited(leftQueryContext, fkContext))
|
|
242
|
+
callback(prepareSourceAndTarget(leftQueryContext, fkContext));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (TargetField.type.isRelational() && TargetField.type.isManyRelation())
|
|
247
|
+
return query;
|
|
248
|
+
|
|
249
|
+
if (context.field.type.isRelational() && context.field.type.isManyRelation())
|
|
250
|
+
return query;
|
|
251
|
+
|
|
252
|
+
// Here the source and target are swapped
|
|
253
|
+
// because the target will be the primary model
|
|
254
|
+
// and the source (target model) is where we will
|
|
255
|
+
// be copying field values from
|
|
256
|
+
let targetModelName = TargetModel.getModelName();
|
|
257
|
+
let source = {
|
|
258
|
+
Model: TargetModel,
|
|
259
|
+
modelName: targetModelName,
|
|
260
|
+
Field: TargetField,
|
|
261
|
+
fieldName: TargetField.fieldName,
|
|
262
|
+
};
|
|
263
|
+
let target = {
|
|
264
|
+
Model: PrimaryModel,
|
|
265
|
+
modelName: PrimaryModel.getModelName(),
|
|
266
|
+
Field: context.field,
|
|
267
|
+
fieldName: context.field.fieldName,
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
if (!isAlreadyVisited(source, target))
|
|
271
|
+
callback(prepareSourceAndTarget(source, target));
|
|
272
|
+
|
|
273
|
+
return query;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
getTargetModel(_connection) {
|
|
277
|
+
let connection = _connection || this.getModel().getConnection();
|
|
278
|
+
return connection.getModel(this.targetModelName);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async prepareQuery({ connection: _connection, self, field, options: _options, userQuery: _userQuery }, args) {
|
|
282
|
+
let options = _options || {};
|
|
283
|
+
let queryFactory = this.queryFactory;
|
|
284
|
+
let connection = _connection || self.getConnection(options.connection);
|
|
285
|
+
let userQuery = _userQuery;
|
|
286
|
+
let TargetModel = this.getTargetModel(connection);
|
|
287
|
+
|
|
288
|
+
if (!QueryEngine.isQuery(userQuery) && Nife.instanceOf(userQuery, 'array', 'object', 'map', 'set'))
|
|
289
|
+
userQuery = Utils.generateQueryFromFilter(connection, TargetModel, userQuery);
|
|
290
|
+
|
|
291
|
+
let rootQuery = await queryFactory.call(
|
|
292
|
+
self,
|
|
293
|
+
Object.assign(
|
|
294
|
+
{
|
|
295
|
+
userQuery: userQuery || undefined,
|
|
296
|
+
type: this,
|
|
297
|
+
self,
|
|
298
|
+
connection,
|
|
299
|
+
field,
|
|
300
|
+
},
|
|
301
|
+
connection.getModels() || {},
|
|
302
|
+
),
|
|
303
|
+
...(args || []),
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
if (!QueryEngine.isQuery(rootQuery)) {
|
|
307
|
+
// eslint-disable-next-line no-magic-numbers
|
|
308
|
+
throw new Error(`${this.constructor.name}::prepareQuery: Query factory is required to return a query. Got "${('' + rootQuery).slice(0, 1000)}" instead.`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return rootQuery.AND[TargetModel.getModelName()];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
toConnectionType() {
|
|
315
|
+
throw new Error(`${this.constructor.name}::toConnectionType: Can not convert relational types to DB types.`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
toString() {
|
|
319
|
+
return `${this.constructor.name} {}`;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
module.exports = RelationalTypeBase;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MiscUtils = require('./misc-utils');
|
|
4
|
+
const ModelUtils = require('./model-utils');
|
|
5
|
+
const QueryUtils = require('./query-utils');
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
collect,
|
|
9
|
+
copyStaticProps,
|
|
10
|
+
objectAssignSpecial,
|
|
11
|
+
iterateStaticProps,
|
|
12
|
+
} = MiscUtils;
|
|
13
|
+
|
|
14
|
+
const {
|
|
15
|
+
assignRelatedModels,
|
|
16
|
+
buildQueryFromModelsAttributes,
|
|
17
|
+
constructModelsForCreationFromOriginField,
|
|
18
|
+
createAndSaveAllRelatedModels,
|
|
19
|
+
fieldToFullyQualifiedName,
|
|
20
|
+
getPrimaryKeysForModels,
|
|
21
|
+
getRelationalModelStatusForField,
|
|
22
|
+
injectModelMethod,
|
|
23
|
+
isUUID,
|
|
24
|
+
parseQualifiedName,
|
|
25
|
+
sanitizeFieldString,
|
|
26
|
+
setRelationalValues,
|
|
27
|
+
sortModelNamesByCreationOrder,
|
|
28
|
+
sortModelNamesByDependencyOrder,
|
|
29
|
+
} = ModelUtils;
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
parseFilterFieldAndOperator,
|
|
33
|
+
generateQueryFromFilter,
|
|
34
|
+
} = QueryUtils;
|
|
35
|
+
|
|
36
|
+
module.exports = {
|
|
37
|
+
MiscUtils,
|
|
38
|
+
ModelUtils,
|
|
39
|
+
|
|
40
|
+
// MiscUtils
|
|
41
|
+
collect,
|
|
42
|
+
copyStaticProps,
|
|
43
|
+
objectAssignSpecial,
|
|
44
|
+
iterateStaticProps,
|
|
45
|
+
|
|
46
|
+
// ModelUtils
|
|
47
|
+
assignRelatedModels,
|
|
48
|
+
buildQueryFromModelsAttributes,
|
|
49
|
+
constructModelsForCreationFromOriginField,
|
|
50
|
+
createAndSaveAllRelatedModels,
|
|
51
|
+
fieldToFullyQualifiedName,
|
|
52
|
+
getPrimaryKeysForModels,
|
|
53
|
+
getRelationalModelStatusForField,
|
|
54
|
+
injectModelMethod,
|
|
55
|
+
isUUID,
|
|
56
|
+
parseQualifiedName,
|
|
57
|
+
sanitizeFieldString,
|
|
58
|
+
setRelationalValues,
|
|
59
|
+
sortModelNamesByCreationOrder,
|
|
60
|
+
sortModelNamesByDependencyOrder,
|
|
61
|
+
|
|
62
|
+
// QueryUtils
|
|
63
|
+
parseFilterFieldAndOperator,
|
|
64
|
+
generateQueryFromFilter,
|
|
65
|
+
};
|