@middy/util 7.3.0 → 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 +73 -13
  2. package/index.js +215 -42
  3. package/package.json +2 -2
package/index.d.ts CHANGED
@@ -185,27 +185,87 @@ 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
+ 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
+ };
202
253
 
203
- export type OptionSchema = Record<string, OptionSchemaRule>;
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;
204
266
 
205
267
  export declare function validateOptions(
206
268
  packageName: string,
207
269
  schema: OptionSchema,
208
270
  options?: Record<string, unknown>,
209
271
  ): void;
210
-
211
- export declare const awsClientOptionSchema: OptionSchema;
package/index.js CHANGED
@@ -3,71 +3,244 @@
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`, `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);
58
186
  }
59
- continue;
60
187
  }
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;
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
+ }
68
194
  }
69
- if (!checker(value)) fail(`Option '${key}' must be ${type}`);
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);
70
242
  }
243
+ return options;
71
244
  };
72
245
 
73
246
  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.1",
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.1",
61
61
  "@types/aws-lambda": "^8.0.0",
62
62
  "@types/node": "^22.0.0",
63
63
  "aws-xray-sdk": "^3.3.3"