mythix-orm 1.10.2 → 1.11.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.
@@ -3,11 +3,64 @@
3
3
  const Nife = require('nife');
4
4
  const ProxyClass = require('../proxy-class');
5
5
  const QueryEngineBase = require('./query-engine-base');
6
+ const QueryUtils = require('../utils/query-utils');
6
7
  const {
7
8
  LiteralBase,
8
9
  DistinctLiteral,
9
10
  } = require('../connection/literals');
10
11
 
12
+ function applyOrderClause(extraData, ...args) {
13
+ let entities = Nife.arrayFlatten(args);
14
+
15
+ entities = Nife.toArray(entities).map((value) => {
16
+ if (value == null)
17
+ return;
18
+
19
+ // Pass literals directly through
20
+ if (LiteralBase.isLiteral(value))
21
+ return value;
22
+
23
+ // Is the projection a field?
24
+ if (value.Model && value.fieldName)
25
+ return `${value.Model.getModelName()}:${value.fieldName}`;
26
+
27
+ if (!Nife.instanceOf(value, 'string'))
28
+ throw new Error('QueryEngine::ModelScope::ORDER: Invalid value provided. All values provided must be strings, fields, or literals. If you want to change the sort order of a given column, add "+" (ASC) or "-" (DESC) to be beginning of the field name. Example: .ORDER("+createdAt"), or .ORDER([ "-name", "+createdAt" ]).');
29
+
30
+ return value;
31
+ }).filter(Boolean);
32
+
33
+ let context = this.getOperationContext();
34
+ let order = this.margeFields(
35
+ context.order,
36
+ entities,
37
+ extraData,
38
+ { isOrderBy: true },
39
+ );
40
+
41
+ this._pushOperationOntoStack({
42
+ control: true,
43
+ operator: 'ORDER',
44
+ queryProp: 'ORDER',
45
+ value: entities,
46
+ order,
47
+ });
48
+
49
+ return this._fetchScope('model');
50
+ }
51
+
52
+ function wrapOrderClause(func) {
53
+ func.DESC = (...args) => {
54
+ return applyOrderClause.call(this, { direction: '-' }, ...args);
55
+ };
56
+
57
+ func.ASC = (...args) => {
58
+ return applyOrderClause.call(this, { direction: '+' }, ...args);
59
+ };
60
+
61
+ return func;
62
+ }
63
+
11
64
  class ModelScope extends QueryEngineBase {
12
65
  _getField(fieldName) {
13
66
  let Model = this.currentContext.Model;
@@ -46,18 +99,22 @@ class ModelScope extends QueryEngineBase {
46
99
  return this.currentContext.queryEngineScope.toString(...args);
47
100
  }
48
101
 
102
+ margeFields(currentFields, incomingFields, extraData, options) {
103
+ return QueryUtils.margeFields(this, currentFields, incomingFields, extraData, options);
104
+ }
105
+
49
106
  NOT = ProxyClass.autoCall(function() {
50
- this._pushOperationOntoStack({ logical: true, operator: 'NOT', not: !this.currentContext.not });
107
+ this._pushOperationOntoStack({ logical: true, operator: 'NOT', queryProp: 'NOT', not: !this.currentContext.not });
51
108
  return this._fetchScope('model');
52
109
  });
53
110
 
54
111
  AND = ProxyClass.autoCall(function(value) {
55
- this._pushOperationOntoStack({ logical: true, operator: 'AND', and: true, or: false, not: false, value });
112
+ this._pushOperationOntoStack({ logical: true, operator: 'AND', queryProp: 'AND', and: true, or: false, not: false, value });
56
113
  return this._fetchScope('model');
57
114
  });
58
115
 
59
116
  OR = ProxyClass.autoCall(function(value) {
60
- this._pushOperationOntoStack({ logical: true, operator: 'OR', and: false, or: true, not: false, value });
117
+ this._pushOperationOntoStack({ logical: true, operator: 'OR', queryProp: 'OR', and: false, or: true, not: false, value });
61
118
  return this._fetchScope('model');
62
119
  });
63
120
 
@@ -67,7 +124,7 @@ class ModelScope extends QueryEngineBase {
67
124
  throw new Error('QueryEngine::ModelScope::LIMIT: Value provided must be a valid positive number, or Infinity.');
68
125
 
69
126
  value = Math.round(value);
70
- this._pushOperationOntoStack({ control: true, operator: 'LIMIT', value, limit: value });
127
+ this._pushOperationOntoStack({ control: true, operator: 'LIMIT', queryProp: 'LIMIT', value, limit: value });
71
128
 
72
129
  return this._fetchScope('model');
73
130
  }
@@ -78,32 +135,93 @@ class ModelScope extends QueryEngineBase {
78
135
  throw new Error('QueryEngine::ModelScope::OFFSET: Value provided must be a valid positive number.');
79
136
 
80
137
  value = Math.round(value);
81
- this._pushOperationOntoStack({ control: true, operator: 'OFFSET', value, offset: value });
138
+ this._pushOperationOntoStack({ control: true, operator: 'OFFSET', queryProp: 'OFFSET', value, offset: value });
82
139
 
83
140
  return this._fetchScope('model');
84
141
  }
85
142
 
86
- ORDER(...args) {
87
- let values = Nife.arrayFlatten(args);
143
+ ORDER = wrapOrderClause.call(this, (...args) => {
144
+ return applyOrderClause.call(this, { direction: '+' }, ...args);
145
+ });
146
+
147
+ GROUP_BY(...args) {
148
+ let entities = Nife.arrayFlatten(args);
88
149
 
89
- values = Nife.toArray(values).filter((value) => {
150
+ entities = Nife.toArray(entities).map((value) => {
90
151
  if (value == null)
91
- return false;
152
+ return;
153
+
154
+ // Pass literals directly through
155
+ if (LiteralBase.isLiteral(value))
156
+ return value;
157
+
158
+ // Is the projection a field?
159
+ if (value.Model && value.fieldName)
160
+ return `${value.Model.getModelName()}:${value.fieldName}`;
92
161
 
93
162
  if (!Nife.instanceOf(value, 'string'))
94
- throw new Error('QueryEngine::ModelScope::ORDER: Invalid value provided. All values provided must be strings. If you want to change the sort order of a given column, add "+" (ASC) or "-" (DESC) to be beginning of the field name. Example: .ORDER("+createdAt"), or .ORDER([ "-name", "+createdAt" ]).');
163
+ throw new Error('QueryEngine::ModelScope::GROUP_BY: Invalid value provided. All values provided must be strings, fields, or literals. If you want to change the sort order of a given column, add "+" (ASC) or "-" (DESC) to be beginning of the field name. Example: .ORDER("+createdAt"), or .ORDER([ "-name", "+createdAt" ]).');
164
+
165
+ return value;
166
+ }).filter(Boolean);
167
+
168
+ let context = this.getOperationContext();
169
+ let groupBy = this.margeFields(
170
+ context.groupBy,
171
+ entities,
172
+ {},
173
+ { isGroupBy: true },
174
+ );
175
+
176
+ this._pushOperationOntoStack({
177
+ control: true,
178
+ operator: 'GROUP_BY',
179
+ queryProp: 'GROUP_BY',
180
+ value: entities,
181
+ groupBy,
182
+ });
183
+
184
+ return this._fetchScope('model');
185
+ }
186
+
187
+ HAVING(query) {
188
+ this._pushOperationOntoStack({ control: true, operator: 'HAVING', queryProp: 'HAVING', value: query, having: query });
189
+ return this._fetchScope('model');
190
+ }
191
+
192
+ EXISTS(_query) {
193
+ let query = _query;
194
+ let queryContext = (QueryEngineBase.isQuery(query)) ? query.getOperationContext() : null;
195
+ if (!queryContext || !queryContext.hasCondition)
196
+ throw new Error('QueryEngine::ModelScope::EXISTS: Provided value must be a query with conditions.');
197
+
198
+ if (!queryContext.projection) {
199
+ let Model = queryContext.Model;
200
+ let pkField = Model.getPrimaryKeyField();
201
+
202
+ if (pkField)
203
+ query = query.clone().PROJECT(pkField);
204
+ else
205
+ throw new Error('QueryEngine::ModelScope::EXISTS: Provided query must have only a single field projected.');
206
+ }
95
207
 
96
- return true;
208
+ this._pushOperationOntoStack({
209
+ condition: true,
210
+ operator: 'EXISTS',
211
+ inverseOperator: 'NOT EXISTS',
212
+ queryProp: 'EXISTS',
213
+ value: query,
214
+ having: query,
215
+ hasCondition: true,
97
216
  });
98
217
 
99
- this._pushOperationOntoStack({ control: true, operator: 'ORDER', value: values, order: values });
100
218
  return this._fetchScope('model');
101
219
  }
102
220
 
103
221
  PROJECT(...args) {
104
- let values = Nife.arrayFlatten(args);
222
+ let entities = Nife.arrayFlatten(args);
105
223
 
106
- values = Nife.toArray(values).map((value) => {
224
+ entities = Nife.toArray(entities).map((value) => {
107
225
  if (value == null)
108
226
  return;
109
227
 
@@ -119,63 +237,78 @@ class ModelScope extends QueryEngineBase {
119
237
  if (value.Model && value.fieldName)
120
238
  return value;
121
239
 
122
- if (!Nife.instanceOf(value, 'string'))
123
- throw new Error('QueryEngine::ModelScope::PROJECT: Invalid value provided. All values provided must be strings.');
240
+ if (!Nife.instanceOf(value, 'string')) {
241
+ console.log(entities);
242
+ throw new Error(`QueryEngine::ModelScope::PROJECT: Invalid value provided [${value.toString()}]. All values provided must be strings.`);
243
+ }
124
244
 
125
245
  return value;
126
246
  }).filter(Boolean);
127
247
 
128
- this._pushOperationOntoStack({ control: true, operator: 'PROJECT', value: values });
248
+ let context = this.getOperationContext();
249
+ let projection = this.margeFields(
250
+ context.projection,
251
+ entities,
252
+ {},
253
+ { isProjection: true },
254
+ );
255
+
256
+ this._pushOperationOntoStack({
257
+ control: true,
258
+ operator: 'PROJECT',
259
+ queryProp: 'PROJECT',
260
+ value: entities,
261
+ projection,
262
+ });
263
+
129
264
  return this._fetchScope('model');
130
265
  }
131
266
 
132
267
  DISTINCT = ProxyClass.autoCall(function(fullyQualifiedName) {
133
268
  let currentQuery = this;
134
269
  let distinctValue = fullyQualifiedName;
270
+ let context = this.getOperationContext();
135
271
 
136
272
  if (arguments.length === 0) {
137
- let context = this.getOperationContext();
138
273
  let rootModel = context.rootModel;
139
- if (!rootModel)
140
- throw new Error(`${this.constructor.name}::DISTINCT: Attempted to apply DISTINCT to the root model of the query, but no root model was found.`);
141
-
142
- let pkFieldName = rootModel.getPrimaryKeyFieldName();
143
- if (!pkFieldName)
144
- throw new Error(`${this.constructor.name}::DISTINCT: Attempted to apply DISTINCT to the root model of the query, but the root model has no primary key. Try directly specifying the DISTINCT field instead.`);
145
-
146
- distinctValue = new DistinctLiteral(`${rootModel.getModelName()}:${pkFieldName}`);
147
- currentQuery = this.PROJECT(`-${rootModel.getModelName()}:${pkFieldName}`);
274
+ if (rootModel) {
275
+ let pkFieldName = rootModel.getPrimaryKeyFieldName();
276
+ if (pkFieldName)
277
+ distinctValue = new DistinctLiteral(`${rootModel.getModelName()}:${pkFieldName}`);
278
+ }
279
+
280
+ if (!distinctValue)
281
+ distinctValue = new DistinctLiteral();
148
282
  } else if (fullyQualifiedName) {
149
283
  distinctValue = new DistinctLiteral(fullyQualifiedName);
150
- currentQuery = this.PROJECT(`-${fullyQualifiedName}`);
151
284
  }
152
285
 
153
- currentQuery._pushOperationOntoStack({ control: true, operator: 'DISTINCT', value: distinctValue, distinct: distinctValue });
286
+ currentQuery._pushOperationOntoStack({ sqlFunc: true, operator: 'DISTINCT', queryProp: 'DISTINCT', value: distinctValue, distinct: distinctValue });
154
287
  return this._fetchScope('model');
155
288
  });
156
289
 
157
290
  INNER_JOIN = ProxyClass.autoCall(function() {
158
- this._pushOperationOntoStack({ control: true, operator: 'JOIN', value: 'inner', joinType: 'inner', joinOuter: false });
291
+ this._pushOperationOntoStack({ join: true, operator: 'JOIN', queryProp: 'INNER_JOIN', value: 'inner', joinType: 'inner', joinOuter: false });
159
292
  return this._fetchScope('model');
160
293
  });
161
294
 
162
295
  LEFT_JOIN = ProxyClass.autoCall(function(outerJoin) {
163
- this._pushOperationOntoStack({ control: true, operator: 'JOIN', value: 'left', joinType: 'left', joinOuter: !!outerJoin });
296
+ this._pushOperationOntoStack({ join: true, operator: 'JOIN', queryProp: 'LEFT_JOIN', value: 'left', joinType: 'left', joinOuter: !!outerJoin });
164
297
  return this._fetchScope('model');
165
298
  });
166
299
 
167
300
  RIGHT_JOIN = ProxyClass.autoCall(function(outerJoin) {
168
- this._pushOperationOntoStack({ control: true, operator: 'JOIN', value: 'right', joinType: 'right', joinOuter: !!outerJoin });
301
+ this._pushOperationOntoStack({ join: true, operator: 'JOIN', queryProp: 'RIGHT_JOIN', value: 'right', joinType: 'right', joinOuter: !!outerJoin });
169
302
  return this._fetchScope('model');
170
303
  });
171
304
 
172
305
  FULL_JOIN = ProxyClass.autoCall(function(outerJoin) {
173
- this._pushOperationOntoStack({ control: true, operator: 'JOIN', value: 'full', joinType: 'full', joinOuter: !!outerJoin });
306
+ this._pushOperationOntoStack({ join: true, operator: 'JOIN', queryProp: 'FULL_JOIN', value: 'full', joinType: 'full', joinOuter: !!outerJoin });
174
307
  return this._fetchScope('model');
175
308
  });
176
309
 
177
310
  CROSS_JOIN = ProxyClass.autoCall(function() {
178
- this._pushOperationOntoStack({ control: true, operator: 'JOIN', value: 'cross', joinType: 'cross', joinOuter: false });
311
+ this._pushOperationOntoStack({ join: true, operator: 'JOIN', queryProp: 'CROSS_JOIN', value: 'cross', joinType: 'cross', joinOuter: false });
179
312
  return this._fetchScope('model');
180
313
  });
181
314
 
@@ -183,7 +316,7 @@ class ModelScope extends QueryEngineBase {
183
316
  if (!(Nife.instanceOf(type, 'string') || LiteralBase.isLiteral(type)))
184
317
  throw new Error('QueryEngine::ModelScope::JOIN: Invalid value provided. Value must be a valid string or Literal specifying JOIN type.');
185
318
 
186
- this._pushOperationOntoStack({ control: true, operator: 'JOIN', value: type, joinType: type, joinOuter: false });
319
+ this._pushOperationOntoStack({ join: true, operator: 'JOIN', queryProp: 'JOIN', value: type, joinType: type, joinOuter: false });
187
320
  return this._fetchScope('model');
188
321
  }
189
322
  }
@@ -13,8 +13,8 @@ class QueryEngineBase extends ProxyClass {
13
13
  return uuid++;
14
14
  }
15
15
 
16
- static isQueryContext(value) {
17
- return !!(value && value.isQueryContext);
16
+ static isQueryOperationContext(value) {
17
+ return !!(value && value.isQueryOperationContext);
18
18
  }
19
19
 
20
20
  static isQuery(value) {
@@ -33,7 +33,7 @@ class QueryEngineBase extends ProxyClass {
33
33
  return false;
34
34
  }
35
35
 
36
- static queryContextType(queryContext) {
36
+ static queryOperationInfo(queryContext) {
37
37
  let contextParams = {
38
38
  hasCondition: false,
39
39
  hasField: false,
@@ -124,8 +124,10 @@ class QueryEngineBase extends ProxyClass {
124
124
  let modelName = Model.getModelName();
125
125
  let context = this.currentContext;
126
126
  let extra = {};
127
+ let isFirst = !context.rootModel;
128
+ let connection = this.getConnection();
127
129
 
128
- if (!context.rootModel) {
130
+ if (isFirst) {
129
131
  extra.rootModelName = modelName;
130
132
  extra.rootModel = Model;
131
133
  }
@@ -141,13 +143,10 @@ class QueryEngineBase extends ProxyClass {
141
143
  throw new Error(`QueryEngine: Model "${Model.getModelName()}" is on a different connection dialect than the current query dialect of "${context.dialect}". You can not match different connection dialects in the same query.`);
142
144
  }
143
145
 
144
- if (!dialect) {
145
- let connection = this.getConnection();
146
- if (connection)
147
- dialect = connection.dialect;
148
- }
146
+ if (!dialect && connection)
147
+ dialect = connection.dialect;
149
148
 
150
- let newContext = this._inheritContext(context, 'model', { operator: 'MODEL', Model, modelName, dialect }, extra);
149
+ let newContext = this._inheritContext(context, 'model', { operator: 'MODEL', queryProp: 'Model', Model, modelName, dialect, value: modelName }, extra);
151
150
  let newScope = new ModelScopeClass(newContext);
152
151
 
153
152
  // We shouldn't add this scope if this is
@@ -155,6 +154,9 @@ class QueryEngineBase extends ProxyClass {
155
154
  if (context.Model !== Model)
156
155
  context.operationStack.push(newContext);
157
156
 
157
+ if (isFirst)
158
+ newScope = newScope.PROJECT(Model);
159
+
158
160
  // But we always need to return the scope
159
161
  // for the proxy to work properly
160
162
  return newScope;
@@ -171,7 +173,7 @@ class QueryEngineBase extends ProxyClass {
171
173
  extra.rootField = Field;
172
174
  }
173
175
 
174
- let newContext = this._inheritContext(context, 'field', { operator: 'FIELD', fieldName, Field }, extra);
176
+ let newContext = this._inheritContext(context, 'field', { operator: 'FIELD', queryProp: 'Field', fieldName, Field, value: fieldName }, extra);
175
177
  let newScope = new FieldScopeClass(newContext);
176
178
 
177
179
  // We shouldn't add this scope if this is
@@ -223,7 +225,7 @@ class QueryEngineBase extends ProxyClass {
223
225
  let currentContext = operationStack[operationStack.length - 1];
224
226
  return currentContext || context;
225
227
  },
226
- set: () => {},
228
+ set: () => {},
227
229
  },
228
230
  });
229
231
  }
@@ -289,7 +291,6 @@ class QueryEngineBase extends ProxyClass {
289
291
  else
290
292
  console.log(`${operator} -> ${queryPart.value}`);
291
293
  }
292
-
293
294
  }
294
295
 
295
296
  _pushOperationOntoStack(queryPart, _context) {
@@ -409,6 +410,38 @@ class QueryEngineBase extends ProxyClass {
409
410
  let checkContextKeys = _checkContextKeys || [ 'value' ];
410
411
  walkQueries(this, null, null, 0);
411
412
  }
413
+
414
+ getAllModelsUsedInQuery() {
415
+ let Models = new Set();
416
+ let query = this.getOperationStack();
417
+
418
+ for (let i = 0, il = query.length; i < il; i++) {
419
+ let queryPart = query[i];
420
+
421
+ if (Object.prototype.hasOwnProperty.call(queryPart, 'operator') && queryPart.operator === 'MODEL') {
422
+ let Model = queryPart.Model;
423
+ Models.add(Model);
424
+ } else if (Object.prototype.hasOwnProperty.call(queryPart, 'condition') && queryPart.condition === true) {
425
+ let operatorValue = queryPart.value;
426
+ if (!QueryEngineBase.isQuery(operatorValue) || operatorValue.queryHasConditions())
427
+ continue;
428
+
429
+ let SubModels = operatorValue.getAllModelsUsedInQuery();
430
+ for (let j = 0, jl = SubModels.length; j < jl; j++) {
431
+ let Model = SubModels[j];
432
+ Models.add(Model);
433
+ }
434
+ }
435
+ }
436
+
437
+ let allModels = Array.from(Models.values());
438
+ return allModels;
439
+ }
440
+
441
+ isModelUsedInQuery(Model) {
442
+ let allModels = this.getAllModelsUsedInQuery();
443
+ return (allModels.indexOf(Model) >= 0);
444
+ }
412
445
  }
413
446
 
414
447
  module.exports = QueryEngineBase;
@@ -19,9 +19,9 @@ export declare interface CallableInterface {
19
19
  export declare class QueryEngine<T = ConnectionBase> {
20
20
  // QueryEngineBase
21
21
  static generateID(): number;
22
- static isQueryContext(value: any): boolean;
22
+ static isQueryOperationContext(value: any): boolean;
23
23
  static isQuery(value: any): boolean;
24
- static queryContextType(queryContext: GenericObject): { hasCondition: boolean; hasField: boolean; hasModel: boolean; };
24
+ static queryOperationInfo(queryContext: GenericObject): { hasCondition: boolean; hasField: boolean; hasModel: boolean; };
25
25
 
26
26
  public getModelScopeClass(): QueryEngine;
27
27
  public getFieldScopeClass(): QueryEngine;
@@ -48,6 +48,7 @@ export declare class QueryEngine<T = ConnectionBase> {
48
48
  public filter(callback: (operation: GenericObject, index: number, operations: Array<GenericObject>, query: QueryEngine) => GenericObject): QueryEngine;
49
49
  public map(callback: (operation: GenericObject, index: number, operations: Array<GenericObject>, query: QueryEngine) => GenericObject): QueryEngine;
50
50
  public walk(callback: (query: QueryEngine, parent: GenericObject | null, contextKey: string, depth: number) => GenericObject, checkContextKeys?: Array<string>): void;
51
+ public getAllModelsUsedInQuery(): Array<ModelClass>;
51
52
 
52
53
  // QueryEngine
53
54
  public getModelScopeClass(): QueryEngineClass;
@@ -78,8 +79,11 @@ export declare class QueryEngine<T = ConnectionBase> {
78
79
  public Field(fieldName: string): QueryEngine;
79
80
  public LIMIT(value: number): QueryEngine;
80
81
  public OFFSET(value: number): QueryEngine;
81
- public ORDER(...args: Array<string | Array<string>>): QueryEngine;
82
- public PROJECT(...args: Array<string | ModelClass | LiteralBase | Field>): QueryEngine;
82
+ public ORDER(...args: Array<LiteralBase | Field | string | Array<LiteralBase | Field | string>>): QueryEngine;
83
+ public GROUP_BY(...args: Array<LiteralBase | Field | string | Array<LiteralBase | Field | string>>): QueryEngine;
84
+ public HAVING(query: QueryEngine): QueryEngine;
85
+ public EXISTS(query: QueryEngine): QueryEngine;
86
+ public PROJECT(...args: Array<string | ModelClass | LiteralBase | Field | Array<string | ModelClass | LiteralBase | Field>>): QueryEngine;
83
87
 
84
88
  declare public NOT: {
85
89
  (): QueryEngine;
@@ -20,9 +20,9 @@ class QueryEngine extends QueryEngineBase {
20
20
  let context = Object.assign(
21
21
  Object.create(_context || {}),
22
22
  {
23
- currentScopeName: 'queryEngine',
24
- isQueryContext: true,
25
- contextID: QueryEngineBase.generateID(),
23
+ currentScopeName: 'queryEngine',
24
+ isQueryOperationContext: true,
25
+ contextID: QueryEngineBase.generateID(),
26
26
  },
27
27
  );
28
28
 
@@ -39,15 +39,15 @@ class QueryEngine extends QueryEngineBase {
39
39
  return this._newModelScope(model);
40
40
  }
41
41
 
42
- unscoped(context) {
42
+ unscoped() {
43
+ let context = this.getOperationContext();
43
44
  let QueryEngineClass = this.constructor;
44
- let currentContext = context || this.currentContext;
45
45
  let queryEngine = new QueryEngineClass({
46
- connection: currentContext.connection,
46
+ connection: this.getConnection(),
47
47
  });
48
48
 
49
- if (currentContext.rootModelName)
50
- queryEngine = queryEngine[currentContext.rootModelName];
49
+ if (context.rootModelName)
50
+ queryEngine = queryEngine[context.rootModelName];
51
51
 
52
52
  return queryEngine;
53
53
  }
@@ -59,36 +59,23 @@ class QueryEngine extends QueryEngineBase {
59
59
  return queryGenerator.toConnectionString(this, ...args);
60
60
  }
61
61
 
62
- MERGE(_queryEngine) {
63
- let queryEngine = _queryEngine;
64
- if (!queryEngine)
62
+ MERGE(_incomingQueryEngine, _options) {
63
+ let incomingQueryEngine = _incomingQueryEngine;
64
+ if (!incomingQueryEngine)
65
65
  return this;
66
66
 
67
- let thisQueryContext = this.getOperationContext();
68
- if (!QueryEngine.isQuery(queryEngine) && Nife.instanceOf(queryEngine, 'array', 'object', 'map', 'set'))
69
- queryEngine = Utils.generateQueryFromFilter(this.getConnection(), thisQueryContext.rootModel, queryEngine);
70
-
71
- let sourceQuery = queryEngine.getOperationStack();
72
- let currentModel = thisQueryContext.Model;
73
- let skipLogicalOperators = true;
74
-
75
- for (let i = 0, il = sourceQuery.length; i < il; i++) {
76
- let queryPart = sourceQuery[i];
77
- let mergeContext = Utils.objectAssignSpecial(queryPart, null, [
78
- // Skip the following keys
79
- // as they are provided by the
80
- // parent queryEngine
81
- 'connection',
82
- 'fieldContext',
83
- 'isQueryContext',
84
- 'modelContext',
85
- 'queryEngineScope',
86
- 'operationStack',
87
- 'rootContext',
88
- 'rootModel',
89
- 'rootModelName',
90
- '_queryFinalized',
91
- ]);
67
+ let options = _options || {};
68
+ let thisQueryContext = this.getOperationContext();
69
+ if (!QueryEngine.isQuery(incomingQueryEngine) && Nife.instanceOf(incomingQueryEngine, 'array', 'object', 'map', 'set'))
70
+ incomingQueryEngine = Utils.generateQueryFromFilter(this.getConnection(options.connection), thisQueryContext.rootModel, incomingQueryEngine);
71
+
72
+ let incomingOperationStack = incomingQueryEngine.getOperationStack();
73
+ let skippingLogical = true;
74
+ let logicalOperatorEncountered = false;
75
+ let queryEngine = this.clone();
76
+
77
+ for (let i = 0, il = incomingOperationStack.length; i < il; i++) {
78
+ let queryPart = incomingOperationStack[i];
92
79
 
93
80
  // For merges, we want to skip the first logical operators
94
81
  // found before any other operation.
@@ -98,32 +85,38 @@ class QueryEngine extends QueryEngineBase {
98
85
  // Since the result we want here is OR merge, not AND merge
99
86
  // we skip the first "AND" we encounter, leaving the "OR" as
100
87
  // the current logical operator.
101
- if (skipLogicalOperators && Object.prototype.hasOwnProperty.call(mergeContext, 'logical') && mergeContext.logical) {
102
- if (mergeContext.value == null && (mergeContext.operator === 'AND' || mergeContext.operator === 'OR'))
88
+ // Logical operators do not always come first in the query,
89
+ // so we need to rely on "logicalOperatorEncountered" to
90
+ // ensure we skip only the first one (or first few in a sequence).
91
+ if (skippingLogical && Object.prototype.hasOwnProperty.call(queryPart, 'logical') && queryPart.logical) {
92
+ if (queryPart.value == null && (queryPart.operator === 'AND' || queryPart.operator === 'OR')) {
93
+ logicalOperatorEncountered = true;
103
94
  continue;
95
+ }
104
96
  }
105
97
 
106
- if (Object.prototype.hasOwnProperty.call(mergeContext, 'operator')) {
107
- // Skip unneeded duplicate model entries
108
- if (mergeContext.operator === 'MODEL') {
109
- if (mergeContext.Model === currentModel)
110
- continue;
98
+ if (logicalOperatorEncountered)
99
+ skippingLogical = false;
111
100
 
112
- currentModel = mergeContext.Model;
113
- } else if (mergeContext.operator !== 'FIELD') {
114
- skipLogicalOperators = false;
115
- }
101
+ let value = queryPart.value;
102
+ if (Object.prototype.hasOwnProperty.call(queryPart, 'control') && queryPart.control === true) {
103
+ if (queryPart.operator === 'PROJECT' && options.projections !== true)
104
+ continue;
105
+
106
+ if (queryPart.operator === 'ORDER' && options.orders === false)
107
+ continue;
108
+
109
+ if (queryPart.operator === 'GROUP_BY' && options.groupBys === false)
110
+ continue;
111
+
112
+ if (queryPart.operator === 'PROJECT' || queryPart.operator === 'ORDER' || queryPart.operator === 'GROUP_BY')
113
+ value = [ '+' ].concat(value);
116
114
  }
117
115
 
118
- this._pushOperationOntoStack(Object.assign({
119
- Model: queryPart.Model,
120
- modelName: queryPart.modelName,
121
- Field: queryPart.Field,
122
- fieldName: queryPart.fieldName,
123
- }, mergeContext));
116
+ queryEngine = queryEngine[queryPart.queryProp](value, ...(queryPart.queryExtraArgs || []));
124
117
  }
125
118
 
126
- return this;
119
+ return queryEngine;
127
120
  }
128
121
 
129
122
  async all(options) {
@@ -269,6 +269,6 @@ class DateTimeType extends Type {
269
269
  }
270
270
 
271
271
  module.exports = {
272
- DATETIME: Type.wrapConstructor(DateTimeType),
272
+ DATETIME: Type.wrapConstructor(DateTimeType),
273
273
  DateTimeType,
274
274
  };
@@ -110,13 +110,13 @@ class SerializedType extends Type {
110
110
  options = { type: options };
111
111
 
112
112
  options = {
113
- serialize: ({ value }) => {
113
+ serialize: ({ value }) => {
114
114
  if (value == null)
115
115
  return value;
116
116
 
117
117
  return JSON.stringify(value);
118
118
  },
119
- deserialize: ({ value }) => {
119
+ deserialize: ({ value }) => {
120
120
  if (value == null)
121
121
  return value;
122
122
 
@@ -72,12 +72,12 @@ const TYPE_OPERATIONS = {
72
72
  if (!model.isDirty())
73
73
  return model;
74
74
 
75
- let [ storedModel ] = await connection.update(model.getModel(), [ model ], options);
75
+ await connection.update(model.getModel(), [ model ], options);
76
76
 
77
77
  // Update this model to reflect the update
78
- ModelUtils.setRelationalValues(connection, this.getModel(), this, storedModel.getModel(), storedModel);
78
+ ModelUtils.setRelationalValues(connection, this.getModel(), this, model.getModel(), model);
79
79
 
80
- return storedModel;
80
+ return model;
81
81
  }, options);
82
82
 
83
83
  // Save needs to go outside of the transaction