@middy/util 7.3.0 → 7.3.2

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 +80 -13
  2. package/index.js +248 -42
  3. package/package.json +2 -2
package/index.d.ts CHANGED
@@ -185,27 +185,94 @@ declare function lambdaContext(
185
185
 
186
186
  declare const httpErrorCodes: Record<number, string>;
187
187
 
188
- export type OptionSchemaRule =
188
+ export type JsonSchemaType =
189
189
  | "string"
190
190
  | "number"
191
+ | "integer"
191
192
  | "boolean"
192
- | "function"
193
193
  | "object"
194
- | "array"
195
- | "string?"
196
- | "number?"
197
- | "boolean?"
198
- | "function?"
199
- | "object?"
200
- | "array?"
201
- | ((value: unknown) => boolean);
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
+ uniqueItems?: boolean;
223
+ examples?: readonly unknown[];
224
+ };
225
+
226
+ export type ObjectRule = {
227
+ type: "object";
228
+ required?: readonly string[];
229
+ properties?: { [key: string]: OptionSchemaRule };
230
+ additionalProperties?: boolean | OptionSchemaRule;
231
+ examples?: readonly object[];
232
+ };
233
+
234
+ export type EnumRule = {
235
+ enum: readonly unknown[];
236
+ type?: JsonSchemaType;
237
+ examples?: readonly unknown[];
238
+ };
239
+
240
+ export type ConstRule = {
241
+ const: unknown;
242
+ examples?: readonly unknown[];
243
+ };
244
+
245
+ export type InstanceofRule = {
246
+ instanceof: string;
247
+ examples?: readonly unknown[];
248
+ };
249
+
250
+ export type OneOfRule = {
251
+ oneOf: readonly OptionSchemaRule[];
252
+ examples?: readonly unknown[];
253
+ };
254
+
255
+ export type AllOfRule = {
256
+ allOf: readonly OptionSchemaRule[];
257
+ examples?: readonly unknown[];
258
+ };
202
259
 
203
- export type OptionSchema = Record<string, OptionSchemaRule>;
260
+ export type OptionSchemaRule =
261
+ | StringRule
262
+ | NumberRule
263
+ | BooleanRule
264
+ | ArrayRule
265
+ | ObjectRule
266
+ | EnumRule
267
+ | ConstRule
268
+ | InstanceofRule
269
+ | OneOfRule
270
+ | AllOfRule;
271
+
272
+ export type OptionSchema = ObjectRule;
204
273
 
205
274
  export declare function validateOptions(
206
275
  packageName: string,
207
276
  schema: OptionSchema,
208
277
  options?: Record<string, unknown>,
209
278
  ): void;
210
-
211
- export declare const awsClientOptionSchema: OptionSchema;
package/index.js CHANGED
@@ -3,71 +3,277 @@
3
3
 
4
4
  // Option validation helper.
5
5
  // Schema values:
6
- // 'string' | 'number' | 'boolean' | 'function' | 'object' | 'array'
6
+ // 'string' | 'number' | 'integer' | 'boolean' | 'function' | 'object' | 'array'
7
7
  // Trailing '?' marks the field as optional (may be undefined).
8
8
  // (value) => boolean predicate — only called when value is not undefined
9
9
  // (i.e. predicates treat the field as optional by design).
10
- // Keys in `options` that are not in `schema` throw, catching typos.
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.
11
27
  const validateOptionsTypeCheckers = {
12
28
  string: (v) => typeof v === "string",
13
29
  number: (v) => typeof v === "number" && !Number.isNaN(v),
30
+ integer: (v) => Number.isInteger(v),
14
31
  boolean: (v) => typeof v === "boolean",
15
32
  function: (v) => typeof v === "function",
16
33
  object: (v) => v !== null && typeof v === "object" && !Array.isArray(v),
17
34
  array: (v) => Array.isArray(v),
18
35
  };
19
36
 
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
- };
37
+ const isPlainObject = (v) =>
38
+ v !== null && typeof v === "object" && !Array.isArray(v);
35
39
 
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");
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
+ );
46
45
  }
47
46
  for (const key of Object.keys(options)) {
48
47
  if (!Object.hasOwn(schema, key)) {
49
- fail(`Unknown option '${key}'`);
48
+ fail(`Unknown option '${path ? `${path}.${key}` : key}'`);
50
49
  }
51
50
  }
