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 entities = (0, lodash_1.cloneDeep)(entities);
95 if (options === null || options === void 0 ? void 0 : options.polymorphicType) {
96 include = include === null || include === void 0 ? void 0 : include.filter(inclusionFilter => {
97 if (typeof inclusionFilter === 'string') {
98 return true;
99 }
100 else {
101 if (inclusionFilter.targetType === undefined ||
102 inclusionFilter.targetType === (options === null || options === void 0 ? void 0 : options.polymorphicType)) {
103 return true;
104 }
105 }
106 });
107 }
108 else {
109 include = (0, lodash_1.cloneDeep)(include);
110 }
111 const result = entities;
112 if (!include)
113 return result;
114 const invalidInclusions = include.filter(inclusionFilter => !isInclusionAllowed(targetRepository, inclusionFilter));
115 if (invalidInclusions.length) {
116 const msg = 'Invalid "filter.include" entries: ' +
117 invalidInclusions
118 .map(inclusionFilter => JSON.stringify(inclusionFilter))
119 .join('; ');
120 const err = new Error(msg);
121 Object.assign(err, {
122 code: 'INVALID_INCLUSION_FILTER',
123 statusCode: 400,
124 });
125 throw err;
126 }
127 const resolveTasks = include.map(async (inclusionFilter) => {
128 const relationName = typeof inclusionFilter === 'string'
129 ? inclusionFilter
130 : inclusionFilter.relation;
131 const resolver = targetRepository.inclusionResolvers.get(relationName);
132 const targets = await resolver(entities, inclusionFilter, options);
133 result.forEach((entity, ix) => {
134 const src = entity;
135 src[relationName] = targets[ix];
136 });
137 });
138 await Promise.all(resolveTasks);
139 return result;
140}
141exports.includeRelatedModels = includeRelatedModels;
142/**
143 * Checks if the resolver of the inclusion relation is registered
144 * in the inclusionResolver of the target repository
145 *
146 * @param targetRepository - The target repository where the relations are registered
147 * @param include - Inclusion filter
148 */
149function isInclusionAllowed(targetRepository, include) {
150 const relationName = typeof include === 'string' ? include : include.relation;
151 if (!relationName) {
152 debug('isInclusionAllowed for %j? No: missing relation name', include);
153 return false;
154 }
155 const allowed = targetRepository.inclusionResolvers.has(relationName);
156 debug('isInclusionAllowed for %j (relation %s)? %s', include, allowed);
157 return allowed;
158}
159/**
160 * Returns an array of instances. The order of arrays is based on
161 * the order of sourceIds
162 *
163 * @param sourceIds - One value or array of values of the target key
164 * @param targetEntities - target entities that satisfy targetKey's value (ids).
165 * @param targetKey - name of the target key
166 *
167 */
168function flattenTargetsOfOneToOneRelation(sourceIds, targetEntities, targetKey) {
169 const lookup = buildLookupMap(targetEntities, targetKey, reduceAsSingleItem);
170 return flattenMapByKeys(sourceIds, lookup);
171}
172exports.flattenTargetsOfOneToOneRelation = flattenTargetsOfOneToOneRelation;
173/**
174 * Returns an array of instances. The order of arrays is based on
175 * as a result of one to many relation. The order of arrays is based on
176 * the order of sourceIds
177 *
178 * @param sourceIds - One value or array of values of the target key
179 * @param targetEntities - target entities that satisfy targetKey's value (ids).
180 * @param targetKey - name of the target key
181 *
182 */
183function flattenTargetsOfOneToManyRelation(sourceIds, targetEntities, targetKey) {
184 debug('flattenTargetsOfOneToManyRelation');
185 debug('sourceIds', sourceIds);
186 debug('sourceId types', sourceIds.map(i => typeof i));
187 debug('targetEntities', targetEntities);
188 debug('targetKey', targetKey);
189 const lookup = buildLookupMap(targetEntities, targetKey, reduceAsArray);
190 debug('lookup map', lookup);
191 return flattenMapByKeys(sourceIds, lookup);
192}
193exports.flattenTargetsOfOneToManyRelation = flattenTargetsOfOneToManyRelation;
194/**
195 * Returns an array of instances from the target map. The order of arrays is based on
196 * the order of sourceIds
197 *
198 * @param sourceIds - One value or array of values (of the target key)
199 * @param targetMap - a map that matches sourceIds with instances
200 */
201function flattenMapByKeys(sourceIds, targetMap) {
202 const result = new Array(sourceIds.length);
203 // mongodb: use string as key of targetMap, and convert sourceId to strings
204 // to make sure it gets the related instances.
205 sourceIds.forEach((id, index) => {
206 const key = normalizeKey(id);
207 const target = targetMap.get(key);
208 result[index] = target;
209 });
210 return result;
211}
212exports.flattenMapByKeys = flattenMapByKeys;
213/**
214 * Returns a map which maps key values(ids) to instances. The instances can be
215 * grouped by different strategies.
216 *
217 * @param list - an array of instances
218 * @param keyName - key name of the source
219 * @param reducer - a strategy to reduce inputs to single item or array
220 */
221function buildLookupMap(list, keyName, reducer) {
222 const lookup = new Map();
223 for (const entity of list) {
224 // get a correct key value
225 const key = getKeyValue(entity, keyName);
226 // these 3 steps are to set up the map, the map differs according to the reducer.
227 const original = lookup.get(key);
228 const reduced = reducer(original, entity);
229 lookup.set(key, reduced);
230 }
231 return lookup;
232}
233exports.buildLookupMap = buildLookupMap;
234/**
235 * Returns value of a keyName. Aims to resolve ObjectId problem of Mongo.
236 *
237 * @param model - target model
238 * @param keyName - target key that gets the value from
239 */
240function getKeyValue(model, keyName) {
241 return normalizeKey(model[keyName]);
242}
243exports.getKeyValue = getKeyValue;
244/**
245 * Workaround for MongoDB, where the connector returns ObjectID
246 * values even for properties configured with "type: string".
247 *
248 * @param rawKey
249 */
250function normalizeKey(rawKey) {
251 if (isBsonType(rawKey)) {
252 return rawKey.toString();
253 }
254 return rawKey;
255}
256exports.normalizeKey = normalizeKey;
257/**
258 * Returns an array of instances. For HasMany relation usage.
259 *
260 * @param acc
261 * @param it
262 */
263function reduceAsArray(acc, it) {
264 if (acc)
265 acc.push(it);
266 else
267 acc = [it];
268 return acc;
269}
270exports.reduceAsArray = reduceAsArray;
271/**
272 * Returns a single of an instance. For HasOne and BelongsTo relation usage.
273 *
274 * @param _acc
275 * @param it
276 */
277function reduceAsSingleItem(_acc, it) {
278 return it;
279}
280exports.reduceAsSingleItem = reduceAsSingleItem;
281/**
282 * Dedupe an array
283 * @param input - an array of sourceIds
284 * @returns an array with unique items
285 */
286function deduplicate(input) {
287 const uniqArray = [];
288 if (!input) {
289 return uniqArray;
290 }
291 (0, assert_1.default)(Array.isArray(input), 'array argument is required');
292 const comparableArray = input.map(item => normalizeKey(item));
293 for (let i = 0, n = comparableArray.length; i < n; i++) {
294 if (comparableArray.indexOf(comparableArray[i]) === i) {
295 uniqArray.push(input[i]);
296 }
297 }
298 return uniqArray;
299}
300exports.deduplicate = deduplicate;
301/**
302 * Checks if the value is BsonType (mongodb)
303 * It uses a general way to check the type ,so that it can detect
304 * different versions of bson that might be used in the code base.
305 * Might need to update in the future.
306 *
307 * @param value
308 */
309function isBsonType(value) {
310 if (typeof value !== 'object' || !value)
311 return false;
312 // bson@1.x stores _bsontype on ObjectID instance, bson@4.x on prototype
313 return check(value) || check(value.constructor.prototype);
314 function check(target) {
315 return Object.prototype.hasOwnProperty.call(target, '_bsontype');
316 }
317}
318exports.isBsonType = isBsonType;
319//# sourceMappingURL=relation.helpers.js.map
\No newline at end of file