@middy/util 7.2.3 → 7.3.1

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 +85 -0
  2. package/index.js +265 -19
  3. package/package.json +2 -5
package/index.d.ts CHANGED
@@ -184,3 +184,88 @@ declare function lambdaContext(
184
184
  ): unknown;
185
185
 
186
186
  declare const httpErrorCodes: Record<number, string>;
187
+
188
+ export type JsonSchemaType =
189
+ | "string"
190
+ | "number"
191
+ | "integer"
192
+ | "boolean"
193
+ | "object"
194
+ | "array";
195
+
196
+ export type StringRule = {
197
+ type: "string";
198
+ pattern?: string;
199
+ minLength?: number;
200
+ maxLength?: number;
201
+ enum?: readonly string[];
202
+ examples?: readonly string[];
203
+ };
204
+
205
+ export type NumberRule = {
206
+ type: "number" | "integer";
207
+ minimum?: number;
208
+ maximum?: number;
209
+ enum?: readonly number[];
210
+ examples?: readonly number[];
211
+ };
212
+
213
+ export type BooleanRule = {
214
+ type: "boolean";
215
+ enum?: readonly boolean[];
216
+ examples?: readonly boolean[];
217
+ };
218
+
219
+ export type ArrayRule = {
220
+ type: "array";
221
+ items?: OptionSchemaRule;
222
+ examples?: readonly unknown[];
223
+ };
224
+
225
+ export type ObjectRule = {
226
+ type: "object";
227
+ required?: readonly string[];
228
+ properties?: { [key: string]: OptionSchemaRule };
229
+ additionalProperties?: boolean | OptionSchemaRule;
230
+ examples?: readonly object[];
231
+ };
232
+
233
+ export type EnumRule = {
234
+ enum: readonly unknown[];
235
+ type?: JsonSchemaType;
236
+ examples?: readonly unknown[];
237
+ };
238
+
239
+ export type ConstRule = {
240
+ const: unknown;
241
+ examples?: readonly unknown[];
242
+ };
243
+
244
+ export type InstanceofRule = {
245
+ instanceof: string;
246
+ examples?: readonly unknown[];
247
+ };
248
+
249
+ export type OneOfRule = {
250
+ oneOf: readonly OptionSchemaRule[];
251
+ examples?: readonly unknown[];
252
+ };
253
+
254
+ export type OptionSchemaRule =
255
+ | StringRule
256
+ | NumberRule
257
+ | BooleanRule
258
+ | ArrayRule
259
+ | ObjectRule
260
+ | EnumRule
261
+ | ConstRule
262
+ | InstanceofRule
263
+ | OneOfRule;
264
+
265
+ export type OptionSchema = ObjectRule;
266
+
267
+ export declare function validateOptions(
268
+ packageName: string,
269
+ schema: OptionSchema,
270
+ options?: Record<string, unknown>,
271
+ ): void;
package/index.js CHANGED
@@ -1,5 +1,248 @@
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' | 'integer' | '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
+ // { type: 'array' | 'array?', items: <itemSchema> }
11
+ // `items` is applied to each array element. It can be a type string,
12
+ // a predicate function, or a plain object treated as a per-element
13
+ // object schema (validated recursively with the same rules).
14
+ // { type: '<type>' | '<type>?', minimum?, maximum?, minLength?, maxLength?, pattern? }
15
+ // Numeric: `minimum`/`maximum` (number/integer).
16
+ // String: `minLength`/`maxLength` (string length), `pattern` (regex source).
17
+ // { type: 'object' | 'object?', properties?: {...}, additionalProperties?: <rule> }
18
+ // `properties` validates known keys with the flat-schema form.
19
+ // `additionalProperties` validates every other key's value against the
20
+ // given rule (string, predicate, or nested object schema). Without it,
21
+ // unknown keys throw.
22
+ // { enum: [...values], type?: '<type>' | '<type>?' }
23
+ // Value must strict-equal one of the listed values. Optional by default;
24
+ // combine with `type` to require a specific type and/or presence.
25
+ // Keys in `options` (or nested objects) that are not in `schema` throw,
26
+ // catching typos.
27
+ const validateOptionsTypeCheckers = {
28
+ string: (v) => typeof v === "string",
29
+ number: (v) => typeof v === "number" && !Number.isNaN(v),
30
+ integer: (v) => Number.isInteger(v),
31
+ boolean: (v) => typeof v === "boolean",
32
+ function: (v) => typeof v === "function",
33
+ object: (v) => v !== null && typeof v === "object" && !Array.isArray(v),
34
+ array: (v) => Array.isArray(v),
35
+ };
36
+
37
+ const isPlainObject = (v) =>
38
+ v !== null && typeof v === "object" && !Array.isArray(v);
39
+
40
+ const checkSchemaObject = (schema, options, path, fail) => {
41
+ if (!isPlainObject(options)) {
42
+ fail(
43
+ path ? `Option '${path}' must be object` : "options must be an object",
44
+ );
45
+ }
46
+ for (const key of Object.keys(options)) {
47
+ if (!Object.hasOwn(schema, key)) {
48
+ fail(`Unknown option '${path ? `${path}.${key}` : key}'`);
49
+ }
50
+ }
51
+ for (const key of Object.keys(schema)) {
52
+ const childPath = path ? `${path}.${key}` : key;
53
+ checkRule(schema[key], options[key], childPath, fail);
54
+ }
55
+ };
56
+
57
+ // Returns true if type check passed (and value is defined), false if the
58
+ // caller should stop validating (value was undefined and optional).
59
+ const checkTypeSpec = (rawType, value, path, fail) => {
60
+ const optional = rawType.endsWith("?");
61
+ const type = optional ? rawType.slice(0, -1) : rawType;
62
+ const checker = validateOptionsTypeCheckers[type];
63
+ if (!checker) fail(`Unknown schema type '${type}' for option '${path}'`);
64
+ if (value === undefined) {
65
+ if (!optional) fail(`Missing required option '${path}' (${type})`);
66
+ return false;
67
+ }
68
+ if (!checker(value)) fail(`Option '${path}' must be ${type}`);
69
+ return true;
70
+ };
71
+
72
+ // Plain object with no rule-marker key (`type`, `enum`, `oneOf`, `const`,
73
+ // `instanceof`) is a flat object schema; anything else is a rule. Used when
74
+ // dispatching `items` and `additionalProperties`.
75
+ const checkNestedRule = (rule, value, path, fail) => {
76
+ if (
77
+ isPlainObject(rule) &&
78
+ typeof rule.type !== "string" &&
79
+ !Array.isArray(rule.enum) &&
80
+ !Array.isArray(rule.oneOf) &&
81
+ !Object.hasOwn(rule, "const") &&
82
+ typeof rule.instanceof !== "string"
83
+ ) {
84
+ checkSchemaObject(rule, value, path, fail);
85
+ } else {
86
+ checkRule(rule, value, path, fail);
87
+ }
88
+ };
89
+
90
+ const childPathOf = (path, key) => (path ? `${path}.${key}` : key);
91
+
92
+ const resolveInstance = (name) => {
93
+ const ctor = globalThis[name];
94
+ if (typeof ctor !== "function") {
95
+ throw new Error(`Unknown 'instanceof' class '${name}'`);
96
+ }
97
+ return ctor;
98
+ };
99
+
100
+ const checkRule = (rule, value, path, fail) => {
101
+ if (typeof rule === "function") {
102
+ if (value !== undefined && !rule(value)) {
103
+ fail(`Invalid option '${path}'`);
104
+ }
105
+ return;
106
+ }
107
+ if (typeof rule === "string") {
108
+ checkTypeSpec(rule, value, path, fail);
109
+ return;
110
+ }
111
+ if (isPlainObject(rule) && Object.hasOwn(rule, "const")) {
112
+ if (value === undefined) return;
113
+ if (value !== rule.const) {
114
+ fail(`Option '${path}' must equal ${JSON.stringify(rule.const)}`);
115
+ }
116
+ return;
117
+ }
118
+ if (isPlainObject(rule) && Array.isArray(rule.oneOf)) {
119
+ if (value === undefined) return;
120
+ let matches = 0;
121
+ for (const sub of rule.oneOf) {
122
+ try {
123
+ checkRule(sub, value, path, (msg) => {
124
+ throw new TypeError(msg);
125
+ });
126
+ matches++;
127
+ } catch {}
128
+ }
129
+ if (matches !== 1) {
130
+ fail(`Option '${path}' must match exactly one schema in oneOf`);
131
+ }
132
+ return;
133
+ }
134
+ if (isPlainObject(rule) && typeof rule.instanceof === "string") {
135
+ if (value === undefined) return;
136
+ const ctor = resolveInstance(rule.instanceof);
137
+ if (!(value instanceof ctor)) {
138
+ fail(`Option '${path}' must be instanceof ${rule.instanceof}`);
139
+ }
140
+ return;
141
+ }
142
+ if (isPlainObject(rule) && Array.isArray(rule.enum)) {
143
+ if (typeof rule.type === "string") {
144
+ if (!checkTypeSpec(rule.type, value, path, fail)) return;
145
+ } else if (value === undefined) {
146
+ return;
147
+ }
148
+ if (!rule.enum.includes(value)) {
149
+ fail(`Option '${path}' must be one of ${JSON.stringify(rule.enum)}`);
150
+ }
151
+ return;
152
+ }
153
+ if (isPlainObject(rule) && typeof rule.type === "string") {
154
+ const {
155
+ type: rawType,
156
+ items,
157
+ properties,
158
+ required,
159
+ additionalProperties,
160
+ minimum,
161
+ maximum,
162
+ pattern,
163
+ minLength,
164
+ maxLength,
165
+ } = rule;
166
+ if (!checkTypeSpec(rawType, value, path, fail)) return;
167
+ const type = rawType.endsWith("?") ? rawType.slice(0, -1) : rawType;
168
+ if (minimum !== undefined && value < minimum) {
169
+ fail(`Option '${path}' must be >= ${minimum}`);
170
+ }
171
+ if (maximum !== undefined && value > maximum) {
172
+ fail(`Option '${path}' must be <= ${maximum}`);
173
+ }
174
+ if (pattern !== undefined && !new RegExp(pattern).test(value)) {
175
+ fail(`Option '${path}' must match pattern ${pattern}`);
176
+ }
177
+ if (minLength !== undefined && value.length < minLength) {
178
+ fail(`Option '${path}' must have length >= ${minLength}`);
179
+ }
180
+ if (maxLength !== undefined && value.length > maxLength) {
181
+ fail(`Option '${path}' must have length <= ${maxLength}`);
182
+ }
183
+ if (type === "array" && items !== undefined) {
184
+ for (let i = 0; i < value.length; i++) {
185
+ checkNestedRule(items, value[i], `${path}[${i}]`, fail);
186
+ }
187
+ }
188
+ if (type === "object" && Array.isArray(required)) {
189
+ for (const key of required) {
190
+ if (value[key] === undefined) {
191
+ fail(`Missing required option '${childPathOf(path, key)}'`);
192
+ }
193
+ }
194
+ }
195
+ if (
196
+ type === "object" &&
197
+ (properties || additionalProperties !== undefined)
198
+ ) {
199
+ for (const key of Object.keys(value)) {
200
+ if (properties && Object.hasOwn(properties, key)) continue;
201
+ if (
202
+ additionalProperties === undefined ||
203
+ additionalProperties === false
204
+ ) {
205
+ fail(`Unknown option '${childPathOf(path, key)}'`);
206
+ }
207
+ if (additionalProperties === true) continue;
208
+ checkNestedRule(
209
+ additionalProperties,
210
+ value[key],
211
+ childPathOf(path, key),
212
+ fail,
213
+ );
214
+ }
215
+ if (properties) {
216
+ for (const key of Object.keys(properties)) {
217
+ if (value[key] === undefined) continue;
218
+ checkRule(properties[key], value[key], childPathOf(path, key), fail);
219
+ }
220
+ }
221
+ }
222
+ return;
223
+ }
224
+ fail(`Invalid schema for option '${path}'`);
225
+ };
226
+
227
+ const isJsonSchemaForm = (schema) =>
228
+ isPlainObject(schema) &&
229
+ schema.type === "object" &&
230
+ (Object.hasOwn(schema, "properties") ||
231
+ Object.hasOwn(schema, "required") ||
232
+ Object.hasOwn(schema, "additionalProperties"));
233
+
234
+ export const validateOptions = (packageName, schema, options = {}) => {
235
+ const fail = (message) => {
236
+ throw new TypeError(message, { cause: { package: packageName } });
237
+ };
238
+ if (isJsonSchemaForm(schema)) {
239
+ checkRule(schema, options, "", fail);
240
+ } else {
241
+ checkSchemaObject(schema, options, "", fail);
242
+ }
243
+ return options;
244
+ };
245
+
3
246
  export const createPrefetchClient = (options) => {
4
247
  const { awsClientOptions } = options;
5
248
  const client = new options.AwsClient(awsClientOptions);
@@ -139,7 +382,10 @@ export const sanitizeKey = (key) => {
139
382
  };
140
383
 
141
384
  // fetch Cache
142
- const cache = Object.create(null); // key: { value:{fetchKey:Promise}, expiry }
385
+ // Map keyed by cacheKey; value shape: { value:{fetchKey:Promise}, expiry, refresh?, modified? }
386
+ // Map chosen over plain object so deletion is O(1), frees the key slot, and
387
+ // avoids the `delete` operator (biome's performance/noDelete rule).
388
+ const cache = new Map();
143
389
  const defaultCacheMaxSize = 128;
144
390
 
145
391
  const validateCacheExpiry = (cacheExpiry) => {
@@ -173,8 +419,9 @@ export const processCache = (
173
419
  if (cached.modified) {
174
420
  const value = middlewareFetch(middlewareFetchRequest, cached.value);
175
421
  Object.assign(cached.value, value);
176
- cache[cacheKey] = { value: cached.value, expiry: cached.expiry };
177
- return cache[cacheKey];
422
+ const entry = { value: cached.value, expiry: cached.expiry };
423
+ cache.set(cacheKey, entry);
424
+ return entry;
178
425
  }
179
426
  cached.cache = true;
180
427
  return cached;
@@ -189,16 +436,18 @@ export const processCache = (
189
436
  const expiry = cacheExpiry > 86400000 ? cacheExpiry : now + cacheExpiry;
190
437
  const duration = cacheExpiry > 86400000 ? cacheExpiry - now : cacheExpiry;
191
438
  if (cacheExpiry) {
192
- clearTimeout(cache[cacheKey]?.refresh);
439
+ clearTimeout(cache.get(cacheKey)?.refresh);
440
+ // .unref() so a pending refresh timer does not keep the Lambda event
441
+ // loop alive (relevant under `callbackWaitsForEmptyEventLoop: false`).
193
442
  const refresh =
194
443
  duration > 0
195
444
  ? setTimeout(
196
445
  () =>
197
446
  processCache(options, middlewareFetch, middlewareFetchRequest),
198
447
  duration,
199
- )
448
+ ).unref()
200
449
  : undefined;
201
- cache[cacheKey] = { value, expiry, refresh };
450
+ cache.set(cacheKey, { value, expiry, refresh });
202
451
  evictCache(cacheMaxSize);
203
452
  }
204
453
  return { value, expiry };
@@ -212,13 +461,12 @@ export const catchInvalidSignatureException = (e, client, command) => {
212
461
  };
213
462
 
214
463
  export const getCache = (key) => {
215
- if (!cache[key]) return {};
216
- return cache[key];
464
+ return cache.get(key) ?? {};
217
465
  };
218
466
 
219
467
  // Used to remove parts of a cache
220
468
  export const modifyCache = (cacheKey, value) => {
221
- const entry = cache[cacheKey];
469
+ const entry = cache.get(cacheKey);
222
470
  if (!entry) return;
223
471
  clearTimeout(entry.refresh);
224
472
  entry.value = value;
@@ -226,32 +474,30 @@ export const modifyCache = (cacheKey, value) => {
226
474
  };
227
475
 
228
476
  const evictCache = (maxSize) => {
229
- const cacheKeys = Object.keys(cache);
230
- if (cacheKeys.length <= maxSize) return;
477
+ if (cache.size <= maxSize) return;
231
478
  let oldestKey = null;
232
479
  let oldestExpiry = Infinity;
233
- for (const key of cacheKeys) {
234
- const entry = cache[key];
480
+ for (const [key, entry] of cache) {
235
481
  if (entry && entry.expiry < oldestExpiry) {
236
482
  oldestExpiry = entry.expiry;
237
483
  oldestKey = key;
238
484
  }
239
485
  }
240
- if (oldestKey) {
241
- clearTimeout(cache[oldestKey]?.refresh);
242
- cache[oldestKey] = undefined;
486
+ if (oldestKey !== null) {
487
+ clearTimeout(cache.get(oldestKey)?.refresh);
488
+ cache.delete(oldestKey);
243
489
  }
244
490
  };
245
491
 
246
492
  export const clearCache = (inputKeys = null) => {
247
493
  let keys = inputKeys;
248
- keys ??= Object.keys(cache);
494
+ keys ??= [...cache.keys()];
249
495
  if (!Array.isArray(keys)) {
250
496
  keys = [keys];
251
497
  }
252
498
  for (const cacheKey of keys) {
253
- clearTimeout(cache[cacheKey]?.refresh);
254
- cache[cacheKey] = undefined;
499
+ clearTimeout(cache.get(cacheKey)?.refresh);
500
+ cache.delete(cacheKey);
255
501
  }
256
502
  };
257
503
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@middy/util",
3
- "version": "7.2.3",
3
+ "version": "7.3.1",
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.1",
64
61
  "@types/aws-lambda": "^8.0.0",
65
62
  "@types/node": "^22.0.0",
66
63
  "aws-xray-sdk": "^3.3.3"