@openstax/ts-utils 1.39.1 → 1.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/coolFile.d.ts +1 -0
- package/dist/cjs/misc/asyncPool.d.ts +24 -0
- package/dist/cjs/misc/asyncPool.js +58 -0
- package/dist/cjs/misc/hashValue.d.ts +1 -1
- package/dist/cjs/misc/hashValue.js +2 -1
- package/dist/cjs/routing/index.d.ts +18 -1
- package/dist/cjs/routing/index.js +39 -21
- package/dist/cjs/routing/lambdaBatchRequestHandler.d.ts +13 -0
- package/dist/cjs/routing/lambdaBatchRequestHandler.js +71 -0
- package/dist/cjs/services/authProvider/subrequest.js +4 -2
- package/dist/cjs/services/authProvider/utils/userSubrequest.d.ts +1 -1
- package/dist/cjs/tsconfig.without-specs.cjs.tsbuildinfo +1 -1
- package/dist/esm/misc/asyncPool.d.ts +24 -0
- package/dist/esm/misc/asyncPool.js +54 -0
- package/dist/esm/misc/hashValue.d.ts +1 -1
- package/dist/esm/misc/hashValue.js +2 -1
- package/dist/esm/routing/index.d.ts +18 -1
- package/dist/esm/routing/index.js +39 -21
- package/dist/esm/routing/lambdaBatchRequestHandler.d.ts +13 -0
- package/dist/esm/routing/lambdaBatchRequestHandler.js +67 -0
- package/dist/esm/services/authProvider/subrequest.js +4 -2
- package/dist/esm/services/authProvider/utils/userSubrequest.d.ts +1 -1
- package/dist/esm/tsconfig.without-specs.esm.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +1 -1
- package/script/bin/.init-params-script.bash.swp +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
content
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
type PromiseInvoker<T> = () => Promise<T>;
|
|
2
|
+
type UnwrapPromiseInvoker<T> = T extends PromiseInvoker<infer U> ? U : never;
|
|
3
|
+
type UnwrapPromiseInvokers<T extends readonly PromiseInvoker<any>[]> = {
|
|
4
|
+
[K in keyof T]: UnwrapPromiseInvoker<T[K]>;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Executes a tuple of promise invokers with a concurrency limit.
|
|
8
|
+
* Similar to Promise.all but limits the number of in-flight promises.
|
|
9
|
+
*
|
|
10
|
+
* @param invokers Tuple of functions that return promises
|
|
11
|
+
* @param concurrency Maximum number of promises to execute concurrently
|
|
12
|
+
* @returns Promise that resolves with a tuple of results in the same order as the invokers
|
|
13
|
+
* @throws Rejects with the first error encountered (similar to Promise.all behavior)
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const results = await asyncPool([
|
|
17
|
+
* () => fetch('/api/1').then(r => r.json() as Promise<number>),
|
|
18
|
+
* () => fetch('/api/2').then(r => r.text()),
|
|
19
|
+
* () => fetch('/api/3').then(r => r.json() as Promise<boolean>),
|
|
20
|
+
* ], 2);
|
|
21
|
+
* // results has type [number, string, boolean]
|
|
22
|
+
*/
|
|
23
|
+
export declare const asyncPool: <T extends readonly PromiseInvoker<any>[]>(invokers: T, concurrency: number) => Promise<UnwrapPromiseInvokers<T>>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.asyncPool = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Executes a tuple of promise invokers with a concurrency limit.
|
|
6
|
+
* Similar to Promise.all but limits the number of in-flight promises.
|
|
7
|
+
*
|
|
8
|
+
* @param invokers Tuple of functions that return promises
|
|
9
|
+
* @param concurrency Maximum number of promises to execute concurrently
|
|
10
|
+
* @returns Promise that resolves with a tuple of results in the same order as the invokers
|
|
11
|
+
* @throws Rejects with the first error encountered (similar to Promise.all behavior)
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const results = await asyncPool([
|
|
15
|
+
* () => fetch('/api/1').then(r => r.json() as Promise<number>),
|
|
16
|
+
* () => fetch('/api/2').then(r => r.text()),
|
|
17
|
+
* () => fetch('/api/3').then(r => r.json() as Promise<boolean>),
|
|
18
|
+
* ], 2);
|
|
19
|
+
* // results has type [number, string, boolean]
|
|
20
|
+
*/
|
|
21
|
+
const asyncPool = async (invokers, concurrency) => {
|
|
22
|
+
if (concurrency <= 0) {
|
|
23
|
+
throw new Error('Concurrency must be greater than 0');
|
|
24
|
+
}
|
|
25
|
+
const results = new Array(invokers.length);
|
|
26
|
+
let rejected = false;
|
|
27
|
+
let currentIndex = 0;
|
|
28
|
+
let completedCount = 0;
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const executeNext = () => {
|
|
31
|
+
if (completedCount === invokers.length) {
|
|
32
|
+
resolve(results);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
while (currentIndex < invokers.length && (currentIndex - completedCount) < concurrency) {
|
|
36
|
+
const index = currentIndex++;
|
|
37
|
+
const invoker = invokers[index];
|
|
38
|
+
invoker()
|
|
39
|
+
.then((result) => {
|
|
40
|
+
if (rejected) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
results[index] = result;
|
|
44
|
+
completedCount++;
|
|
45
|
+
executeNext();
|
|
46
|
+
})
|
|
47
|
+
.catch((error) => {
|
|
48
|
+
if (!rejected) {
|
|
49
|
+
rejected = true;
|
|
50
|
+
reject(error);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
executeNext();
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
exports.asyncPool = asyncPool;
|
|
@@ -8,10 +8,11 @@ const crypto_1 = require("crypto");
|
|
|
8
8
|
* @example hashValue({someKey: 'someValue'})
|
|
9
9
|
*/
|
|
10
10
|
const hashValue = (value) => {
|
|
11
|
+
var _a;
|
|
11
12
|
// hack for sorting keys https://stackoverflow.com/a/53593328/14809536
|
|
12
13
|
const allKeys = new Set();
|
|
13
14
|
JSON.stringify(value, (k, v) => (allKeys.add(k), v));
|
|
14
|
-
const strValue = JSON.stringify(value, Array.from(allKeys).sort());
|
|
15
|
+
const strValue = (_a = JSON.stringify(value, Array.from(allKeys).sort())) !== null && _a !== void 0 ? _a : 'undefined';
|
|
15
16
|
return (0, crypto_1.createHash)('sha1').update(strValue).digest('hex');
|
|
16
17
|
};
|
|
17
18
|
exports.hashValue = hashValue;
|
|
@@ -187,6 +187,22 @@ type RequestResponder<Sa, Ri, Ro> = {
|
|
|
187
187
|
logger: Logger;
|
|
188
188
|
}) => RoF): (request: Ri) => RoF;
|
|
189
189
|
};
|
|
190
|
+
export type BatchItem<Ro> = {
|
|
191
|
+
id: string;
|
|
192
|
+
makeRequest: () => undefined | Ro;
|
|
193
|
+
};
|
|
194
|
+
export type BatchHandler<Sa, Ri, Ro> = (services: CompatibleServices<Sa>) => ({
|
|
195
|
+
isBatchRequest: (request: Ri) => boolean;
|
|
196
|
+
extractBatchRequests: (request: Ri) => {
|
|
197
|
+
id: string;
|
|
198
|
+
request: Ri;
|
|
199
|
+
}[];
|
|
200
|
+
responseMiddleware?: (app: Sa) => (response: Ro | undefined, request: {
|
|
201
|
+
request: Ri;
|
|
202
|
+
logger: Logger;
|
|
203
|
+
}) => Ro;
|
|
204
|
+
composeBatchResponse: (items: BatchItem<Ro>[]) => Ro;
|
|
205
|
+
});
|
|
190
206
|
/**
|
|
191
207
|
* A factory factory for creating request responders (functions that take a request and return a
|
|
192
208
|
* response -- these functions let us implement Lambda `handler` functions).
|
|
@@ -217,12 +233,13 @@ type RequestResponder<Sa, Ri, Ro> = {
|
|
|
217
233
|
* );
|
|
218
234
|
* ```
|
|
219
235
|
*/
|
|
220
|
-
export declare const makeGetRequestResponder: <Sa, Ru, Ri, Ro>() => ({ routes, pathExtractor, routeMatcher, errorHandler, logExtractor }: {
|
|
236
|
+
export declare const makeGetRequestResponder: <Sa, Ru, Ri, Ro>() => ({ routes, pathExtractor, routeMatcher, errorHandler, logExtractor, batch }: {
|
|
221
237
|
routes: () => AnySpecificRoute<Ru, Sa, Ri, Ro>[];
|
|
222
238
|
pathExtractor: RequestPathExtractor<Ri>;
|
|
223
239
|
logExtractor?: RequestLogExtractor<Ri>;
|
|
224
240
|
routeMatcher?: RequestRouteMatcher<Ri, AnySpecificRoute<Ru, Sa, Ri, Ro>>;
|
|
225
241
|
errorHandler?: (e: Error, logger: Logger) => Ro;
|
|
242
|
+
batch?: BatchHandler<Sa, Ri, Ro>;
|
|
226
243
|
}) => RequestResponder<Sa, Ri, Ro>;
|
|
227
244
|
/** HTTP Headers */
|
|
228
245
|
export type HttpHeaders = {
|
|
@@ -195,11 +195,13 @@ const bindRoute = (services, appBinder, pathExtractor, matcher) => (route) => {
|
|
|
195
195
|
* );
|
|
196
196
|
* ```
|
|
197
197
|
*/
|
|
198
|
-
const makeGetRequestResponder = () => ({ routes, pathExtractor, routeMatcher, errorHandler, logExtractor }) => (services, responseMiddleware) => {
|
|
198
|
+
const makeGetRequestResponder = () => ({ routes, pathExtractor, routeMatcher, errorHandler, logExtractor, batch }) => (services, responseMiddleware) => {
|
|
199
199
|
const appBinderImpl = (app, middleware) => middleware(app, appBinder);
|
|
200
200
|
const appBinder = (0, helpers_1.memoize)(appBinderImpl);
|
|
201
201
|
const boundRoutes = routes().map(bindRoute(services, appBinder, pathExtractor, routeMatcher));
|
|
202
202
|
const boundResponseMiddleware = responseMiddleware ? responseMiddleware(services) : undefined;
|
|
203
|
+
const boundBatch = batch ? batch(services) : undefined;
|
|
204
|
+
const boundBatchResponseMiddleware = (boundBatch === null || boundBatch === void 0 ? void 0 : boundBatch.responseMiddleware) ? boundBatch.responseMiddleware(services) : undefined;
|
|
203
205
|
const appLogger = services.logger || (0, console_1.createConsoleLogger)();
|
|
204
206
|
// *note* this opaque promise guard is less generic than i hoped so
|
|
205
207
|
// i'm leaving it here instead of the guards file.
|
|
@@ -209,42 +211,58 @@ const makeGetRequestResponder = () => ({ routes, pathExtractor, routeMatcher, er
|
|
|
209
211
|
// promise or a non-promise value and the promise figures it out, but those
|
|
210
212
|
// types are getting complicated quickly here.
|
|
211
213
|
const isPromise = (thing) => thing instanceof Promise;
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
if (logExtractor) {
|
|
215
|
-
logger.setContext(logExtractor(request));
|
|
216
|
-
}
|
|
217
|
-
logger.log('begin request');
|
|
214
|
+
const requestResponder = (request, logger) => {
|
|
215
|
+
const boundErrorHandler = errorHandler ? (e) => errorHandler(e, logger) : undefined;
|
|
218
216
|
try {
|
|
219
217
|
const route = (0, helpers_1.mapFind)(boundRoutes, (route) => route(request, logger));
|
|
220
218
|
if (route) {
|
|
221
219
|
logger.log(`route matched ${route.name}`);
|
|
222
|
-
const result =
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const errorHandlerWithMiddleware = (e) => boundResponseMiddleware ?
|
|
226
|
-
boundResponseMiddleware(errorHandler(e, logger), { request, logger }) : errorHandler(e, logger);
|
|
227
|
-
return result.catch(errorHandlerWithMiddleware);
|
|
220
|
+
const result = route.executor();
|
|
221
|
+
if (isPromise(result) && boundErrorHandler) {
|
|
222
|
+
return result.catch(boundErrorHandler);
|
|
228
223
|
}
|
|
229
224
|
else {
|
|
230
225
|
return result;
|
|
231
226
|
}
|
|
232
227
|
}
|
|
233
|
-
else if (boundResponseMiddleware) {
|
|
234
|
-
logger.log('no route matched, returning 404');
|
|
235
|
-
return boundResponseMiddleware(undefined, { request, logger });
|
|
236
|
-
}
|
|
237
228
|
}
|
|
238
229
|
catch (e) {
|
|
239
|
-
if (
|
|
240
|
-
return
|
|
241
|
-
? boundResponseMiddleware(errorHandler(e, logger), { request, logger })
|
|
242
|
-
: errorHandler(e, logger);
|
|
230
|
+
if (boundErrorHandler && e instanceof Error) {
|
|
231
|
+
return boundErrorHandler(e);
|
|
243
232
|
}
|
|
244
233
|
throw e;
|
|
245
234
|
}
|
|
246
235
|
return undefined;
|
|
247
236
|
};
|
|
237
|
+
return (request) => {
|
|
238
|
+
const logger = appLogger.createSubContext();
|
|
239
|
+
if (logExtractor)
|
|
240
|
+
logger.setContext(logExtractor(request));
|
|
241
|
+
logger.log('begin request');
|
|
242
|
+
let response = undefined;
|
|
243
|
+
if (boundBatch === null || boundBatch === void 0 ? void 0 : boundBatch.isBatchRequest(request)) {
|
|
244
|
+
const batchRequests = boundBatch.extractBatchRequests(request);
|
|
245
|
+
const batchResponses = [];
|
|
246
|
+
for (const batchRequest of batchRequests) {
|
|
247
|
+
const subRequestLogger = logger.createSubContext();
|
|
248
|
+
subRequestLogger.setContext({ batchRequestId: batchRequest.id });
|
|
249
|
+
if (logExtractor)
|
|
250
|
+
subRequestLogger.setContext(logExtractor(batchRequest.request));
|
|
251
|
+
batchResponses.push({
|
|
252
|
+
id: batchRequest.id,
|
|
253
|
+
makeRequest: () => {
|
|
254
|
+
const batchItemResponse = requestResponder(batchRequest.request, subRequestLogger);
|
|
255
|
+
return boundBatchResponseMiddleware ? boundBatchResponseMiddleware(batchItemResponse, { request: batchRequest.request, logger: subRequestLogger }) : batchItemResponse;
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
response = boundBatch.composeBatchResponse(batchResponses);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
response = requestResponder(request, logger);
|
|
263
|
+
}
|
|
264
|
+
return boundResponseMiddleware ? boundResponseMiddleware(response, { request, logger }) : response;
|
|
265
|
+
};
|
|
248
266
|
};
|
|
249
267
|
exports.makeGetRequestResponder = makeGetRequestResponder;
|
|
250
268
|
/**
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { APIGatewayProxyEventV2 } from 'aws-lambda';
|
|
2
|
+
import { Logger } from '../services/logger';
|
|
3
|
+
import { ApiResponse, BatchHandler } from '.';
|
|
4
|
+
type Ro = Promise<ApiResponse<number, any>>;
|
|
5
|
+
export declare const lambdaBatchRequestHandler: <Sa, Ri extends APIGatewayProxyEventV2>(config: {
|
|
6
|
+
batchPath: string;
|
|
7
|
+
concurrency: number;
|
|
8
|
+
responseMiddleware?: (app: Sa) => (response: Ro | undefined, request: {
|
|
9
|
+
request: Ri;
|
|
10
|
+
logger: Logger;
|
|
11
|
+
}) => Ro;
|
|
12
|
+
}) => BatchHandler<Sa, Ri, Ro>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.lambdaBatchRequestHandler = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const asyncPool_1 = require("../misc/asyncPool");
|
|
6
|
+
const helpers_1 = require("./helpers");
|
|
7
|
+
const _1 = require(".");
|
|
8
|
+
const BatchItemSchema = zod_1.z.object({
|
|
9
|
+
id: zod_1.z.string(),
|
|
10
|
+
path: zod_1.z.string(),
|
|
11
|
+
method: zod_1.z.string(),
|
|
12
|
+
queryParams: zod_1.z.record(zod_1.z.string(), zod_1.z.string()).optional(),
|
|
13
|
+
body: zod_1.z.string().optional(),
|
|
14
|
+
headers: zod_1.z.record(zod_1.z.string(), zod_1.z.string()).optional(),
|
|
15
|
+
});
|
|
16
|
+
const BatchRequestSchema = zod_1.z.object({
|
|
17
|
+
requests: zod_1.z.array(BatchItemSchema),
|
|
18
|
+
});
|
|
19
|
+
const lambdaBatchRequestHandler = (config) => {
|
|
20
|
+
const { batchPath, concurrency, responseMiddleware } = config;
|
|
21
|
+
return () => ({
|
|
22
|
+
responseMiddleware,
|
|
23
|
+
isBatchRequest: (request) => {
|
|
24
|
+
return request.requestContext.http.path === batchPath;
|
|
25
|
+
},
|
|
26
|
+
extractBatchRequests: (request) => {
|
|
27
|
+
// Parse and validate payload
|
|
28
|
+
const payload = (0, helpers_1.getRequestBody)(request);
|
|
29
|
+
const parsed = BatchRequestSchema.parse(payload);
|
|
30
|
+
// Create modified requests for each batch item
|
|
31
|
+
return parsed.requests.map(item => {
|
|
32
|
+
var _a;
|
|
33
|
+
return ({
|
|
34
|
+
id: item.id,
|
|
35
|
+
request: {
|
|
36
|
+
...request,
|
|
37
|
+
requestContext: {
|
|
38
|
+
...request.requestContext,
|
|
39
|
+
http: {
|
|
40
|
+
...request.requestContext.http,
|
|
41
|
+
path: item.path,
|
|
42
|
+
method: item.method,
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
headers: { ...request.headers, ...item.headers },
|
|
46
|
+
body: (_a = item.body) !== null && _a !== void 0 ? _a : request.body,
|
|
47
|
+
rawQueryString: item.queryParams
|
|
48
|
+
? new URLSearchParams(item.queryParams).toString()
|
|
49
|
+
: request.rawQueryString,
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
composeBatchResponse: async (items) => {
|
|
55
|
+
const results = await (0, asyncPool_1.asyncPool)(items.map(({ id, makeRequest }) => async () => {
|
|
56
|
+
const response = await makeRequest();
|
|
57
|
+
return { id, response };
|
|
58
|
+
}), concurrency);
|
|
59
|
+
const data = {
|
|
60
|
+
results: results.map(({ id, response }) => ({
|
|
61
|
+
id,
|
|
62
|
+
statusCode: (response === null || response === void 0 ? void 0 : response.statusCode) || 500,
|
|
63
|
+
body: (response === null || response === void 0 ? void 0 : response.body) || '',
|
|
64
|
+
headers: (response === null || response === void 0 ? void 0 : response.headers) || {}
|
|
65
|
+
}))
|
|
66
|
+
};
|
|
67
|
+
return (0, _1.apiJsonResponse)(200, data);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
exports.lambdaBatchRequestHandler = lambdaBatchRequestHandler;
|
|
@@ -27,10 +27,12 @@ const subrequestAuthProvider = (initializer) => (configProvider) => {
|
|
|
27
27
|
return undefined;
|
|
28
28
|
}
|
|
29
29
|
const user = await (0, userSubrequest_1.loadUserData)(initializer.fetch, await accountsBase(), resolvedCookieName, token);
|
|
30
|
-
|
|
30
|
+
// this returns `{"error_id":null}` when the token is invalid
|
|
31
|
+
if (user.uuid) {
|
|
31
32
|
logger.setContext({ user: user.uuid });
|
|
33
|
+
return user;
|
|
32
34
|
}
|
|
33
|
-
return
|
|
35
|
+
return undefined;
|
|
34
36
|
};
|
|
35
37
|
const getUser = async () => {
|
|
36
38
|
if (!user) {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { ApiUser } from '..';
|
|
2
2
|
import { GenericFetch } from '../../../fetch';
|
|
3
|
-
export declare const loadUserData: (fetch: GenericFetch, accountsBase: string, cookieName: string, token: string) => Promise<ApiUser
|
|
3
|
+
export declare const loadUserData: (fetch: GenericFetch, accountsBase: string, cookieName: string, token: string) => Promise<ApiUser>;
|