wormajs 0.2.0 → 0.2.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.
@@ -12,6 +12,10 @@ function extendsConfig(config, newConfig) {
12
12
  }
13
13
  // handleApi is a special case, we need to merge the functions
14
14
  if (typeof newValue === 'function' && typeof srcValue === 'function') {
15
+ // Avoid chaining the same function reference with itself (idempotency guard).
16
+ if (newValue === srcValue) {
17
+ return newValue;
18
+ }
15
19
  // chain the functions
16
20
  return (...args) => {
17
21
  const result = srcValue(...args);
package/dist/generate.js CHANGED
@@ -19,12 +19,11 @@ async function generate(config, options) {
19
19
  return [];
20
20
  const projectPath = options?.projectPath ?? process.cwd();
21
21
  const emit = options?.onProgress;
22
- // Load phase (shared, no per-gen events during load) plugins may modify generator configs
23
- helper_1.logger.debug('Loading config', { projectPath });
24
- await helper_1.configHelper.load(config, projectPath);
25
- // Use the processed generators from ConfigManager (after plugin hooks have run)
26
- const generators = helper_1.configHelper.getConfig().generator;
27
- helper_1.logger.debug('Config loaded', { generatorCount: generators.length });
22
+ // Each generate() call creates its own ConfigHelper / ConfigManager,
23
+ // so multiple concurrent calls never share mutable config state.
24
+ const helper = new helper_1.ConfigHelper();
25
+ await helper.load(config, projectPath);
26
+ const generators = helper.getConfig().generator;
28
27
  // Run all generators in parallel, each with its own ProgressTracker
29
28
  const results = await Promise.all(generators.map(async (gen, i) => {
30
29
  const generatorName = gen.input || `generator-${i}`;
@@ -1,30 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.configHelper = exports.ConfigHelper = void 0;
3
+ exports.ConfigHelper = void 0;
4
4
  const lodash_1 = require("lodash");
5
5
  const helper_1 = require("../../helper");
6
- const logger_1 = require("../../helper/logger");
7
6
  const ConfigManager_1 = require("./ConfigManager");
8
7
  const GeneratorHelper_1 = require("./GeneratorHelper");
9
8
  class ConfigHelper {
10
9
  constructor() {
11
- this.configManager = ConfigManager_1.ConfigManager.getInstance();
12
- }
13
- static getInstance() {
14
- if (!ConfigHelper.instance) {
15
- ConfigHelper.instance = new ConfigHelper();
16
- }
17
- return ConfigHelper.instance;
10
+ this.configManager = new ConfigManager_1.ConfigManager();
18
11
  }
19
12
  async load(config, projectPath = process.cwd(), tracker) {
20
13
  this.projectPath = projectPath;
21
- logger_1.logger.debug('ConfigHelper.load — loading config manager', { projectPath, generatorCount: config.generator?.length ?? 0 });
22
14
  await this.configManager.load(config, projectPath, tracker);
23
- logger_1.logger.debug('ConfigHelper.load — reading cache data');
24
15
  await this.readAlovaJson();
25
- logger_1.logger.debug('ConfigHelper.load — complete');
26
16
  }
27
- async readUserConfig(userConfig) {
17
+ static async readUserConfig(userConfig) {
28
18
  if (typeof userConfig === 'function') {
29
19
  return await userConfig();
30
20
  }
@@ -76,4 +66,3 @@ class ConfigHelper {
76
66
  }
77
67
  }
78
68
  exports.ConfigHelper = ConfigHelper;
79
- exports.configHelper = ConfigHelper.getInstance();
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.configManager = exports.ConfigManager = void 0;
6
+ exports.ConfigManager = void 0;
7
7
  const zod_validation_error_1 = require("zod-validation-error");
8
8
  const prepareConfig_1 = __importDefault(require("../../functions/prepareConfig"));
9
9
  const GeneratorHelper_1 = require("../../helper/config/GeneratorHelper");
@@ -17,12 +17,6 @@ class ConfigManager {
17
17
  this.defaultGeneratorConfig = GeneratorHelper_1.generatorHelper.getDefaultConfig();
18
18
  this.config = this.defaultConfig;
19
19
  }
20
- static getInstance() {
21
- if (!ConfigManager.instance) {
22
- ConfigManager.instance = new ConfigManager();
23
- }
24
- return ConfigManager.instance;
25
- }
26
20
  /**
27
21
  * 加载并验证配置
28
22
  */
@@ -84,5 +78,3 @@ class ConfigManager {
84
78
  }
85
79
  }
86
80
  exports.ConfigManager = ConfigManager;
87
- // 导出单例实例
88
- exports.configManager = ConfigManager.getInstance();
@@ -1,8 +1,73 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.applyModifierSchema = applyModifierSchema;
4
+ // Detect a `SchemaOptional` wrapper: { required: boolean, type: Schema }.
5
+ // The `required` must be a literal boolean so a plain SchemaReference whose
6
+ // property happens to be named "required" (e.g. { required: 'boolean' }) is not misread.
7
+ function isSchemaOptional(val) {
8
+ return !!val
9
+ && typeof val === 'object'
10
+ && !Array.isArray(val)
11
+ && typeof val.required === 'boolean'
12
+ && 'type' in val;
13
+ }
14
+ // Collapse (possibly nested) `SchemaOptional` wrappers.
15
+ // - The OUTERMOST `required` wins; inner `required` fields are ignored.
16
+ // - A non-wrapped value defaults to required (required: true).
17
+ function unwrapOptional(s) {
18
+ if (!isSchemaOptional(s)) {
19
+ return { required: true, type: s };
20
+ }
21
+ const required = s.required;
22
+ let type = s.type;
23
+ while (isSchemaOptional(type)) {
24
+ type = type.type;
25
+ }
26
+ return { required, type };
27
+ }
28
+ // Remove the internal `_$ref` marker that `removeAll$ref` stamps onto dereferenced
29
+ // component schemas. When a handler replaces a schema, the result must NOT inherit the
30
+ // original component's `_$ref` — otherwise `mergeObject`/`removeBaseReference` downstream
31
+ // treats the replacement as a reference to the original component and discards the change.
32
+ function stripInternalRef(schema) {
33
+ if (!schema || typeof schema !== 'object') {
34
+ return schema;
35
+ }
36
+ if (Array.isArray(schema)) {
37
+ return schema.map(stripInternalRef);
38
+ }
39
+ const out = {};
40
+ for (const key of Object.keys(schema)) {
41
+ if (key === '_$ref') {
42
+ continue;
43
+ }
44
+ out[key] = stripInternalRef(schema[key]);
45
+ }
46
+ return out;
47
+ }
48
+ // Set of valid SchemaPrimitive values for O(1) validation lookup
49
+ const VALID_PRIMITIVES = new Set([
50
+ 'number',
51
+ 'string',
52
+ 'boolean',
53
+ 'undefined',
54
+ 'null',
55
+ 'unknown',
56
+ 'any',
57
+ 'never',
58
+ ]);
59
+ function validatePrimitive(val) {
60
+ if (!VALID_PRIMITIVES.has(val)) {
61
+ throw new Error(`[payloadModifier] Invalid schema type "${val}". Must be one of: ${[...VALID_PRIMITIVES].join(', ')}`);
62
+ }
63
+ }
4
64
  // Convert Schema (custom spec) -> OpenAPI SchemaObject
5
65
  function toSchemaObject(base, s) {
66
+ // A `SchemaOptional` wrapper only affects requiredness (handled by the caller);
67
+ // here we care about the type shape, so fully unwrap nested wrappers first.
68
+ if (isSchemaOptional(s)) {
69
+ s = unwrapOptional(s).type;
70
+ }
6
71
  const result = { ...base };
7
72
  const cleanType = (schema) => {
8
73
  delete schema.type;
@@ -24,8 +89,9 @@ function toSchemaObject(base, s) {
24
89
  result.items = (items.length === 1 ? items[0] : items);
25
90
  return result;
26
91
  }
27
- // Primitive types and no-op primitives
92
+ // Primitive types validate against SchemaPrimitive set during conversion
28
93
  if (typeof s === 'string') {
94
+ validatePrimitive(s);
29
95
  result.type = s;
30
96
  return result;
31
97
  }
@@ -56,12 +122,16 @@ function toSchemaObject(base, s) {
56
122
  const spec = s;
57
123
  result.enum = spec.enum;
58
124
  if (spec.type) {
125
+ if (typeof spec.type === 'string') {
126
+ validatePrimitive(spec.type);
127
+ }
59
128
  result.type = spec.type;
60
129
  }
61
130
  return result;
62
131
  }
63
132
  // Object (reference-like map): replace properties and required with handler's spec
64
- // (the SchemaReference returned by the handler fully replaces this field, only keeping scalar fields like description from base)
133
+ // (the SchemaReference returned by the handler fully replaces this field, only keeping
134
+ // scalar fields like description from base)
65
135
  const ref = s;
66
136
  if (ref && typeof ref === 'object') {
67
137
  result.type = 'object';
@@ -72,15 +142,27 @@ function toSchemaObject(base, s) {
72
142
  if (!val) {
73
143
  continue;
74
144
  }
75
- const optional = key.endsWith('?');
76
- const cleanKey = optional ? key.slice(0, -1) : key;
77
- const baseProp = properties[cleanKey];
78
- properties[cleanKey] = toSchemaObject(baseProp || {}, val);
79
- if (optional) {
80
- requiredSet.delete(cleanKey);
145
+ // SchemaOptional wrapper: optionality expressed via { required, type };
146
+ // bare value defaults to required. Nested wrappers are collapsed — outermost
147
+ // `required` wins, inner ones are ignored.
148
+ let isOptional;
149
+ let effectiveVal;
150
+ if (isSchemaOptional(val)) {
151
+ const { required, type } = unwrapOptional(val);
152
+ isOptional = !required;
153
+ effectiveVal = type;
154
+ }
155
+ else {
156
+ isOptional = false;
157
+ effectiveVal = val;
158
+ }
159
+ const baseProp = properties[key];
160
+ properties[key] = toSchemaObject(baseProp || {}, effectiveVal);
161
+ if (isOptional) {
162
+ requiredSet.delete(key);
81
163
  }
82
164
  else {
83
- requiredSet.add(cleanKey);
165
+ requiredSet.add(key);
84
166
  }
85
167
  }
86
168
  result.properties = properties;
@@ -145,8 +227,8 @@ function toSchemaSpec(obj) {
145
227
  const result = {};
146
228
  for (const key of Object.keys(properties)) {
147
229
  const spec = toSchemaSpec(properties[key]);
148
- const finalKey = requiredSet.has(key) ? key : `${key}?`;
149
- result[finalKey] = spec;
230
+ // Required fields are written bare; optional fields wrapped with SchemaOptional
231
+ result[key] = requiredSet.has(key) ? spec : { required: false, type: spec };
150
232
  }
151
233
  return result;
152
234
  }
@@ -159,9 +241,9 @@ function toSchemaSpec(obj) {
159
241
  return schemaTypeToPrimitiveType(obj.type);
160
242
  }
161
243
  // Replace whole schema based on handler result (used for params/pathParams)
162
- function applyModifierSchema(schema, config, { required }) {
244
+ function applyModifierSchema(schema, config, { required, key }) {
163
245
  if (!schema || typeof schema !== 'object') {
164
- return schema;
246
+ return { required, schema: schema };
165
247
  }
166
248
  const cloned = { ...schema };
167
249
  const currentSpec = toSchemaSpec(cloned);
@@ -169,23 +251,38 @@ function applyModifierSchema(schema, config, { required }) {
169
251
  const handlerInput = (required === false && typeof currentSpec === 'string')
170
252
  ? { required: false, type: currentSpec }
171
253
  : currentSpec;
172
- const ret = config.handler(handlerInput);
254
+ const ret = config.handler(handlerInput, key);
173
255
  if (!ret) {
174
256
  return {
175
257
  required,
176
258
  schema: null,
177
259
  };
178
260
  }
179
- // A returned { required, type } means changing requiredness (driven by the `type` field)
180
- if (typeof ret === 'object' && !Array.isArray(ret) && 'required' in ret && 'type' in ret) {
181
- const opt = ret;
261
+ // A returned SchemaOptional means changing requiredness (driven by the `type` field).
262
+ // Nested wrappers are collapsed: the outermost `required` wins, inner ones are ignored;
263
+ // `type` may be any Schema expression (primitive, object, array, union, ...).
264
+ if (isSchemaOptional(ret)) {
265
+ const { required: nextRequired, type } = unwrapOptional(ret);
266
+ let r = stripInternalRef(toSchemaObject(cloned, type));
267
+ // When handler explicitly sets required=false, propagate to object-level required array
268
+ // so all properties become nullable as semantically expected
269
+ if (!nextRequired && r && typeof r === 'object' && !Array.isArray(r)) {
270
+ const robj = r;
271
+ if (robj.type === 'object' && Array.isArray(robj.required) && robj.required.length > 0) {
272
+ r = { ...r, required: [] };
273
+ }
274
+ }
182
275
  return {
183
- required: !!(opt.required ?? required),
184
- schema: toSchemaObject(cloned, opt.type),
276
+ required: nextRequired,
277
+ schema: r,
185
278
  };
186
279
  }
280
+ // Non-SchemaOptional return: handler explicitly provides a type value,
281
+ // so default to required=true (the handler had the chance to wrap with
282
+ // { required: false, type: ... } if it wanted to keep it optional).
283
+ const r = stripInternalRef(toSchemaObject(cloned, ret));
187
284
  return {
188
- required,
189
- schema: toSchemaObject(cloned, ret),
285
+ required: true,
286
+ schema: r,
190
287
  };
191
288
  }
@@ -4,13 +4,58 @@ exports.payloadModifier = payloadModifier;
4
4
  const constant_1 = require("../../../constant");
5
5
  const utils_1 = require("../utils");
6
6
  const hepler_1 = require("./hepler");
7
- // Apply modifications to object properties (for data/response scopes)
7
+ // Convert parameters of a specific type (query/path) into an object schema
8
+ function parametersToSchema(parameters, type) {
9
+ if (!parameters || !Array.isArray(parameters)) {
10
+ return { type: 'object', properties: {}, required: [] };
11
+ }
12
+ const schema = { type: 'object', properties: {}, required: [] };
13
+ for (const param of parameters) {
14
+ if (param.in === type) {
15
+ ;
16
+ schema.properties[param.name] = param.schema;
17
+ if (param.required) {
18
+ ;
19
+ schema.required.push(param.name);
20
+ }
21
+ }
22
+ }
23
+ return schema;
24
+ }
25
+ // Convert an object schema back to parameters, keeping other types untouched
26
+ function schemaToParameters(parameters, schema, type) {
27
+ if (!parameters || !Array.isArray(parameters)) {
28
+ return parameters;
29
+ }
30
+ if (!schema || typeof schema !== 'object' || !schema.properties) {
31
+ return parameters.filter(param => param.in !== type);
32
+ }
33
+ const requiredSet = new Set(Array.isArray(schema.required) ? schema.required : []);
34
+ const newParameters = [];
35
+ for (const param of parameters) {
36
+ if (param.in !== type) {
37
+ newParameters.push(param);
38
+ continue;
39
+ }
40
+ const propSchema = schema.properties[param.name];
41
+ if (!propSchema) {
42
+ continue;
43
+ }
44
+ newParameters.push({
45
+ ...param,
46
+ schema: propSchema,
47
+ required: requiredSet.has(param.name),
48
+ });
49
+ }
50
+ return newParameters;
51
+ }
52
+ // Apply modifications to properties of an object schema (used when `match` is set)
8
53
  function modifySchemaProperties(schema, config) {
9
54
  if (!schema || typeof schema !== 'object') {
10
55
  return schema;
11
56
  }
12
57
  const targetSchema = { ...schema };
13
- // union recursively
58
+ // recurse into union keywords
14
59
  if (Array.isArray(targetSchema.oneOf)) {
15
60
  targetSchema.oneOf = targetSchema.oneOf.map(item => modifySchemaProperties(item, config));
16
61
  }
@@ -20,7 +65,7 @@ function modifySchemaProperties(schema, config) {
20
65
  if (Array.isArray(targetSchema.allOf)) {
21
66
  targetSchema.allOf = targetSchema.allOf.map(item => modifySchemaProperties(item, config));
22
67
  }
23
- // modify properties
68
+ // modify matched properties
24
69
  if (targetSchema.properties) {
25
70
  const props = { ...targetSchema.properties };
26
71
  let required = Array.isArray(targetSchema.required) ? [...targetSchema.required] : [];
@@ -28,7 +73,7 @@ function modifySchemaProperties(schema, config) {
28
73
  if (!(0, utils_1.isMatch)(key, config.match)) {
29
74
  continue;
30
75
  }
31
- const { required: requiredOverride, schema: schemaValue } = (0, hepler_1.applyModifierSchema)(props[key], config, { required: required.includes(key) });
76
+ const { required: requiredOverride, schema: schemaValue } = (0, hepler_1.applyModifierSchema)(props[key], config, { required: required.includes(key), key });
32
77
  required = required.filter(r => r !== key);
33
78
  if (!schemaValue) {
34
79
  delete props[key];
@@ -44,56 +89,59 @@ function modifySchemaProperties(schema, config) {
44
89
  }
45
90
  return targetSchema;
46
91
  }
92
+ // Apply modifications to matched parameters (used when `match` is set for params/pathParams)
47
93
  function modifyParameters(parameters, type, config) {
48
94
  if (!parameters || !Array.isArray(parameters)) {
49
95
  return parameters;
50
96
  }
51
97
  return parameters.map((param) => {
52
- if (param.in === type) {
53
- if (!(0, utils_1.isMatch)(param.name, config.match)) {
54
- return param;
55
- }
56
- const { schema, required } = (0, hepler_1.applyModifierSchema)(param.schema, config, { required: !!param.required });
57
- if (!schema) {
58
- return null;
59
- }
60
- return {
61
- ...param,
62
- schema,
63
- required,
64
- };
98
+ if (param.in !== type || !(0, utils_1.isMatch)(param.name, config.match)) {
99
+ return param;
100
+ }
101
+ const { schema, required } = (0, hepler_1.applyModifierSchema)(param.schema, config, { required: !!param.required, key: param.name });
102
+ if (!schema) {
103
+ return null;
65
104
  }
66
- return param;
105
+ return { ...param, schema, required };
67
106
  }).filter(item => item !== null);
68
107
  }
108
+ // Apply config to a parameter scope (params or pathParams)
109
+ function applyToParameters(parameters, type, config) {
110
+ if (!parameters)
111
+ return undefined;
112
+ if (config.match) {
113
+ return modifyParameters(parameters, type, config);
114
+ }
115
+ const schema = parametersToSchema(parameters, type);
116
+ const result = (0, hepler_1.applyModifierSchema)(schema, config, { required: false });
117
+ return schemaToParameters(parameters, result.schema, type);
118
+ }
119
+ // Apply config to a schema scope (data or response)
120
+ function applyToSchemaField(schema, config) {
121
+ if (!schema)
122
+ return undefined;
123
+ if (config.match) {
124
+ return modifySchemaProperties(schema, config);
125
+ }
126
+ return (0, hepler_1.applyModifierSchema)(schema, config, { required: false }).schema ?? undefined;
127
+ }
69
128
  function payloadModifierApiDescriptor(apiDescriptor, config) {
70
- if (!apiDescriptor) {
129
+ if (!apiDescriptor)
71
130
  return null;
72
- }
73
131
  const newDescriptor = { ...apiDescriptor };
74
132
  const { scope } = config;
75
133
  switch (scope) {
76
134
  case 'params':
77
- if (newDescriptor.parameters) {
78
- newDescriptor.parameters = modifyParameters(newDescriptor.parameters, constant_1.ParameterIn.QUERY, config);
79
- }
135
+ newDescriptor.parameters = applyToParameters(newDescriptor.parameters, constant_1.ParameterIn.QUERY, config);
80
136
  break;
81
137
  case 'pathParams':
82
- if (newDescriptor.parameters) {
83
- newDescriptor.parameters = modifyParameters(newDescriptor.parameters, constant_1.ParameterIn.PATH, config);
84
- }
138
+ newDescriptor.parameters = applyToParameters(newDescriptor.parameters, constant_1.ParameterIn.PATH, config);
85
139
  break;
86
140
  case 'data':
87
- if (newDescriptor.requestBody) {
88
- newDescriptor.requestBody = modifySchemaProperties(newDescriptor.requestBody, config);
89
- }
141
+ newDescriptor.requestBody = applyToSchemaField(newDescriptor.requestBody, config);
90
142
  break;
91
143
  case 'response':
92
- if (newDescriptor.responses) {
93
- newDescriptor.responses = modifySchemaProperties(newDescriptor.responses, config);
94
- }
95
- break;
96
- default:
144
+ newDescriptor.responses = applyToSchemaField(newDescriptor.responses, config);
97
145
  break;
98
146
  }
99
147
  return newDescriptor;
@@ -34,7 +34,6 @@ async function readConfig(projectPath = process.cwd()) {
34
34
  name: 'readConfig',
35
35
  });
36
36
  }
37
- await helper_1.configHelper.load(config, projectPath);
38
37
  return config;
39
38
  }
40
39
  // 获取用户已安装的依赖
@@ -67,10 +66,8 @@ async function readConfig(projectPath = process.cwd()) {
67
66
  finally {
68
67
  await (0, promises_1.unlink)(outfile);
69
68
  }
70
- const config = await helper_1.configHelper.readUserConfig(module.default || module);
71
- // Read the cache file and save it
72
- await helper_1.configHelper.load(config, projectPath);
73
- return helper_1.configHelper.getConfig();
69
+ const config = await helper_1.ConfigHelper.readUserConfig(module.default || module);
70
+ return config;
74
71
  }
75
72
  /**
76
73
  * Get cached API docs. Cache is self-describing — no config needed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wormajs",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "A modern OpenAPI code generator - Generate type-safe API clients from OpenAPI specs",
5
5
  "author": "worma",
6
6
  "license": "MIT",
@@ -497,7 +497,7 @@ export declare const logger: Logger$1;
497
497
  * @param projectPath The project path where the configuration file is located. The default value is `process.cwd()`.
498
498
  * @returns a promise instance that contains configuration object.
499
499
  */
500
- export declare function readConfig(projectPath?: string): Promise<Readonly<Config>>;
500
+ export declare function readConfig(projectPath?: string): Promise<Config>;
501
501
  /**
502
502
  * Get cached API docs. Cache is self-describing — no config needed.
503
503
  * In monorepo, pass ANY sub-package path; cache is always read from the unified cacheRoot.
@@ -542,7 +542,9 @@ export type SchemaPrimitive = "number" | "string" | "boolean" | "undefined" | "n
542
542
  export type SchemaArray = Schema[];
543
543
  /**
544
544
  * Object/reference type.
545
- * Append '?' to the end of a key to mark it optional.
545
+ * Required properties are written directly; optional properties are wrapped
546
+ * with the `SchemaOptional` form `{ required: false, type: Schema }`
547
+ * (consistent with how a standalone optional primitive is represented).
546
548
  */
547
549
  export interface SchemaReference {
548
550
  [attr: string]: Schema;
@@ -578,8 +580,8 @@ export interface SchemaOptional {
578
580
  * The data Schema.
579
581
  * - SchemaArray is a native array (elements are Schemas)
580
582
  * - composite types use { oneOf | anyOf | allOf: Schema[] }
581
- * - optional object properties use a trailing '?' on the key;
582
- * a standalone optional primitive uses the SchemaOptional wrapper
583
+ * - optional object properties are wrapped with `SchemaOptional` ({ required: false, type: Schema });
584
+ * a standalone optional primitive uses the same SchemaOptional wrapper
583
585
  */
584
586
  export type Schema = SchemaPrimitive | SchemaReference | SchemaArray | SchemaEnum | SchemaOneOf | SchemaAnyOf | SchemaAllOf | SchemaOptional;
585
587
  export interface ModifierConfig {
@@ -599,10 +601,12 @@ export interface ModifierConfig {
599
601
  * @param schema the original field type, already converted to the user-facing Schema representation.
600
602
  * When the field itself is optional and is a primitive, it is passed as { required: false, type: 'string' }.
601
603
  * Narrow the type inside handler if needed (e.g. with a cast).
604
+ * @param key the matched field key. When `match` is omitted, the whole scope object is passed to the handler
605
+ * once and `key` is `undefined`; when `match` is set, `key` is the matched field name for each call.
602
606
  * @returns Schema to change the type; { required: boolean, type: Schema } to change requiredness (driven by `type`);
603
607
  * void | null | undefined to remove the field.
604
608
  */
605
- handler: (schema: Schema) => Schema | {
609
+ handler: (schema: Schema, key?: string) => Schema | {
606
610
  required: boolean;
607
611
  type: Schema;
608
612
  } | void | null | undefined;