@middy/util 7.2.3 → 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.
Files changed (3) hide show
  1. package/index.d.ts +25 -0
  2. package/index.js +92 -19
  3. package/package.json +2 -5
package/index.d.ts CHANGED
@@ -184,3 +184,28 @@ declare function lambdaContext(
184
184
  ): unknown;
185
185
 
186
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,7 +209,10 @@ export const sanitizeKey = (key) => {
139
209
  };
140
210
 
141
211
  // fetch Cache
142
- const cache = Object.create(null); // key: { value:{fetchKey:Promise}, expiry }
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();
143
216
  const defaultCacheMaxSize = 128;
144
217
 
145
218
  const validateCacheExpiry = (cacheExpiry) => {
@@ -173,8 +246,9 @@ export const processCache = (
173
246
  if (cached.modified) {
174
247
  const value = middlewareFetch(middlewareFetchRequest, cached.value);
175
248
  Object.assign(cached.value, value);
176
- cache[cacheKey] = { value: cached.value, expiry: cached.expiry };
177
- return cache[cacheKey];
249
+ const entry = { value: cached.value, expiry: cached.expiry };
250
+ cache.set(cacheKey, entry);
251
+ return entry;
178
252
  }
179
253
  cached.cache = true;
180
254
  return cached;
@@ -189,16 +263,18 @@ export const processCache = (
189
263
  const expiry = cacheExpiry > 86400000 ? cacheExpiry : now + cacheExpiry;
190
264
  const duration = cacheExpiry > 86400000 ? cacheExpiry - now : cacheExpiry;
191
265
  if (cacheExpiry) {
192
- clearTimeout(cache[cacheKey]?.refresh);
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`).
193
269
  const refresh =
194
270
  duration > 0
195
271
  ? setTimeout(
196
272
  () =>
197
273
  processCache(options, middlewareFetch, middlewareFetchRequest),
198
274
  duration,
199
- )
275
+ ).unref()
200
276
  : undefined;
201
- cache[cacheKey] = { value, expiry, refresh };
277
+ cache.set(cacheKey, { value, expiry, refresh });
202
278
  evictCache(cacheMaxSize);
203
279
  }
204
280
  return { value, expiry };
@@ -212,13 +288,12 @@ export const catchInvalidSignatureException = (e, client, command) => {
212
288
  };
213
289
 
214
290
  export const getCache = (key) => {
215
- if (!cache[key]) return {};
216
- return cache[key];
291
+ return cache.get(key) ?? {};
217
292
  };
218
293
 
219
294
  // Used to remove parts of a cache
220
295
  export const modifyCache = (cacheKey, value) => {
221
- const entry = cache[cacheKey];
296
+ const entry = cache.get(cacheKey);
222
297
  if (!entry) return;
223
298
  clearTimeout(entry.refresh);
224
299
  entry.value = value;
@@ -226,32 +301,30 @@ export const modifyCache = (cacheKey, value) => {
226
301
  };
227
302
 
228
303
  const evictCache = (maxSize) => {
229
- const cacheKeys = Object.keys(cache);
230
- if (cacheKeys.length <= maxSize) return;
304
+ if (cache.size <= maxSize) return;
231
305
  let oldestKey = null;
232
306
  let oldestExpiry = Infinity;
233
- for (const key of cacheKeys) {
234
- const entry = cache[key];
307
+ for (const [key, entry] of cache) {
235
308
  if (entry && entry.expiry < oldestExpiry) {
236
309
  oldestExpiry = entry.expiry;
237
310
  oldestKey = key;
238
311
  }
239
312
  }
240
- if (oldestKey) {
241
- clearTimeout(cache[oldestKey]?.refresh);
242
- cache[oldestKey] = undefined;
313
+ if (oldestKey !== null) {
314
+ clearTimeout(cache.get(oldestKey)?.refresh);
315
+ cache.delete(oldestKey);
243
316
  }
244
317
  };
245
318
 
246
319
  export const clearCache = (inputKeys = null) => {
247
320
  let keys = inputKeys;
248
- keys ??= Object.keys(cache);
321
+ keys ??= [...cache.keys()];
249
322
  if (!Array.isArray(keys)) {
250
323
  keys = [keys];
251
324
  }
252
325
  for (const cacheKey of keys) {
253
- clearTimeout(cache[cacheKey]?.refresh);
254
- cache[cacheKey] = undefined;
326
+ clearTimeout(cache.get(cacheKey)?.refresh);
327
+ cache.delete(cacheKey);
255
328
  }
256
329
  };
257
330
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@middy/util",
3
- "version": "7.2.3",
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.2.3",
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"