@middy/util 7.2.2 → 7.3.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/index.d.ts +28 -0
- package/index.js +138 -17
- package/package.json +2 -5
package/index.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface Options<Client, ClientOptions> {
|
|
|
21
21
|
cacheKey?: string;
|
|
22
22
|
cacheExpiry?: number;
|
|
23
23
|
cacheKeyExpiry?: Record<string, number>;
|
|
24
|
+
cacheMaxSize?: number;
|
|
24
25
|
setToContext?: boolean;
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -157,6 +158,8 @@ declare function jsonSafeStringify(
|
|
|
157
158
|
space?: string | number,
|
|
158
159
|
): string | unknown;
|
|
159
160
|
|
|
161
|
+
declare const jsonContentTypePattern: RegExp;
|
|
162
|
+
|
|
160
163
|
declare function decodeBody(event: {
|
|
161
164
|
body?: string | null;
|
|
162
165
|
isBase64Encoded?: boolean;
|
|
@@ -181,3 +184,28 @@ declare function lambdaContext(
|
|
|
181
184
|
): unknown;
|
|
182
185
|
|
|
183
186
|
declare const httpErrorCodes: Record<number, string>;
|
|
187
|
+
|
|
188
|
+
export type OptionSchemaRule =
|
|
189
|
+
| "string"
|
|
190
|
+
| "number"
|
|
191
|
+
| "boolean"
|
|
192
|
+
| "function"
|
|
193
|
+
| "object"
|
|
194
|
+
| "array"
|
|
195
|
+
| "string?"
|
|
196
|
+
| "number?"
|
|
197
|
+
| "boolean?"
|
|
198
|
+
| "function?"
|
|
199
|
+
| "object?"
|
|
200
|
+
| "array?"
|
|
201
|
+
| ((value: unknown) => boolean);
|
|
202
|
+
|
|
203
|
+
export type OptionSchema = Record<string, OptionSchemaRule>;
|
|
204
|
+
|
|
205
|
+
export declare function validateOptions(
|
|
206
|
+
packageName: string,
|
|
207
|
+
schema: OptionSchema,
|
|
208
|
+
options?: Record<string, unknown>,
|
|
209
|
+
): void;
|
|
210
|
+
|
|
211
|
+
export declare const awsClientOptionSchema: OptionSchema;
|
package/index.js
CHANGED
|
@@ -1,5 +1,75 @@
|
|
|
1
1
|
// Copyright 2017 - 2026 will Farrell, Luciano Mammino, and Middy contributors.
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
// Option validation helper.
|
|
5
|
+
// Schema values:
|
|
6
|
+
// 'string' | 'number' | 'boolean' | 'function' | 'object' | 'array'
|
|
7
|
+
// Trailing '?' marks the field as optional (may be undefined).
|
|
8
|
+
// (value) => boolean predicate — only called when value is not undefined
|
|
9
|
+
// (i.e. predicates treat the field as optional by design).
|
|
10
|
+
// Keys in `options` that are not in `schema` throw, catching typos.
|
|
11
|
+
const validateOptionsTypeCheckers = {
|
|
12
|
+
string: (v) => typeof v === "string",
|
|
13
|
+
number: (v) => typeof v === "number" && !Number.isNaN(v),
|
|
14
|
+
boolean: (v) => typeof v === "boolean",
|
|
15
|
+
function: (v) => typeof v === "function",
|
|
16
|
+
object: (v) => v !== null && typeof v === "object" && !Array.isArray(v),
|
|
17
|
+
array: (v) => Array.isArray(v),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Shared schema for middlewares that wrap an AWS SDK client. Spread into
|
|
21
|
+
// package-specific schemas: `{ ...awsClientOptionSchema, extraField: 'type?' }`.
|
|
22
|
+
export const awsClientOptionSchema = {
|
|
23
|
+
AwsClient: "function?",
|
|
24
|
+
awsClientOptions: "object?",
|
|
25
|
+
awsClientAssumeRole: "string?",
|
|
26
|
+
awsClientCapture: "function?",
|
|
27
|
+
fetchData: "object?",
|
|
28
|
+
disablePrefetch: "boolean?",
|
|
29
|
+
cacheKey: "string?",
|
|
30
|
+
cacheKeyExpiry: "object?",
|
|
31
|
+
cacheExpiry: (v) => typeof v === "number" && v >= -1,
|
|
32
|
+
cacheMaxSize: (v) => Number.isInteger(v) && v >= 1,
|
|
33
|
+
setToContext: "boolean?",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const validateOptions = (packageName, schema, options = {}) => {
|
|
37
|
+
const fail = (message) => {
|
|
38
|
+
throw new TypeError(message, { cause: { package: packageName } });
|
|
39
|
+
};
|
|
40
|
+
if (
|
|
41
|
+
options === null ||
|
|
42
|
+
typeof options !== "object" ||
|
|
43
|
+
Array.isArray(options)
|
|
44
|
+
) {
|
|
45
|
+
fail("options must be an object");
|
|
46
|
+
}
|
|
47
|
+
for (const key of Object.keys(options)) {
|
|
48
|
+
if (!Object.hasOwn(schema, key)) {
|
|
49
|
+
fail(`Unknown option '${key}'`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (const key of Object.keys(schema)) {
|
|
53
|
+
const rule = schema[key];
|
|
54
|
+
const value = options[key];
|
|
55
|
+
if (typeof rule === "function") {
|
|
56
|
+
if (value !== undefined && !rule(value)) {
|
|
57
|
+
fail(`Invalid option '${key}'`);
|
|
58
|
+
}
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const optional = rule.endsWith("?");
|
|
62
|
+
const type = optional ? rule.slice(0, -1) : rule;
|
|
63
|
+
const checker = validateOptionsTypeCheckers[type];
|
|
64
|
+
if (!checker) fail(`Unknown schema type '${type}' for option '${key}'`);
|
|
65
|
+
if (value === undefined) {
|
|
66
|
+
if (!optional) fail(`Missing required option '${key}' (${type})`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!checker(value)) fail(`Option '${key}' must be ${type}`);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
3
73
|
export const createPrefetchClient = (options) => {
|
|
4
74
|
const { awsClientOptions } = options;
|
|
5
75
|
const client = new options.AwsClient(awsClientOptions);
|
|
@@ -139,14 +209,34 @@ export const sanitizeKey = (key) => {
|
|
|
139
209
|
};
|
|
140
210
|
|
|
141
211
|
// fetch Cache
|
|
142
|
-
|
|
212
|
+
// Map keyed by cacheKey; value shape: { value:{fetchKey:Promise}, expiry, refresh?, modified? }
|
|
213
|
+
// Map chosen over plain object so deletion is O(1), frees the key slot, and
|
|
214
|
+
// avoids the `delete` operator (biome's performance/noDelete rule).
|
|
215
|
+
const cache = new Map();
|
|
216
|
+
const defaultCacheMaxSize = 128;
|
|
217
|
+
|
|
218
|
+
const validateCacheExpiry = (cacheExpiry) => {
|
|
219
|
+
if (
|
|
220
|
+
typeof cacheExpiry === "number" &&
|
|
221
|
+
cacheExpiry < -1 &&
|
|
222
|
+
!Number.isNaN(cacheExpiry)
|
|
223
|
+
) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`Invalid cacheExpiry value: ${cacheExpiry}. Must be -1 (infinite), 0 (disabled), or a positive number (ms duration or unix timestamp)`,
|
|
226
|
+
{ cause: { package: "@middy/util" } },
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
143
231
|
export const processCache = (
|
|
144
232
|
options,
|
|
145
233
|
middlewareFetch = () => undefined,
|
|
146
234
|
middlewareFetchRequest = {},
|
|
147
235
|
) => {
|
|
148
|
-
let { cacheKey, cacheKeyExpiry, cacheExpiry } = options;
|
|
236
|
+
let { cacheKey, cacheKeyExpiry, cacheExpiry, cacheMaxSize } = options;
|
|
237
|
+
cacheMaxSize ??= defaultCacheMaxSize;
|
|
149
238
|
cacheExpiry = cacheKeyExpiry?.[cacheKey] ?? cacheExpiry;
|
|
239
|
+
validateCacheExpiry(cacheExpiry);
|
|
150
240
|
const now = Date.now();
|
|
151
241
|
if (cacheExpiry) {
|
|
152
242
|
const cached = getCache(cacheKey);
|
|
@@ -156,27 +246,36 @@ export const processCache = (
|
|
|
156
246
|
if (cached.modified) {
|
|
157
247
|
const value = middlewareFetch(middlewareFetchRequest, cached.value);
|
|
158
248
|
Object.assign(cached.value, value);
|
|
159
|
-
|
|
160
|
-
|
|
249
|
+
const entry = { value: cached.value, expiry: cached.expiry };
|
|
250
|
+
cache.set(cacheKey, entry);
|
|
251
|
+
return entry;
|
|
161
252
|
}
|
|
162
253
|
cached.cache = true;
|
|
163
254
|
return cached;
|
|
164
255
|
}
|
|
165
256
|
}
|
|
166
257
|
const value = middlewareFetch(middlewareFetchRequest);
|
|
167
|
-
//
|
|
258
|
+
// cacheExpiry semantics:
|
|
259
|
+
// >86400000 (24h): treated as unix timestamp (ms)
|
|
260
|
+
// >0 && <=86400000: treated as duration (ms) from now
|
|
261
|
+
// -1: infinite cache (never expires)
|
|
262
|
+
// 0/undefined/null: no caching
|
|
168
263
|
const expiry = cacheExpiry > 86400000 ? cacheExpiry : now + cacheExpiry;
|
|
169
264
|
const duration = cacheExpiry > 86400000 ? cacheExpiry - now : cacheExpiry;
|
|
170
265
|
if (cacheExpiry) {
|
|
266
|
+
clearTimeout(cache.get(cacheKey)?.refresh);
|
|
267
|
+
// .unref() so a pending refresh timer does not keep the Lambda event
|
|
268
|
+
// loop alive (relevant under `callbackWaitsForEmptyEventLoop: false`).
|
|
171
269
|
const refresh =
|
|
172
270
|
duration > 0
|
|
173
271
|
? setTimeout(
|
|
174
272
|
() =>
|
|
175
273
|
processCache(options, middlewareFetch, middlewareFetchRequest),
|
|
176
274
|
duration,
|
|
177
|
-
)
|
|
275
|
+
).unref()
|
|
178
276
|
: undefined;
|
|
179
|
-
cache
|
|
277
|
+
cache.set(cacheKey, { value, expiry, refresh });
|
|
278
|
+
evictCache(cacheMaxSize);
|
|
180
279
|
}
|
|
181
280
|
return { value, expiry };
|
|
182
281
|
};
|
|
@@ -189,27 +288,43 @@ export const catchInvalidSignatureException = (e, client, command) => {
|
|
|
189
288
|
};
|
|
190
289
|
|
|
191
290
|
export const getCache = (key) => {
|
|
192
|
-
|
|
193
|
-
return cache[key];
|
|
291
|
+
return cache.get(key) ?? {};
|
|
194
292
|
};
|
|
195
293
|
|
|
196
294
|
// Used to remove parts of a cache
|
|
197
295
|
export const modifyCache = (cacheKey, value) => {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
296
|
+
const entry = cache.get(cacheKey);
|
|
297
|
+
if (!entry) return;
|
|
298
|
+
clearTimeout(entry.refresh);
|
|
299
|
+
entry.value = value;
|
|
300
|
+
entry.modified = true;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const evictCache = (maxSize) => {
|
|
304
|
+
if (cache.size <= maxSize) return;
|
|
305
|
+
let oldestKey = null;
|
|
306
|
+
let oldestExpiry = Infinity;
|
|
307
|
+
for (const [key, entry] of cache) {
|
|
308
|
+
if (entry && entry.expiry < oldestExpiry) {
|
|
309
|
+
oldestExpiry = entry.expiry;
|
|
310
|
+
oldestKey = key;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (oldestKey !== null) {
|
|
314
|
+
clearTimeout(cache.get(oldestKey)?.refresh);
|
|
315
|
+
cache.delete(oldestKey);
|
|
316
|
+
}
|
|
202
317
|
};
|
|
203
318
|
|
|
204
319
|
export const clearCache = (inputKeys = null) => {
|
|
205
320
|
let keys = inputKeys;
|
|
206
|
-
keys ??=
|
|
321
|
+
keys ??= [...cache.keys()];
|
|
207
322
|
if (!Array.isArray(keys)) {
|
|
208
323
|
keys = [keys];
|
|
209
324
|
}
|
|
210
325
|
for (const cacheKey of keys) {
|
|
211
|
-
clearTimeout(cache
|
|
212
|
-
cache
|
|
326
|
+
clearTimeout(cache.get(cacheKey)?.refresh);
|
|
327
|
+
cache.delete(cacheKey);
|
|
213
328
|
}
|
|
214
329
|
};
|
|
215
330
|
|
|
@@ -269,6 +384,9 @@ export const jsonSafeStringify = (value, replacer, space) => {
|
|
|
269
384
|
}
|
|
270
385
|
};
|
|
271
386
|
|
|
387
|
+
export const jsonContentTypePattern =
|
|
388
|
+
/^application\/([a-z0-9.+-]+\+)?json(;|$)/i;
|
|
389
|
+
|
|
272
390
|
export const decodeBody = (event) => {
|
|
273
391
|
const { body, isBase64Encoded } = event;
|
|
274
392
|
if (typeof body === "undefined" || body === null) return body;
|
|
@@ -304,7 +422,10 @@ export class HttpError extends Error {
|
|
|
304
422
|
message ??= httpErrorCodes[code];
|
|
305
423
|
super(message, options);
|
|
306
424
|
|
|
307
|
-
const name = httpErrorCodes[code]
|
|
425
|
+
const name = (httpErrorCodes[code] ?? "Unknown").replace(
|
|
426
|
+
createErrorRegexp,
|
|
427
|
+
"",
|
|
428
|
+
);
|
|
308
429
|
this.name = !name.endsWith("Error") ? `${name}Error` : name;
|
|
309
430
|
|
|
310
431
|
this.status = this.statusCode = code; // setting `status` for backwards compatibility w/ `http-errors`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@middy/util",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.3.0",
|
|
4
4
|
"description": "🛵 The stylish Node.js middleware engine for AWS Lambda (util package)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -17,9 +17,6 @@
|
|
|
17
17
|
"import": {
|
|
18
18
|
"types": "./index.d.ts",
|
|
19
19
|
"default": "./index.js"
|
|
20
|
-
},
|
|
21
|
-
"require": {
|
|
22
|
-
"default": "./index.js"
|
|
23
20
|
}
|
|
24
21
|
}
|
|
25
22
|
},
|
|
@@ -60,7 +57,7 @@
|
|
|
60
57
|
},
|
|
61
58
|
"devDependencies": {
|
|
62
59
|
"@aws-sdk/client-ssm": "^3.0.0",
|
|
63
|
-
"@middy/core": "7.
|
|
60
|
+
"@middy/core": "7.3.0",
|
|
64
61
|
"@types/aws-lambda": "^8.0.0",
|
|
65
62
|
"@types/node": "^22.0.0",
|
|
66
63
|
"aws-xray-sdk": "^3.3.3"
|