UNPKG

12.4 kBJavaScriptView Raw
1"use strict";
2// Copyright IBM Corp. and LoopBack contributors 2019,2020. All Rights Reserved.
3// Node module: @loopback/repository
4// This file is licensed under the MIT License.
5// License text available at https://opensource.org/licenses/MIT
6Object.defineProperty(exports, "__esModule", { value: true });
7exports.isBsonType = exports.deduplicate = exports.reduceAsSingleItem = exports.reduceAsArray = exports.normalizeKey = exports.getKeyValue = exports.buildLookupMap = exports.flattenMapByKeys = exports.flattenTargetsOfOneToManyRelation = exports.flattenTargetsOfOneToOneRelation = exports.includeRelatedModels = exports.findByForeignKeys = void 0;
8const tslib_1 = require("tslib");
9const assert_1 = tslib_1.__importDefault(require("assert"));
10const debug_1 = tslib_1.__importDefault(require("debug"));
11const lodash_1 = tslib_1.__importStar(require("lodash"));
12const __1 = require("..");
13const debug = (0, debug_1.default)('loopback:repository:relation-helpers');
14/**
15 * Finds model instances that contain any of the provided foreign key values.
16 *
17 * @param targetRepository - The target repository where the related model instances are found
18 * @param fkName - Name of the foreign key
19 * @param fkValues - One value or array of values of the foreign key to be included
20 * @param scope - Additional scope constraints
21 * @param options - Options for the operations
22 */
23async function findByForeignKeys(targetRepository, fkName, fkValues, scope, options) {
24 let value;
25 scope = (0, lodash_1.cloneDeep)(scope);
26 if (Array.isArray(fkValues)) {
27 if (fkValues.length === 0)
28 return [];
29 value = fkValues.length === 1 ? fkValues[0] : { inq: fkValues };
30 }
31 else {
32 value = fkValues;
33 }
34 let useScopeFilterGlobally = false;
35 // If its an include from a through model, fkValues will be an array.
36 // However, in this case we DO want to use the scope in the entire query, not
37 // on a per-fk basis
38 if (options) {
39 useScopeFilterGlobally = options.isThroughModelInclude;
40 }
41 // If `scope.limit` is not defined, there is no reason to apply the scope to
42 // each fk. This is to prevent unecessarily high database query counts.
43 // See: https://github.com/loopbackio/loopback-next/issues/8074
44 if (!(scope === null || scope === void 0 ? void 0 : scope.limit)) {
45 useScopeFilterGlobally = true;
46 }
47 // This code is to keep backward compatibility.
48 // See https://github.com/loopbackio/loopback-next/issues/6832 for more info.
49 if (scope === null || scope === void 0 ? void 0 : scope.totalLimit) {
50 scope.limit = scope.totalLimit;
51 useScopeFilterGlobally = true;
52 delete scope.totalLimit;
53 }
54 const isScopeSet = scope && !lodash_1.default.isEmpty(scope);
55 if (isScopeSet && Array.isArray(fkValues) && !useScopeFilterGlobally) {
56 // Since there is a scope, there could be a where filter, a limit, an order
57 // and we should run the scope in multiple queries so we can respect the
58 // scope filter params
59 const findPromises = fkValues.map(fk => {
60 const where = { [fkName]: fk };
61 let localScope = (0, lodash_1.cloneDeep)(scope);
62 // combine where clause to scope filter
63 localScope = new __1.FilterBuilder(localScope).impose({ where }).filter;
64 return targetRepository.find(localScope, options);
65 });
66 return Promise.all(findPromises).then(findResults => {
67 //findResults is an array of arrays for each scope result, so we need to flatten it before returning it
68 return lodash_1.default.flatten(findResults);
69 });
70 }
71 else {
72 const where = { [fkName]: value };
73 if (isScopeSet) {
74 // combine where clause to scope filter
75 scope = new __1.FilterBuilder(scope).impose({ where }).filter;
76 }
77 else {
78 scope = { where };
79 }
80 return targetRepository.find(scope, options);
81 }
82}
83exports.findByForeignKeys = findByForeignKeys;
84/**
85 * Returns model instances that include related models that have a registered
86 * resolver.
87 *
88 * @param targetRepository - The target repository where the model instances are found
89 * @param entities - An array of entity instances or data
90 * @param include -Inclusion filter
91 * @param options - Options for the operations
92 */
93async function includeRelatedModels(targetRepository, entities, include, options) {
94 if (options === null || options === void 0 ? void 0 : options.polymorphicType) {
95 include = include === null || include === void 0 ? void 0 : include.filter(inclusionFilter => {
96 if (typeof inclusionFilter === 'string') {
97 return true;
98 }
99 else {
100 if (inclusionFilter.targetType === undefined ||
101 inclusionFilter.targetType === (options === null || options === void 0 ? void 0 : options.polymorphicType)) {
102 return true;
103 }
104 }
105 });
106 }
107 else {
108 include = (0, lodash_1.cloneDeep)(include);
109 }
110 if (include) {
111 entities = (0, lodash_1.cloneDeep)(entities);
112 }
113 const result = entities;
114 if (!include)
115 return result;
116 const invalidInclusions = include.filter(inclusionFilter => !isInclusionAllowed(targetRepository, inclusionFilter));
117 if (invalidInclusions.length) {
118 const msg = 'Invalid "filter.include" entries: ' +
119 invalidInclusions
120 .map(inclusionFilter => JSON.stringify(inclusionFilter))
121 .join('; ');
122 const err = new Error(msg);
123 Object.assign(err, {
124 code: 'INVALID_INCLUSION_FILTER',
125 statusCode: 400,
126 });
127 throw err;
128 }
129 const resolveTasks = include.map(async (inclusionFilter) => {
130 const relationName = typeof inclusionFilter === 'string'
131 ? inclusionFilter
132 : inclusionFilter.relation;
133 const resolver = targetRepository.inclusionResolvers.get(relationName);
134 const targets = await resolver(entities, inclusionFilter, options);
135 result.forEach((entity, ix) => {
136 const src = entity;
137 src[relationName] = targets[ix];
138 });
139 });
140 await Promise.all(resolveTasks);
141 return result;
142}
143exports.includeRelatedModels = includeRelatedModels;
144/**
145 * Checks if the resolver of the inclusion relation is registered
146 * in the inclusionResolver of the target repository
147 *
148 * @param targetRepository - The target repository where the relations are registered
149 * @param include - Inclusion filter
150 */
151function isInclusionAllowed(targetRepository, include) {
152 const relationName = typeof include === 'string' ? include : include.relation;
153 if (!relationName) {
154 debug('isInclusionAllowed for %j? No: missing relation name', include);
155 return false;
156 }
157 const allowed = targetRepository.inclusionResolvers.has(relationName);
158 debug('isInclusionAllowed for %j (relation %s)? %s', include, allowed);
159 return allowed;
160}
161/**
162 * Returns an array of instances. The order of arrays is based on
163 * the order of sourceIds
164 *
165 * @param sourceIds - One value or array of values of the target key
166 * @param targetEntities - target entities that satisfy targetKey's value (ids).
167 * @param targetKey - name of the target key
168 *
169 */
170function flattenTargetsOfOneToOneRelation(sourceIds, targetEntities, targetKey) {
171 const lookup = buildLookupMap(targetEntities, targetKey, reduceAsSingleItem);
172 return flattenMapByKeys(sourceIds, lookup);
173}
174exports.flattenTargetsOfOneToOneRelation = flattenTargetsOfOneToOneRelation;
175/**
176 * Returns an array of instances. The order of arrays is based on
177 * as a result of one to many relation. The order of arrays is based on
178 * the order of sourceIds
179 *
180 * @param sourceIds - One value or array of values of the target key
181 * @param targetEntities - target entities that satisfy targetKey's value (ids).
182 * @param targetKey - name of the target key
183 *
184 */
185function flattenTargetsOfOneToManyRelation(sourceIds, targetEntities, targetKey) {
186 debug('flattenTargetsOfOneToManyRelation');
187 debug('sourceIds', sourceIds);
188 debug('sourceId types', sourceIds.map(i => typeof i));
189 debug('targetEntities', targetEntities);
190 debug('targetKey', targetKey);
191 const lookup = buildLookupMap(targetEntities, targetKey, reduceAsArray);
192 debug('lookup map', lookup);
193 return flattenMapByKeys(sourceIds, lookup);
194}
195exports.flattenTargetsOfOneToManyRelation = flattenTargetsOfOneToManyRelation;
196/**
197 * Returns an array of instances from the target map. The order of arrays is based on
198 * the order of sourceIds
199 *
200 * @param sourceIds - One value or array of values (of the target key)
201 * @param targetMap - a map that matches sourceIds with instances
202 */
203function flattenMapByKeys(sourceIds, targetMap) {
204 const result = new Array(sourceIds.length);
205 // mongodb: use string as key of targetMap, and convert sourceId to strings
206 // to make sure it gets the related instances.
207 sourceIds.forEach((id, index) => {
208 const key = normalizeKey(id);
209 const target = targetMap.get(key);
210 result[index] = target;
211 });
212 return result;
213}
214exports.flattenMapByKeys = flattenMapByKeys;
215/**
216 * Returns a map which maps key values(ids) to instances. The instances can be
217 * grouped by different strategies.
218 *
219 * @param list - an array of instances
220 * @param keyName - key name of the source
221 * @param reducer - a strategy to reduce inputs to single item or array
222 */
223function buildLookupMap(list, keyName, reducer) {
224 const lookup = new Map();
225 for (const entity of list) {
226 // get a correct key value
227 const key = getKeyValue(entity, keyName);
228 // these 3 steps are to set up the map, the map differs according to the reducer.
229 const original = lookup.get(key);
230 const reduced = reducer(original, entity);
231 lookup.set(key, reduced);
232 }
233 return lookup;
234}
235exports.buildLookupMap = buildLookupMap;
236/**
237 * Returns value of a keyName. Aims to resolve ObjectId problem of Mongo.
238 *
239 * @param model - target model
240 * @param keyName - target key that gets the value from
241 */
242function getKeyValue(model, keyName) {
243 return normalizeKey(model[keyName]);
244}
245exports.getKeyValue = getKeyValue;
246/**
247 * Workaround for MongoDB, where the connector returns ObjectID
248 * values even for properties configured with "type: string".
249 *
250 * @param rawKey
251 */
252function normalizeKey(rawKey) {
253 if (isBsonType(rawKey)) {
254 return rawKey.toString();
255 }
256 return rawKey;
257}
258exports.normalizeKey = normalizeKey;
259/**
260 * Returns an array of instances. For HasMany relation usage.
261 *
262 * @param acc
263 * @param it
264 */
265function reduceAsArray(acc, it) {
266 if (acc)
267 acc.push(it);
268 else
269 acc = [it];
270 return acc;
271}
272exports.reduceAsArray = reduceAsArray;
273/**
274 * Returns a single of an instance. For HasOne and BelongsTo relation usage.
275 *
276 * @param _acc
277 * @param it
278 */
279function reduceAsSingleItem(_acc, it) {
280 return it;
281}
282exports.reduceAsSingleItem = reduceAsSingleItem;
283/**
284 * Dedupe an array
285 * @param input - an array of sourceIds
286 * @returns an array with unique items
287 */
288function deduplicate(input) {
289 const uniqArray = [];
290 if (!input) {
291 return uniqArray;
292 }
293 (0, assert_1.default)(Array.isArray(input), 'array argument is required');
294 const comparableArray = input.map(item => normalizeKey(item));
295 for (let i = 0, n = comparableArray.length; i < n; i++) {
296 if (comparableArray.indexOf(comparableArray[i]) === i) {
297 uniqArray.push(input[i]);
298 }
299 }
300 return uniqArray;
301}
302exports.deduplicate = deduplicate;
303/**
304 * Checks if the value is BsonType (mongodb)
305 * It uses a general way to check the type ,so that it can detect
306 * different versions of bson that might be used in the code base.
307 * Might need to update in the future.
308 *
309 * @param value
310 */
311function isBsonType(value) {
312 if (typeof value !== 'object' || !value)
313 return false;
314 // bson@1.x stores _bsontype on ObjectID instance, bson@4.x on prototype
315 return check(value) || check(value.constructor.prototype);
316 function check(target) {
317 return Object.prototype.hasOwnProperty.call(target, '_bsontype');
318 }
319}
320exports.isBsonType = isBsonType;
321//# sourceMappingURL=relation.helpers.js.map
\No newline at end of file