UNPKG

14.1 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 getDeleteOnUnlink () {
164 return this._deleteOnUnlink;
165 }
166
167 relateOne (primaryModel, refKey, linkKey) {
168 this.primaryModel = primaryModel;
169 this.refKey = refKey;
170 this.linkKey = linkKey;
171 this._multiple = false;
172 return this;
173 }
174
175 relateMany (primaryModel, refKey, linkKey) {
176 this.primaryModel = primaryModel;
177 this.refKey = refKey;
178 this.linkKey = linkKey;
179 this._multiple = true;
180 return this;
181 }
182
183 deleteOnUnlink (value = true) {
184 this._deleteOnUnlink = value;
185 return this;
186 }
187
188 with (...relations) {
189 this._with = this._with || {};
190 for (const data of relations) {
191 if (!data) {
192 } else if (typeof data === 'string') {
193 this._with[data] = true;
194 } else if (Array.isArray(data)) {
195 this.with(...data);
196 } else if (data instanceof this.constructor) {
197 Object.assign(this._with, data._with);
198 } else {
199 Object.assign(this._with, data);
200 }
201 }
202 return this;
203 }
204
205 withOnly () {
206 this._with = {};
207 return this.with(...arguments);
208 }
209
210 via (name, filter) {
211 this._viaRelation = this.primaryModel.getRelation(name);
212 if (!this._viaRelation) {
213 this.primaryModel.log('error', this.wrapClassMessage(`via: Relation not found: ${name}`));
214 return this;
215 }
216 this._viaRelationName = name;
217 if (typeof filter === 'function') {
218 filter(this._viaRelation);
219 } else if (filter) { // as condition
220 this._viaRelation.and(filter);
221 }
222 return this;
223 }
224
225 viaTable (tableName, refKey, linkKey, filter) {
226 this._viaTable = new this.constructor({
227 model: this.primaryModel,
228 refKey,
229 linkKey
230 });
231 this._viaTable.from(tableName).multiple().raw();
232 if (typeof filter === 'function') {
233 filter(this._viaTable);
234 } else if (filter) { // as condition
235 this._viaTable.and(filter);
236 }
237 return this;
238 }
239
240 viaArray () {
241 this._viaArray = true;
242 if (this._orderByKeys === undefined) {
243 this._orderByKeys = true;
244 }
245 return this;
246 }
247
248 inverseOf (relationName) {
249 this._inverseOf = relationName;
250 return this;
251 }
252
253 async resolveWith (data, models) {
254 data = QueryHelper.normalizeRelations(this.model.spawnSelf(), data);
255 for (const name of Object.keys(data)) {
256 const relation = data[name];
257 if (relation.getRaw() === null) { // relation is ActiveQuery
258 relation.raw(this._raw); // inherit from primary query
259 }
260 await relation.populateRelation(name, models);
261 }
262 }
263
264 resolve () {
265 return this._multiple
266 ? (this.hasLinkValue() ? this.all() : [])
267 : (this.hasLinkValue() ? this.one() : null);
268 }
269
270 hasLinkValue () {
271 if (this._viaRelation || this._viaTable) {
272 return true;
273 }
274 const link = this.primaryModel.get(this.linkKey);
275 return link !== null && link !== undefined && (!Array.isArray(link) || link.length);
276 }
277
278 // POPULATE
279
280 populate (items) {
281 if (this._raw) {
282 return super.populate(items);
283 }
284 const models = [];
285 for (const item of items) {
286 const model = this.model.spawnSelf();
287 model.populate(item);
288 models.push(model);
289 }
290 return this.populateWith(models);
291 }
292
293 async populateWith (models) {
294 if (this._with && models.length) {
295 await this.resolveWith(this._with, models);
296 }
297 return this._index
298 ? QueryHelper.indexModels(models, this._index)
299 : models;
300 }
301
302 async populateRelation (name, primaryModels) {
303 const [viaModels, viaQuery] = await this.populateViaRelation(primaryModels);
304 if (!this._multiple && primaryModels.length === 1) {
305 const models = await this.all();
306 if (!models.length) {
307 return models;
308 }
309 this.populateOneRelation(name, models[0], primaryModels);
310 return models.slice(0, 1);
311 }
312 const index = this._index;
313 this._index = null;
314 const models = await this.all();
315 const buckets = this.getRelationBuckets(models, viaModels, viaQuery);
316 this._index = index;
317 const key = viaQuery ? viaQuery.linkKey : this.linkKey;
318 this.populateMultipleRelation(name, primaryModels, buckets, key);
319 return models;
320 }
321
322 populateOneRelation (name, model, primaryModels) {
323 for (const pm of primaryModels) {
324 if (pm instanceof ActiveRecord) {
325 pm.populateRelation(name, model);
326 } else {
327 pm[name] = model;
328 }
329 }
330 }
331
332 populateMultipleRelation (name, primaryModels, buckets, linkKey) {
333 for (const pm of primaryModels) {
334 const key = QueryHelper.getAttr(pm, linkKey);
335 let value = this._multiple ? [] : null;
336 if (Array.isArray(key)) {
337 for (const item of key) {
338 if (Object.prototype.hasOwnProperty.call(buckets, item)) {
339 value = value.concat(buckets[item]);
340 }
341 }
342 } else if (Object.prototype.hasOwnProperty.call(buckets, key)) {
343 value = buckets[key];
344 }
345 if (this._index && Array.isArray(value)) {
346 value = this._raw
347 ? QueryHelper.indexObjects(value, this._index)
348 : QueryHelper.indexModels(value, this._index);
349 }
350 if (pm instanceof ActiveRecord) {
351 pm.populateRelation(name, value);
352 } else {
353 pm[name] = value;
354 }
355 }
356 }
357
358 async populateViaRelation (viaModels) {
359 let viaQuery;
360 if (this._viaTable) {
361 viaQuery = this._viaTable;
362 viaModels = await viaQuery.resolveJunctionRows(viaModels);
363 }
364 if (this._viaRelation) {
365 if (this._viaRelation.getRaw() === null) {
366 this._viaRelation.raw(this._raw); // inherit from primary query
367 }
368 this._viaRelation.primaryModel = null;
369 viaQuery = this._viaRelation;
370 viaModels = await viaQuery.populateRelation(this._viaRelationName, viaModels);
371 }
372 this.prepareFilter(viaModels);
373 return [viaModels, viaQuery];
374 }
375
376 // BUCKETS
377
378 getRelationBuckets (models, viaModels, viaQuery) {
379 const buckets = (viaModels && viaQuery)
380 ? this.buildViaBuckets(models, viaModels, viaQuery.refKey)
381 : this.buildBuckets(models, this.refKey);
382 if (!this._multiple) {
383 for (const name of Object.keys(buckets)) {
384 buckets[name] = buckets[name][0];
385 }
386 }
387 return buckets;
388 }
389
390 buildViaBuckets (models, viaModels, viaRefKey) {
391 const buckets = {}, data = {};
392 for (const model of viaModels) {
393 const ref = QueryHelper.getAttr(model, viaRefKey);
394 const link = QueryHelper.getAttr(model, this.linkKey);
395 if (!Object.prototype.hasOwnProperty.call(data, link)) {
396 data[link] = {};
397 }
398 data[link][ref] = true;
399 }
400 for (const model of models) {
401 const ref = QueryHelper.getAttr(model, this.refKey);
402 if (data[ref]) {
403 for (const key of Object.keys(data[ref])) {
404 if (Array.isArray(buckets[key])) {
405 buckets[key].push(model);
406 } else {
407 buckets[key] = [model];
408 }
409 }
410 }
411 }
412 return buckets;
413 }
414
415 buildBuckets (models) {
416 const buckets = {};
417 for (const model of models) {
418 const key = QueryHelper.getAttr(model, this.refKey);
419 if (Array.isArray(buckets[key])) {
420 buckets[key].push(model);
421 } else {
422 buckets[key] = [model];
423 }
424 }
425 return buckets;
426 }
427
428 //
429
430 filterByModels (models) {
431 if (!Array.isArray(models)) {
432 return false;
433 }
434 const values = [];
435 const isActiveRecord = models[0] instanceof ActiveRecord;
436 const attr = this.linkKey;
437 for (const model of models) {
438 const value = isActiveRecord ? model.get(attr) : model[attr];
439 if (Array.isArray(value)) {
440 values.push(...value);
441 } else if (value !== undefined && value !== null && value !== '') {
442 values.push(value);
443 }
444 }
445 if (this._orderByKeys) {
446 this._orderByKeys = values;
447 }
448 values.length
449 ? this.and(['IN', this.refKey, values])
450 : this.where(['FALSE']);
451 }
452
453 resolveJunctionRows (models) {
454 if (!models.length) {
455 return [];
456 }
457 this.filterByModels(models);
458 return this.raw().all();
459 }
460};
461
462const ObjectHelper = require('../helper/ObjectHelper');
463const QueryHelper = require('../helper/QueryHelper');
464const ActiveRecord = require('./ActiveRecord');
\No newline at end of file