UNPKG

16.8 kBJavaScriptView Raw
1// Copyright IBM Corp. 2013,2019. All Rights Reserved.
2// Node module: loopback-datasource-juggler
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6'use strict';
7
8const _ = require('lodash');
9const i8n = require('inflection');
10const g = require('strong-globalize')();
11const utils = require('./utils');
12const defineCachedRelations = utils.defineCachedRelations;
13const setScopeValuesFromWhere = utils.setScopeValuesFromWhere;
14const mergeQuery = utils.mergeQuery;
15const DefaultModelBaseClass = require('./model.js');
16const collectTargetIds = utils.collectTargetIds;
17const idName = utils.idName;
18const deprecated = require('depd')('loopback-datasource-juggler');
19
20/**
21 * Module exports
22 */
23exports.defineScope = defineScope;
24
25function ScopeDefinition(definition) {
26 this.isStatic = definition.isStatic;
27 this.modelFrom = definition.modelFrom;
28 this.modelTo = definition.modelTo || definition.modelFrom;
29 this.name = definition.name;
30 this.params = definition.params;
31 this.methods = definition.methods || {};
32 this.options = definition.options || {};
33}
34
35ScopeDefinition.prototype.targetModel = function(receiver) {
36 let modelTo;
37 if (typeof this.options.modelTo === 'function') {
38 modelTo = this.options.modelTo.call(this, receiver) || this.modelTo;
39 } else {
40 modelTo = this.modelTo;
41 }
42 if (!(modelTo.prototype instanceof DefaultModelBaseClass)) {
43 let msg = 'Invalid target model for scope `';
44 msg += (this.isStatic ? this.modelFrom : this.modelFrom.constructor).modelName;
45 msg += this.isStatic ? '.' : '.prototype.';
46 msg += this.name + '`.';
47 throw new Error(msg);
48 }
49 return modelTo;
50};
51
52/*!
53 * Find related model instances
54 * @param {*} receiver The target model class/prototype
55 * @param {Object|Function} scopeParams
56 * @param {Boolean|Object} [condOrRefresh] true for refresh or object as a filter
57 * @param {Object} [options]
58 * @param {Function} cb
59 * @returns {*}
60 */
61ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefresh, options, cb) {
62 const name = this.name;
63 const self = receiver;
64
65 let actualCond = {};
66 let actualRefresh = false;
67 let saveOnCache = receiver instanceof DefaultModelBaseClass;
68 if (typeof condOrRefresh === 'function' &&
69 options === undefined && cb === undefined) {
70 // related(receiver, scopeParams, cb)
71 cb = condOrRefresh;
72 options = {};
73 condOrRefresh = undefined;
74 } else if (typeof options === 'function' && cb === undefined) {
75 cb = options;
76 options = {};
77 }
78 options = options || {};
79 if (condOrRefresh !== undefined) {
80 if (typeof condOrRefresh === 'boolean') {
81 actualRefresh = condOrRefresh;
82 } else {
83 actualCond = condOrRefresh;
84 actualRefresh = true;
85 saveOnCache = false;
86 }
87 }
88 cb = cb || utils.createPromiseCallback();
89 const refreshIsNeeded = !self.__cachedRelations ||
90 self.__cachedRelations[name] === undefined ||
91 actualRefresh;
92 if (refreshIsNeeded) {
93 // It either doesn't hit the cache or refresh is required
94 const params = mergeQuery(actualCond, scopeParams, {nestedInclude: true});
95 const targetModel = this.targetModel(receiver);
96
97 // If there is a through model
98 // run another query to apply filter on relatedModel(targetModel)
99 // see github.com/strongloop/loopback-datasource-juggler/issues/166
100 const scopeOnRelatedModel = params.collect &&
101 params.include.scope !== null &&
102 typeof params.include.scope === 'object';
103 let filter, queryRelated;
104 if (scopeOnRelatedModel) {
105 filter = params.include;
106 // The filter applied on relatedModel
107 queryRelated = filter.scope;
108 delete params.include.scope;
109 }
110
111 targetModel.find(params, options, function(err, data) {
112 if (!err && saveOnCache) {
113 defineCachedRelations(self);
114 self.__cachedRelations[name] = data;
115 }
116
117 if (scopeOnRelatedModel === true) {
118 const relatedModel = targetModel.relations[filter.relation].modelTo;
119 const IdKey = idName(relatedModel);
120
121 // return {inq: [1,2,3]}}
122 const smartMerge = function(idCollection, qWhere) {
123 if (!qWhere[IdKey]) return idCollection;
124 let merged = {};
125
126 const idsA = idCollection.inq;
127 const idsB = qWhere[IdKey].inq ? qWhere[IdKey].inq : [qWhere[IdKey]];
128
129 const intersect = _.intersectionWith(idsA, idsB, _.isEqual);
130 if (intersect.length === 1) merged = intersect[0];
131 if (intersect.length > 1) merged = {inq: intersect};
132
133 return merged;
134 };
135
136 if (queryRelated.where !== undefined) {
137 // Merge queryRelated filter and targetId filter
138 const IdKeyCondition = {};
139 IdKeyCondition[IdKey] = smartMerge(collectTargetIds(data, IdKey),
140 queryRelated.where);
141
142 // if the id in filter doesn't exist after the merge,
143 // return empty result
144 if (_.isObject(IdKeyCondition[IdKey]) && _.isEmpty(IdKeyCondition[IdKey])) return cb(null, []);
145
146 const mergedWhere = {
147 and: [
148 IdKeyCondition,
149 _.omit(queryRelated.where, IdKey),
150 ],
151 };
152 queryRelated.where = mergedWhere;
153 } else {
154 queryRelated.where = {};
155 queryRelated.where[IdKey] = collectTargetIds(data, IdKey);
156 }
157
158 relatedModel.find(queryRelated, options, cb);
159 } else {
160 cb(err, data);
161 }
162 });
163 } else {
164 // Return from cache
165 cb(null, self.__cachedRelations[name]);
166 }
167 return cb.promise;
168};
169
170/**
171 * Define a scope method
172 * @param {String} name of the method
173 * @param {Function} function to define
174 */
175ScopeDefinition.prototype.defineMethod = function(name, fn) {
176 return this.methods[name] = fn;
177};
178
179/**
180 * Define a scope to the class
181 * @param {Model} cls The class where the scope method is added
182 * @param {Model} targetClass The class that a query to run against
183 * @param {String} name The name of the scope
184 * @param {Object|Function} params The parameters object for the query or a function
185 * to return the query object
186 * @param methods An object of methods keyed by the method name to be bound to the class
187 */
188function defineScope(cls, targetClass, name, params, methods, options) {
189 // collect meta info about scope
190 if (!cls._scopeMeta) {
191 cls._scopeMeta = {};
192 }
193
194 // only makes sense to add scope in meta if base and target classes
195 // are same
196 if (cls === targetClass) {
197 cls._scopeMeta[name] = params;
198 } else if (targetClass) {
199 if (!targetClass._scopeMeta) {
200 targetClass._scopeMeta = {};
201 }
202 }
203
204 options = options || {};
205 // Check if the cls is the class itself or its prototype
206 const isStatic = (typeof cls === 'function') || options.isStatic || false;
207 const definition = new ScopeDefinition({
208 isStatic: isStatic,
209 modelFrom: cls,
210 modelTo: targetClass,
211 name: name,
212 params: params,
213 methods: methods,
214 options: options,
215 });
216
217 if (isStatic) {
218 cls.scopes = cls.scopes || {};
219 cls.scopes[name] = definition;
220 } else {
221 cls.constructor.scopes = cls.constructor.scopes || {};
222 cls.constructor.scopes[name] = definition;
223 }
224
225 // Define a property for the scope
226 Object.defineProperty(cls, name, {
227 enumerable: false,
228 configurable: true,
229 /**
230 * This defines a property for the scope. For example, user.accounts or
231 * User.vips. Please note the cls can be the model class or prototype of
232 * the model class.
233 *
234 * The property value is function. It can be used to query the scope,
235 * such as user.accounts(condOrRefresh, cb) or User.vips(cb). The value
236 * can also have child properties for create/build/delete. For example,
237 * user.accounts.create(act, cb).
238 *
239 */
240 get: function() {
241 const targetModel = definition.targetModel(this);
242 const self = this;
243
244 const f = function(condOrRefresh, options, cb) {
245 if (arguments.length === 0) {
246 if (typeof f.value === 'function') {
247 return f.value(self);
248 } else if (self.__cachedRelations) {
249 return self.__cachedRelations[name];
250 }
251 } else {
252 const condOrRefreshIsCallBack = typeof condOrRefresh === 'function' &&
253 options === undefined &&
254 cb === undefined;
255 if (condOrRefreshIsCallBack) {
256 // customer.orders(cb)
257 cb = condOrRefresh;
258 options = {};
259 condOrRefresh = undefined;
260 } else if (typeof options === 'function' && cb === undefined) {
261 // customer.orders(condOrRefresh, cb);
262 cb = options;
263 options = {};
264 }
265 options = options || {};
266 // Check if there is a through model
267 // see https://github.com/strongloop/loopback/issues/1076
268 if (f._scope.collect &&
269 condOrRefresh !== null && typeof condOrRefresh === 'object') {
270 f._scope.include = {
271 relation: f._scope.collect,
272 scope: condOrRefresh,
273 };
274 condOrRefresh = {};
275 }
276 return definition.related(self, f._scope, condOrRefresh, options, cb);
277 }
278 };
279
280 f._receiver = this;
281 f._scope = typeof definition.params === 'function' ?
282 definition.params.call(self) : definition.params;
283
284 f._targetClass = targetModel.modelName;
285 if (f._scope.collect) {
286 const rel = targetModel.relations[f._scope.collect];
287 f._targetClass = rel && rel.modelTo && rel.modelTo.modelName || i8n.camelize(f._scope.collect);
288 }
289
290 f.find = function(condOrRefresh, options, cb) {
291 if (typeof condOrRefresh === 'function' &&
292 options === undefined && cb === undefined) {
293 cb = condOrRefresh;
294 options = {};
295 condOrRefresh = {};
296 } else if (typeof options === 'function' && cb === undefined) {
297 cb = options;
298 options = {};
299 }
300 options = options || {};
301 return definition.related(self, f._scope, condOrRefresh, options, cb);
302 };
303
304 f.getAsync = function() {
305 deprecated(g.f('Scope method "getAsync()" is deprecated, use "find()" instead.'));
306 return this.find.apply(this, arguments);
307 };
308
309 f.build = build;
310 f.create = create;
311 f.updateAll = updateAll;
312 f.destroyAll = destroyAll;
313 f.findById = findById;
314 f.findOne = findOne;
315 f.count = count;
316
317 for (const i in definition.methods) {
318 f[i] = definition.methods[i].bind(self);
319 }
320
321 if (!targetClass) return f;
322
323 // Define scope-chaining, such as
324 // Station.scope('active', {where: {isActive: true}});
325 // Station.scope('subway', {where: {isUndeground: true}});
326 // Station.active.subway(cb);
327 Object.keys(targetClass._scopeMeta).forEach(function(name) {
328 Object.defineProperty(f, name, {
329 enumerable: false,
330 get: function() {
331 mergeQuery(f._scope, targetModel._scopeMeta[name]);
332 return f;
333 },
334 });
335 }.bind(self));
336 return f;
337 },
338 });
339
340 // Wrap the property into a function for remoting
341 const fn = function() {
342 // primaryObject.scopeName, such as user.accounts
343 const f = this[name];
344 // set receiver to be the scope property whose value is a function
345 f.apply(this[name], arguments);
346 };
347
348 cls['__get__' + name] = fn;
349
350 const fnCreate = function() {
351 const f = this[name].create;
352 f.apply(this[name], arguments);
353 };
354
355 cls['__create__' + name] = fnCreate;
356
357 const fnDelete = function() {
358 const f = this[name].destroyAll;
359 f.apply(this[name], arguments);
360 };
361
362 cls['__delete__' + name] = fnDelete;
363
364 const fnUpdate = function() {
365 const f = this[name].updateAll;
366 f.apply(this[name], arguments);
367 };
368
369 cls['__update__' + name] = fnUpdate;
370
371 const fnFindById = function(cb) {
372 const f = this[name].findById;
373 f.apply(this[name], arguments);
374 };
375
376 cls['__findById__' + name] = fnFindById;
377
378 const fnFindOne = function(cb) {
379 const f = this[name].findOne;
380 f.apply(this[name], arguments);
381 };
382
383 cls['__findOne__' + name] = fnFindOne;
384
385 const fnCount = function(cb) {
386 const f = this[name].count;
387 f.apply(this[name], arguments);
388 };
389
390 cls['__count__' + name] = fnCount;
391
392 // and it should have create/build methods with binded thisModelNameId param
393 function build(data) {
394 data = data || {};
395 // Find all fixed property values for the scope
396 const targetModel = definition.targetModel(this._receiver);
397 const where = (this._scope && this._scope.where) || {};
398 setScopeValuesFromWhere(data, where, targetModel);
399 return new targetModel(data);
400 }
401
402 function create(data, options, cb) {
403 if (typeof data === 'function' &&
404 options === undefined && cb === undefined) {
405 // create(cb)
406 cb = data;
407 data = {};
408 } else if (typeof options === 'function' && cb === undefined) {
409 // create(data, cb)
410 cb = options;
411 options = {};
412 }
413 options = options || {};
414 return this.build(data).save(options, cb);
415 }
416
417 /*
418 Callback
419 - The callback will be called after all elements are destroyed
420 - For every destroy call which results in an error
421 - If fetching the Elements on which destroyAll is called results in an error
422 */
423 function destroyAll(where, options, cb) {
424 if (typeof where === 'function') {
425 // destroyAll(cb)
426 cb = where;
427 where = {};
428 } else if (typeof options === 'function' && cb === undefined) {
429 // destroyAll(where, cb)
430 cb = options;
431 options = {};
432 }
433 options = options || {};
434
435 const targetModel = definition.targetModel(this._receiver);
436 const scoped = (this._scope && this._scope.where) || {};
437 const filter = mergeQuery({where: scoped}, {where: where || {}});
438 return targetModel.destroyAll(filter.where, options, cb);
439 }
440
441 function updateAll(where, data, options, cb) {
442 if (typeof data === 'function' &&
443 options === undefined && cb === undefined) {
444 // updateAll(data, cb)
445 cb = data;
446 data = where;
447 where = {};
448 options = {};
449 } else if (typeof options === 'function' && cb === undefined) {
450 // updateAll(where, data, cb)
451 cb = options;
452 options = {};
453 }
454 options = options || {};
455 const targetModel = definition.targetModel(this._receiver);
456 const scoped = (this._scope && this._scope.where) || {};
457 const filter = mergeQuery({where: scoped}, {where: where || {}});
458 return targetModel.updateAll(filter.where, data, options, cb);
459 }
460
461 function findById(id, filter, options, cb) {
462 if (options === undefined && cb === undefined) {
463 if (typeof filter === 'function') {
464 // findById(id, cb)
465 cb = filter;
466 filter = {};
467 }
468 } else if (cb === undefined) {
469 if (typeof options === 'function') {
470 // findById(id, query, cb)
471 cb = options;
472 options = {};
473 if (typeof filter === 'object' && !(filter.include || filter.fields)) {
474 // If filter doesn't have include or fields, assuming it's options
475 options = filter;
476 filter = {};
477 }
478 }
479 }
480
481 options = options || {};
482 filter = filter || {};
483 const targetModel = definition.targetModel(this._receiver);
484 const idName = targetModel.definition.idName();
485 let query = {where: {}};
486 query.where[idName] = id;
487 query = mergeQuery(query, filter);
488 return this.findOne(query, options, cb);
489 }
490
491 function findOne(filter, options, cb) {
492 if (typeof filter === 'function') {
493 // findOne(cb)
494 cb = filter;
495 filter = {};
496 options = {};
497 } else if (typeof options === 'function' && cb === undefined) {
498 // findOne(filter, cb)
499 cb = options;
500 options = {};
501 }
502 options = options || {};
503 const targetModel = definition.targetModel(this._receiver);
504 const scoped = (this._scope && this._scope.where) || {};
505 filter = mergeQuery({where: scoped}, filter || {});
506 return targetModel.findOne(filter, options, cb);
507 }
508
509 function count(where, options, cb) {
510 if (typeof where === 'function') {
511 // count(cb)
512 cb = where;
513 where = {};
514 } else if (typeof options === 'function' && cb === undefined) {
515 // count(where, cb)
516 cb = options;
517 options = {};
518 }
519 options = options || {};
520
521 const targetModel = definition.targetModel(this._receiver);
522 const scoped = (this._scope && this._scope.where) || {};
523 const filter = mergeQuery({where: scoped}, {where: where || {}});
524 return targetModel.count(filter.where, options, cb);
525 }
526
527 return definition;
528}