UNPKG

13.9 kBJavaScriptView Raw
1/**
2 * @copyright Copyright (c) 2019 Maxim Khorin <maksimovichu@gmail.com>
3 */
4'use strict';
5
6const Base = require('./Query');
7
8module.exports = class ActiveQuery extends Base {
9
10 _db = this.model.getDb();
11 _from = this.model.getTable();
12 _raw = null;
13
14 id () {
15 return this._db.queryScalar(this, this.model.PK);
16 }
17
18 ids () {
19 return this._db.queryColumn(this, this.model.PK);
20 }
21
22 raw (value = true) {
23 this._raw = value;
24 return this;
25 }
26
27 excludeModel (model) {
28 return model && model.getId()
29 ? this.and(['!=', model.PK, model.getId()])
30 : this;
31 }
32
33 indexByKey () {
34 return this.index(this.model.PK);
35 }
36
37 orderByKey (direction = 1) {
38 return this.order({[this.model.PK]: direction});
39 }
40
41 // PREPARE
42
43 // add condition after relation criteria creating
44 addAfterPrepare (handler) {
45 ObjectHelper.push(handler, '_afterPrepareHandlers', this);
46 return this;
47 }
48
49 prepare () {
50 if (!this.primaryModel) {
51 return this.afterPrepare();
52 }
53 if (this._viaArray) { // lazy loading of a relation
54 return this.prepareViaArray();
55 }
56 if (this._viaTable) {
57 return this.prepareViaTable(); // via junction table
58 }
59 if (this._viaRelation) {
60 return this._viaRelation.isMultiple()
61 ? this.prepareViaMultipleRelation()
62 : this.prepareViaRelation();
63 }
64 this.prepareFilter([this.primaryModel]);
65 return this.afterPrepare();
66 }
67
68 prepareViaArray () {
69 this._whereBeforeFilter = this._where;
70 const value = this.primaryModel.get(this.linkKey);
71 if (value === undefined || value === null) {
72 this.where(['FALSE']);
73 } else if (Array.isArray(value)) {
74 if (this._orderByKeys) {
75 this._orderByKeys = value;
76 }
77 this.and(['IN', this.refKey, value]);
78 } else { // back reference to array
79 this.and({[this.refKey]: value});
80 }
81 return this.afterPrepare();
82 }
83
84 async prepareViaTable () {
85 const viaModels = await this._viaTable.resolveJunctionRows([this.primaryModel]);
86 this.prepareFilter(viaModels);
87 await this.afterPrepare();
88 }
89
90 async prepareViaMultipleRelation () {
91 const models = await this._viaRelation.all();
92 this.primaryModel.populateRelation(this._viaRelationName, models);
93 this.prepareFilter(models);
94 await this.afterPrepare();
95 }
96
97 async prepareViaRelation () {
98 const model = await this._viaRelation.one();
99 this.primaryModel.populateRelation(this._viaRelationName, model);
100 this.prepareFilter(model ? [model] : []);
101 await this.afterPrepare();
102 }
103
104 prepareFilter (models) {
105 this._whereBeforeFilter = this._where;
106 this.filterByModels(models);
107 }
108
109 async afterPrepare () {
110 if (Array.isArray(this._afterPrepareHandlers)) {
111 for (const handler of this._afterPrepareHandlers) {
112 await handler(this);
113 }
114 }
115 }
116
117 // RELATION
118
119 asBackRef (value = true) {
120 this._asBackRef = value;
121 return this;
122 }
123
124 multiple (value = true) {
125 this._multiple = value;
126 return this;
127 }
128
129 isBackRef () {
130 return this._asBackRef === undefined
131 ? (this.primaryModel.PK === this.linkKey || !this.primaryModel.ATTRS.includes(this.linkKey))
132 : this._asBackRef;
133 }
134
135 isMultiple () {
136 return this._multiple;
137 }
138
139 isInternalArray () {
140 return this._viaArray && !this.isBackRef();
141 }
142
143 isOuterLink () {
144 return this._viaRelation || this._viaTable;
145 }
146
147 getViaArray () {
148 return this._viaArray;
149 }
150
151 getViaRelation () {
152 return this._viaRelation;
153 }
154
155 getViaRelationName () {
156 return this._viaRelationName;
157 }
158
159 getViaTable () {
160 return this._viaTable;
161 }
162
163 relateOne (primaryModel, refKey, linkKey) {
164 this.primaryModel = primaryModel;
165 this.refKey = refKey;
166 this.linkKey = linkKey;
167 this._multiple = false;
168 return this;
169 }
170
171 relateMany (primaryModel, refKey, linkKey) {
172 this.primaryModel = primaryModel;
173 this.refKey = refKey;
174 this.linkKey = linkKey;
175 this._multiple = true;
176 return this;
177 }
178
179 with (...relations) {
180 this._with = this._with || {};
181 for (const data of relations) {
182 if (!data) {
183 } else if (typeof data === 'string') {
184 this._with[data] = true;
185 } else if (Array.isArray(data)) {
186 this.with(...data);
187 } else if (data instanceof this.constructor) {
188 Object.assign(this._with, data._with);
189 } else {
190 Object.assign(this._with, data);
191 }
192 }
193 return this;
194 }
195
196 withOnly () {
197 this._with = {};
198 return this.with(...arguments);
199 }
200
201 via (name, filter) {
202 this._viaRelation = this.primaryModel.getRelation(name);
203 if (!this._viaRelation) {
204 this.primaryModel.log('error', this.wrapClassMessage(`via: Relation not found: ${name}`));
205 return this;
206 }
207 this._viaRelationName = name;
208 if (typeof filter === 'function') {
209 filter(this._viaRelation);
210 } else if (filter) { // as condition
211 this._viaRelation.and(filter);
212 }
213 return this;
214 }
215
216 viaTable (tableName, refKey, linkKey, filter) {
217 this._viaTable = new this.constructor({
218 model: this.primaryModel,
219 refKey,
220 linkKey
221 });
222 this._viaTable.from(tableName).multiple().raw();
223 if (typeof filter === 'function') {
224 filter(this._viaTable);
225 } else if (filter) { // as condition
226 this._viaTable.and(filter);
227 }
228 return this;
229 }
230
231 viaArray () {
232 this._viaArray = true;
233 if (this._orderByKeys === undefined) {
234 this._orderByKeys = true;
235 }
236 return this;
237 }
238
239 inverseOf (relationName) {
240 this._inverseOf = relationName;
241 return this;
242 }
243
244 async resolveWith (data, models) {
245 data = QueryHelper.normalizeRelations(this.model.spawnSelf(), data);
246 for (const name of Object.keys(data)) {
247 const relation = data[name];
248 if (relation.getRaw() === null) { // relation is ActiveQuery
249 relation.raw(this._raw); // inherit from primary query
250 }
251 await relation.populateRelation(name, models);
252 }
253 }
254
255 resolve () {
256 return this._multiple
257 ? (this.hasLinkValue() ? this.all() : [])
258 : (this.hasLinkValue() ? this.one() : null);
259 }
260
261 hasLinkValue () {
262 if (this._viaRelation || this._viaTable) {
263 return true;
264 }
265 const link = this.primaryModel.get(this.linkKey);
266 return link !== null && link !== undefined && (!Array.isArray(link) || link.length);
267 }
268
269 // POPULATE
270
271 populate (items) {
272 if (this._raw) {
273 return super.populate(items);
274 }
275 const models = [];
276 for (const item of items) {
277 const model = this.model.spawnSelf();
278 model.populate(item);
279 models.push(model);
280 }
281 return this.populateWith(models);
282 }
283
284 async populateWith (models) {
285 if (this._with && models.length) {
286 await this.resolveWith(this._with, models);
287 }
288 return this._index
289 ? QueryHelper.indexModels(models, this._index)
290 : models;
291 }
292
293 async populateRelation (name, primaryModels) {
294 const [viaModels, viaQuery] = await this.populateViaRelation(primaryModels);
295 if (!this._multiple && primaryModels.length === 1) {
296 const models = await this.all();
297 if (!models.length) {
298 return models;
299 }
300 this.populateOneRelation(name, models[0], primaryModels);
301 return models.slice(0, 1);
302 }
303 const index = this._index;
304 this._index = null;
305 const models = await this.all();
306 const buckets = this.getRelationBuckets(models, viaModels, viaQuery);
307 this._index = index;
308 const key = viaQuery ? viaQuery.linkKey : this.linkKey;
309 this.populateMultipleRelation(name, primaryModels, buckets, key);
310 return models;
311 }
312
313 populateOneRelation (name, model, primaryModels) {
314 for (const pm of primaryModels) {
315 if (pm instanceof ActiveRecord) {
316 pm.populateRelation(name, model);
317 } else {
318 pm[name] = model;
319 }
320 }
321 }
322
323 populateMultipleRelation (name, primaryModels, buckets, linkKey) {
324 for (const pm of primaryModels) {
325 const key = QueryHelper.getAttr(pm, linkKey);
326 let value = this._multiple ? [] : null;
327 if (Array.isArray(key)) {
328 for (const item of key) {
329 if (Object.prototype.hasOwnProperty.call(buckets, item)) {
330 value = value.concat(buckets[item]);
331 }
332 }
333 } else if (Object.prototype.hasOwnProperty.call(buckets, key)) {
334 value = buckets[key];
335 }
336 if (this._index && Array.isArray(value)) {
337 value = this._raw
338 ? QueryHelper.indexObjects(value, this._index)
339 : QueryHelper.indexModels(value, this._index);
340 }
341 if (pm instanceof ActiveRecord) {
342 pm.populateRelation(name, value);
343 } else {
344 pm[name] = value;
345 }
346 }
347 }
348
349 async populateViaRelation (viaModels) {
350 let viaQuery;
351 if (this._viaTable) {
352 viaQuery = this._viaTable;
353 viaModels = await viaQuery.resolveJunctionRows(viaModels);
354 }
355 if (this._viaRelation) {
356 if (this._viaRelation.getRaw() === null) {
357 this._viaRelation.raw(this._raw); // inherit from primary query
358 }
359 this._viaRelation.primaryModel = null;
360 viaQuery = this._viaRelation;
361 viaModels = await viaQuery.populateRelation(this._viaRelationName, viaModels);
362 }
363 this.prepareFilter(viaModels);
364 return [viaModels, viaQuery];
365 }
366
367 // BUCKETS
368
369 getRelationBuckets (models, viaModels, viaQuery) {
370 const buckets = (viaModels && viaQuery)
371 ? this.buildViaBuckets(models, viaModels, viaQuery.refKey)
372 : this.buildBuckets(models, this.refKey);
373 if (!this._multiple) {
374 for (const name of Object.keys(buckets)) {
375 buckets[name] = buckets[name][0];
376 }
377 }
378 return buckets;
379 }
380
381 buildViaBuckets (models, viaModels, viaRefKey) {
382 const buckets = {}, data = {};
383 for (const model of viaModels) {
384 const ref = QueryHelper.getAttr(model, viaRefKey);
385 const link = QueryHelper.getAttr(model, this.linkKey);
386 if (!Object.prototype.hasOwnProperty.call(data, link)) {
387 data[link] = {};
388 }
389 data[link][ref] = true;
390 }
391 for (const model of models) {
392 const ref = QueryHelper.getAttr(model, this.refKey);
393 if (data[ref]) {
394 for (const key of Object.keys(data[ref])) {
395 if (Array.isArray(buckets[key])) {
396 buckets[key].push(model);
397 } else {
398 buckets[key] = [model];
399 }
400 }
401 }
402 }
403 return buckets;
404 }
405
406 buildBuckets (models) {
407 const buckets = {};
408 for (const model of models) {
409 const key = QueryHelper.getAttr(model, this.refKey);
410 if (Array.isArray(buckets[key])) {
411 buckets[key].push(model);
412 } else {
413 buckets[key] = [model];
414 }
415 }
416 return buckets;
417 }
418
419 //
420
421 filterByModels (models) {
422 if (!Array.isArray(models)) {
423 return false;
424 }
425 const values = [];
426 const isActiveRecord = models[0] instanceof ActiveRecord;
427 const attr = this.linkKey;
428 for (const model of models) {
429 const value = isActiveRecord ? model.get(attr) : model[attr];
430 if (Array.isArray(value)) {
431 values.push(...value);
432 } else if (value !== undefined && value !== null && value !== '') {
433 values.push(value);
434 }
435 }
436 if (this._orderByKeys) {
437 this._orderByKeys = values;
438 }
439 values.length
440 ? this.and(['IN', this.refKey, values])
441 : this.where(['FALSE']);
442 }
443
444 resolveJunctionRows (models) {
445 if (!models.length) {
446 return [];
447 }
448 this.filterByModels(models);
449 return this.raw().all();
450 }
451};
452
453const ObjectHelper = require('../helper/ObjectHelper');
454const QueryHelper = require('../helper/QueryHelper');
455const ActiveRecord = require('./ActiveRecord');
\No newline at end of file