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 |
|
11 | import {v4 as uuidv4} from 'uuid';
|
12 | /**
|
13 | * A class constructor accepting arbitrary arguments.
|
14 | */
|
15 | export 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
|
20 | export 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 | */
|
31 | export type ValueOrPromise<T> = T | PromiseLike<T>;
|
32 |
|
33 | export 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 | */
|
41 | export 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 | */
|
54 | export 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 | */
|
95 | export 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 | */
|
157 | export 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 | */
|
193 | export 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 | */
|
213 | export 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 | */
|
256 | export 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 | */
|
291 | export 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 | */
|
308 | export 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 | */
|
317 | export 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;
|