UNPKG

9.8 kBPlain TextView Raw
1// Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved.
2// Node module: @loopback/context
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6/**
7 * This module contains types for values and/or promises as well as a set of
8 * utility methods to handle values and/or promises.
9 */
10
11import {v4 as uuidv4} from 'uuid';
12/**
13 * A class constructor accepting arbitrary arguments.
14 */
15export type Constructor<T> =
16 // eslint-disable-next-line @typescript-eslint/no-explicit-any
17 new (...args: any[]) => T;
18
19// eslint-disable-next-line @typescript-eslint/no-explicit-any
20export type BoundValue = any;
21
22/**
23 * Representing a value or promise. This type is used to represent results of
24 * synchronous/asynchronous resolution of values.
25 *
26 * Note that we are using PromiseLike instead of native Promise to describe
27 * the asynchronous variant. This allows producers of async values to use
28 * any Promise implementation (e.g. Bluebird) instead of native Promises
29 * provided by JavaScript runtime.
30 */
31export type ValueOrPromise<T> = T | PromiseLike<T>;
32
33export type MapObject<T> = Record<string, T>;
34
35/**
36 * Check whether a value is a Promise-like instance.
37 * Recognizes both native promises and third-party promise libraries.
38 *
39 * @param value - The value to check.
40 */
41export function isPromiseLike<T>(
42 value: T | PromiseLike<T> | undefined,
43): value is PromiseLike<T> {
44 if (!value) return false;
45 if (typeof value !== 'object' && typeof value !== 'function') return false;
46 return typeof (value as PromiseLike<T>).then === 'function';
47}
48
49/**
50 * Get nested properties of an object by path
51 * @param value - Value of the source object
52 * @param path - Path to the property
53 */
54export function getDeepProperty<OUT = BoundValue, IN = BoundValue>(
55 value: IN,
56 path: string,
57): OUT | undefined {
58 let result: BoundValue = value;
59 const props = path.split('.').filter(Boolean);
60 for (const p of props) {
61 if (result == null) {
62 return undefined;
63 }
64 result = result[p];
65 }
66 return <OUT>result;
67}
68
69/**
70 * Resolve entries of an object into a new object with the same keys. If one or
71 * more entries of the source object are resolved to a promise by the `resolver`
72 * function, this method returns a promise which will be resolved to the new
73 * object with fully resolved entries.
74 *
75 * @example
76 *
77 * - Example 1: resolve all entries synchronously
78 * ```ts
79 * const result = resolveMap({a: 'x', b: 'y'}, v => v.toUpperCase());
80 * ```
81 * The `result` will be `{a: 'X', b: 'Y'}`.
82 *
83 * - Example 2: resolve one or more entries asynchronously
84 * ```ts
85 * const result = resolveMap({a: 'x', b: 'y'}, v =>
86 * Promise.resolve(v.toUpperCase()),
87 * );
88 * ```
89 * The `result` will be a promise of `{a: 'X', b: 'Y'}`.
90 *
91 * @param map - The original object containing the source entries
92 * @param resolver - A function resolves an entry to a value or promise. It will
93 * be invoked with the property value, the property name, and the source object.
94 */
95export function resolveMap<T, V>(
96 map: MapObject<T>,
97 resolver: (val: T, key: string, values: MapObject<T>) => ValueOrPromise<V>,
98): ValueOrPromise<MapObject<V>> {
99 const result: MapObject<V> = {};
100 let asyncResolvers: PromiseLike<void>[] | undefined = undefined;
101
102 const setter = (key: string) => (val: V) => {
103 if (val !== undefined) {
104 // Only set the value if it's not undefined so that the default value
105 // for a key will be honored
106 result[key] = val;
107 }
108 };
109
110 for (const key in map) {
111 const valueOrPromise = resolver(map[key], key, map);
112 if (isPromiseLike(valueOrPromise)) {
113 if (!asyncResolvers) asyncResolvers = [];
114 asyncResolvers.push(valueOrPromise.then(setter(key)));
115 } else {
116 if (valueOrPromise !== undefined) {
117 // Only set the value if it's not undefined so that the default value
118 // for a key will be honored
119 result[key] = valueOrPromise;
120 }
121 }
122 }
123
124 if (asyncResolvers) {
125 return Promise.all(asyncResolvers).then(() => result);
126 } else {
127 return result;
128 }
129}
130
131/**
132 * Resolve entries of an array into a new array with the same indexes. If one or
133 * more entries of the source array are resolved to a promise by the `resolver`
134 * function, this method returns a promise which will be resolved to the new
135 * array with fully resolved entries.
136 *
137 * @example
138 *
139 * - Example 1: resolve all entries synchronously
140 * ```ts
141 * const result = resolveList(['a', 'b'], v => v.toUpperCase());
142 * ```
143 * The `result` will be `['A', 'B']`.
144 *
145 * - Example 2: resolve one or more entries asynchronously
146 * ```ts
147 * const result = resolveList(['a', 'b'], v =>
148 * Promise.resolve(v.toUpperCase()),
149 * );
150 * ```
151 * The `result` will be a promise of `['A', 'B']`.
152 *
153 * @param list - The original array containing the source entries
154 * @param resolver - A function resolves an entry to a value or promise. It will
155 * be invoked with the property value, the property index, and the source array.
156 */
157export function resolveList<T, V>(
158 list: T[],
159 resolver: (val: T, index: number, values: T[]) => ValueOrPromise<V>,
160): ValueOrPromise<V[]> {
161 const result: V[] = new Array<V>(list.length);
162 let asyncResolvers: PromiseLike<void>[] | undefined = undefined;
163
164 const setter = (index: number) => (val: V) => {
165 result[index] = val;
166 };
167
168 for (let ix = 0; ix < list.length; ix++) {
169 const valueOrPromise = resolver(list[ix], ix, list);
170 if (isPromiseLike(valueOrPromise)) {
171 if (!asyncResolvers) asyncResolvers = [];
172 asyncResolvers.push(valueOrPromise.then(setter(ix)));
173 } else {
174 result[ix] = valueOrPromise;
175 }
176 }
177
178 if (asyncResolvers) {
179 return Promise.all(asyncResolvers).then(() => result);
180 } else {
181 return result;
182 }
183}
184
185/**
186 * Try to run an action that returns a promise or a value
187 * @param action - A function that returns a promise or a value
188 * @param finalAction - A function to be called once the action
189 * is fulfilled or rejected (synchronously or asynchronously)
190 *
191 * @typeParam T - Type for the return value
192 */
193export function tryWithFinally<T>(
194 action: () => ValueOrPromise<T>,
195 finalAction: () => void,
196): ValueOrPromise<T> {
197 return tryCatchFinally(action, undefined, finalAction);
198}
199
200/**
201 * Try to run an action that returns a promise or a value with error and final
202 * actions to mimic `try {} catch(err) {} finally {}` for a value or promise.
203 *
204 * @param action - A function that returns a promise or a value
205 * @param errorAction - A function to be called once the action
206 * is rejected (synchronously or asynchronously). It must either return a new
207 * value or throw an error.
208 * @param finalAction - A function to be called once the action
209 * is fulfilled or rejected (synchronously or asynchronously)
210 *
211 * @typeParam T - Type for the return value
212 */
213export function tryCatchFinally<T>(
214 action: () => ValueOrPromise<T>,
215 errorAction: (err: unknown) => T | never = err => {
216 throw err;
217 },
218 finalAction: () => void = () => {},
219): ValueOrPromise<T> {
220 let result: ValueOrPromise<T>;
221 try {
222 result = action();
223 } catch (err) {
224 result = reject(err);
225 }
226 if (isPromiseLike(result)) {
227 return result.then(resolve, reject);
228 }
229
230 return resolve(result);
231
232 function resolve(value: T) {
233 try {
234 return value;
235 } finally {
236 finalAction();
237 }
238 }
239
240 function reject(err: unknown): T | never {
241 try {
242 return errorAction(err);
243 } finally {
244 finalAction();
245 }
246 }
247}
248
249/**
250 * Resolve an iterator of source values into a result until the evaluator
251 * returns `true`
252 * @param source - The iterator of source values
253 * @param resolver - The resolve function that maps the source value to a result
254 * @param evaluator - The evaluate function that decides when to stop
255 */
256export function resolveUntil<T, V>(
257 source: Iterator<T>,
258 resolver: (sourceVal: T) => ValueOrPromise<V | undefined>,
259 evaluator: (sourceVal: T, targetVal: V | undefined) => boolean,
260): ValueOrPromise<V | undefined> {
261 // Do iteration in loop for synchronous values to avoid stack overflow
262 // eslint-disable-next-line no-constant-condition
263 while (true) {
264 const next = source.next();
265 if (next.done) return undefined; // End of the iterator
266 const sourceVal = next.value;
267 const valueOrPromise = resolver(sourceVal);
268 if (isPromiseLike(valueOrPromise)) {
269 return valueOrPromise.then(v => {
270 if (evaluator(sourceVal, v)) {
271 return v;
272 } else {
273 return resolveUntil(source, resolver, evaluator);
274 }
275 });
276 } else {
277 if (evaluator(sourceVal, valueOrPromise)) {
278 return valueOrPromise;
279 }
280 // Continue with the while loop
281 }
282 }
283}
284
285/**
286 * Transform a value or promise with a function that produces a new value or
287 * promise
288 * @param valueOrPromise - The value or promise
289 * @param transformer - A function that maps the source value to a value or promise
290 */
291export function transformValueOrPromise<T, V>(
292 valueOrPromise: ValueOrPromise<T>,
293 transformer: (val: T) => ValueOrPromise<V>,
294): ValueOrPromise<V> {
295 if (isPromiseLike(valueOrPromise)) {
296 return valueOrPromise.then(transformer);
297 } else {
298 return transformer(valueOrPromise);
299 }
300}
301
302/**
303 * A utility to generate uuid v4
304 *
305 * @deprecated Use `generateUniqueId`, [uuid](https://www.npmjs.com/package/uuid)
306 * or [hyperid](https://www.npmjs.com/package/hyperid) instead.
307 */
308export function uuid() {
309 return uuidv4();
310}
311
312/**
313 * A regular expression for testing uuid v4 PATTERN
314 * @deprecated This pattern is an internal helper used by unit-tests, we are no
315 * longer using it.
316 */
317export const UUID_PATTERN =
318 /[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}/i;