52
51
  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}'`);
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`, `allOf`,
73
+ // `const`, `instanceof`) is a flat object schema; anything else is a rule.
74
+ // Used when 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
+ !Array.isArray(rule.allOf) &&
82
+ !Object.hasOwn(rule, "const") &&
83
+ typeof rule.instanceof !== "string"
84
+ ) {
85
+ checkSchemaObject(rule, value, path, fail);
86
+ } else {
87
+ checkRule(rule, value, path, fail);
88
+ }
89
+ };
90
+
91
+ const childPathOf = (path, key) => (path ? `${path}.${key}` : key);
92
+
93
+ // Stable JSON form: recursively sorts object keys, skips function-typed
94
+ // values. Used for `uniqueItems` so items that differ only by handler
95
+ // identity or key ordering collide.
96
+ const stableStringify = (value) => {
97
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
98
+ if (Array.isArray(value)) {
99
+ return `[${value.map(stableStringify).join(",")}]`;
100
+ }
101
+ const keys = Object.keys(value)
102
+ .filter((k) => typeof value[k] !== "function")
103
+ .sort();
104
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(",")}}`;
105
+ };
106
+
107
+ const resolveInstance = (name) => {
108
+ const ctor = globalThis[name];
109
+ if (typeof ctor !== "function") {
110
+ throw new Error(`Unknown 'instanceof' class '${name}'`);
111
+ }
112
+ return ctor;
113
+ };
114
+
115
+ const checkRule = (rule, value, path, fail) => {
116
+ if (typeof rule === "function") {
117
+ if (value !== undefined && !rule(value)) {
118
+ fail(`Invalid option '${path}'`);
119
+ }
120
+ return;
121
+ }
122
+ if (typeof rule === "string") {
123
+ checkTypeSpec(rule, value, path, fail);
124
+ return;
125
+ }
126
+ if (isPlainObject(rule) && Object.hasOwn(rule, "const")) {
127
+ if (value === undefined) return;
128
+ if (value !== rule.const) {
129
+ fail(`Option '${path}' must equal ${JSON.stringify(rule.const)}`);
130
+ }
131
+ return;
132
+ }
133
+ if (isPlainObject(rule) && Array.isArray(rule.allOf)) {
134
+ if (value === undefined) return;
135
+ for (const sub of rule.allOf) {
136
+ checkRule(sub, value, path, fail);
137
+ }
138
+ return;
139
+ }
140
+ if (isPlainObject(rule) && Array.isArray(rule.oneOf)) {
141
+ if (value === undefined) return;
142
+ let matches = 0;
143
+ for (const sub of rule.oneOf) {
144
+ try {
145
+ checkRule(sub, value, path, (msg) => {
146
+ throw new TypeError(msg);
147
+ });
148
+ matches++;
149
+ } catch {}
150
+ }
151
+ if (matches !== 1) {
152
+ fail(`Option '${path}' must match exactly one schema in oneOf`);
153
+ }
154
+ return;
155
+ }
156
+ if (isPlainObject(rule) && typeof rule.instanceof === "string") {
157
+ if (value === undefined) return;
158
+ const ctor = resolveInstance(rule.instanceof);
159
+ if (!(value instanceof ctor)) {
160
+ fail(`Option '${path}' must be instanceof ${rule.instanceof}`);
161
+ }
162
+ return;
163
+ }
164
+ if (isPlainObject(rule) && Array.isArray(rule.enum)) {
165
+ if (typeof rule.type === "string") {
166
+ if (!checkTypeSpec(rule.type, value, path, fail)) return;
167
+ } else if (value === undefined) {
168
+ return;
169
+ }
170
+ if (!rule.enum.includes(value)) {
171
+ fail(`Option '${path}' must be one of ${JSON.stringify(rule.enum)}`);
172
+ }
173
+ return;
174
+ }
175
+ if (isPlainObject(rule) && typeof rule.type === "string") {
176
+ const {
177
+ type: rawType,
178
+ items,
179
+ uniqueItems,
180
+ properties,
181
+ required,
182
+ additionalProperties,
183
+ minimum,
184
+ maximum,
185
+ pattern,
186
+ minLength,
187
+ maxLength,
188
+ } = rule;
189
+ if (!checkTypeSpec(rawType, value, path, fail)) return;
190
+ const type = rawType.endsWith("?") ? rawType.slice(0, -1) : rawType;
191
+ if (minimum !== undefined && value < minimum) {
192
+ fail(`Option '${path}' must be >= ${minimum}`);
193
+ }
194
+ if (maximum !== undefined && value > maximum) {
195
+ fail(`Option '${path}' must be <= ${maximum}`);
196
+ }
197
+ if (pattern !== undefined && !new RegExp(pattern).test(value)) {
198
+ fail(`Option '${path}' must match pattern ${pattern}`);
199
+ }
200
+ if (minLength !== undefined && value.length < minLength) {
201
+ fail(`Option '${path}' must have length >= ${minLength}`);
202
+ }
203
+ if (maxLength !== undefined && value.length > maxLength) {
204
+ fail(`Option '${path}' must have length <= ${maxLength}`);
205
+ }
206
+ if (type === "array" && items !== undefined) {
207
+ for (let i = 0; i < value.length; i++) {
208
+ checkNestedRule(items, value[i], `${path}[${i}]`, fail);
209
+ }
210
+ }
211
+ if (type === "array" && uniqueItems === true) {
212
+ const seen = new Set();
213
+ for (let i = 0; i < value.length; i++) {
214
+ const key = stableStringify(value[i]);
215
+ if (seen.has(key)) {
216
+ fail(`Duplicate item at '${path}[${i}]'`);
217
+ }
218
+ seen.add(key);
219
+ }
220
+ }
221
+ if (type === "object" && Array.isArray(required)) {
222
+ for (const key of required) {
223
+ if (value[key] === undefined) {
224
+ fail(`Missing required option '${childPathOf(path, key)}'`);
225
+ }
58
226
  }
59
- continue;
60
227
  }
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;
228
+ if (
229
+ type === "object" &&
230
+ (properties || additionalProperties !== undefined)
231
+ ) {
232
+ for (const key of Object.keys(value)) {
233
+ if (properties && Object.hasOwn(properties, key)) continue;
234
+ if (
235
+ additionalProperties === undefined ||
236
+ additionalProperties === false
237
+ ) {
238
+ fail(`Unknown option '${childPathOf(path, key)}'`);
239
+ }
240
+ if (additionalProperties === true) continue;
241
+ checkNestedRule(
242
+ additionalProperties,
243
+ value[key],
244
+ childPathOf(path, key),
245
+ fail,
246
+ );
247
+ }
248
+ if (properties) {
249
+ for (const key of Object.keys(properties)) {
250
+ if (value[key] === undefined) continue;
251
+ checkRule(properties[key], value[key], childPathOf(path, key), fail);
252
+ }
253
+ }
68
254
  }
69
- if (!checker(value)) fail(`Option '${key}' must be ${type}`);
255
+ return;
256
+ }
257
+ fail(`Invalid schema for option '${path}'`);
258
+ };
259
+
260
+ const isJsonSchemaForm = (schema) =>
261
+ isPlainObject(schema) &&
262
+ schema.type === "object" &&
263
+ (Object.hasOwn(schema, "properties") ||
264
+ Object.hasOwn(schema, "required") ||
265
+ Object.hasOwn(schema, "additionalProperties"));
266
+
267
+ export const validateOptions = (packageName, schema, options = {}) => {
268
+ const fail = (message) => {
269
+ throw new TypeError(message, { cause: { package: packageName } });
270
+ };
271
+ if (isJsonSchemaForm(schema)) {
272
+ checkRule(schema, options, "", fail);
273
+ } else {
274
+ checkSchemaObject(schema, options, "", fail);
70
275
  }
276
+ return options;
71
277
  };
72
278
 
73
279
  export const createPrefetchClient = (options) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@middy/util",
3
- "version": "7.3.0",
3
+ "version": "7.3.2",
4
4
  "description": "🛵 The stylish Node.js middleware engine for AWS Lambda (util package)",
5
5
  "type": "module",
6
6
  "engines": {
@@ -57,7 +57,7 @@
57
57
  },
58
58
  "devDependencies": {
59
59
  "@aws-sdk/client-ssm": "^3.0.0",
60
- "@middy/core": "7.3.0",
60
+ "@middy/core": "7.3.2",
61
61
  "@types/aws-lambda": "^8.0.0",
62
62
  "@types/node": "^22.0.0",
63
63
  "aws-xray-sdk": "^3.3.3"