@webpieces/core-util 0.4.805 → 0.4.807
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.
- package/package.json +1 -1
- package/src/http/StreamingContract.d.ts +20 -10
- package/src/http/StreamingContract.js +23 -16
- package/src/http/StreamingContract.js.map +1 -1
- package/src/http/api-type.js +2 -2
- package/src/http/api-type.js.map +1 -1
- package/src/index.d.ts +5 -4
- package/src/index.js +9 -11
- package/src/index.js.map +1 -1
- package/src/mcp/DtoSchema.d.ts +72 -85
- package/src/mcp/DtoSchema.js +154 -345
- package/src/mcp/DtoSchema.js.map +1 -1
- package/src/mcp/McpCompileAssertions.js +22 -34
- package/src/mcp/McpCompileAssertions.js.map +1 -1
- package/src/mcp/McpMetadata.d.ts +23 -17
- package/src/mcp/McpMetadata.js +25 -15
- package/src/mcp/McpMetadata.js.map +1 -1
- package/src/mcp/McpToolCatalog.d.ts +83 -0
- package/src/mcp/McpToolCatalog.js +185 -0
- package/src/mcp/McpToolCatalog.js.map +1 -0
package/src/mcp/DtoSchema.js
CHANGED
|
@@ -1,84 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DtoSchemaBuilder = exports.DtoValidationFailure = exports.ApiJsonSchema = exports.WpDtoFieldMetadata = exports.WpDtoMapFieldOptions = exports.WpMcpHeader = exports.WpDtoFieldOptions = void 0;
|
|
4
|
-
exports.WpDto = WpDto;
|
|
5
|
-
exports.WpDtoField = WpDtoField;
|
|
6
|
-
exports.WpResponseDto = WpResponseDto;
|
|
7
|
-
require("reflect-metadata");
|
|
8
|
-
const DTO_MARKER = 'webpieces:dto-schema';
|
|
9
|
-
const DTO_FIELDS = 'webpieces:dto-fields';
|
|
10
|
-
const RESPONSE_DTOS = 'webpieces:response-dtos';
|
|
11
|
-
/** Additive field metadata; the scalar/object type itself comes from `design:type`. */
|
|
12
|
-
class WpDtoFieldOptions {
|
|
13
|
-
description;
|
|
14
|
-
required;
|
|
15
|
-
arrayItems;
|
|
16
|
-
integer;
|
|
17
|
-
minimum;
|
|
18
|
-
maximum;
|
|
19
|
-
enumValues;
|
|
20
|
-
mcpHeader;
|
|
21
|
-
constructor(description,
|
|
22
|
-
/** Required/optional is explicit because TypeScript erases `?` at runtime. */
|
|
23
|
-
required,
|
|
24
|
-
/** Required for arrays because TypeScript erases their element type. */
|
|
25
|
-
arrayItems,
|
|
26
|
-
/** A reflected Number is otherwise emitted as JSON Schema `number`. */
|
|
27
|
-
integer = false, minimum, maximum, enumValues,
|
|
28
|
-
/** MCP 2026 SEP-2243 header mirrored as `Mcp-Param-{name}`. */
|
|
29
|
-
mcpHeader) {
|
|
30
|
-
this.description = description;
|
|
31
|
-
this.required = required;
|
|
32
|
-
this.arrayItems = arrayItems;
|
|
33
|
-
this.integer = integer;
|
|
34
|
-
this.minimum = minimum;
|
|
35
|
-
this.maximum = maximum;
|
|
36
|
-
this.enumValues = enumValues;
|
|
37
|
-
this.mcpHeader = mcpHeader;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
exports.WpDtoFieldOptions = WpDtoFieldOptions;
|
|
41
2
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
3
|
+
* The JSON Schema subset webpieces publishes on its API boundaries, and the ONE runtime validator of
|
|
4
|
+
* it.
|
|
5
|
+
*
|
|
6
|
+
* ## Nothing here reads reflect-metadata any more
|
|
7
|
+
*
|
|
8
|
+
* Until #984 this file also carried `@WpDto`, `@WpDtoField`, `WpDtoFieldOptions`,
|
|
9
|
+
* `WpDtoMapFieldOptions`, `WpMcpHeader`, `@WpResponseDto` and `DtoSchemaBuilder` — a second, runtime
|
|
10
|
+
* spelling of facts the TypeScript compiler already holds. #983 measured the two against each other
|
|
11
|
+
* and found the compiler reproduces every one of those arguments byte for byte, so they are deleted
|
|
12
|
+
* rather than deprecated (`.claude/rules/no-backwards-compat.md`): a schema is now BUILT at build
|
|
13
|
+
* time from the source by `@webpieces/api-doc-model`, and the runtime is HANDED the result.
|
|
14
|
+
*
|
|
15
|
+
* What survives is the schema itself, a builder for the boundaries that construct one by hand, and a
|
|
16
|
+
* validator that reads the schema instead of a decorator.
|
|
45
17
|
*/
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
constructor(name) {
|
|
49
|
-
this.name = name;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
exports.WpMcpHeader = WpMcpHeader;
|
|
53
|
-
/**
|
|
54
|
-
* Field metadata for a typed map such as `Record<string, string>` or `Record<string, SomeDto>`.
|
|
55
|
-
* Keys are open strings; every VALUE must match `mapValues`. The value type is explicit for the same
|
|
56
|
-
* reason `arrayItems` is: TypeScript erases it, and a `Record` reflects as plain `Object`.
|
|
57
|
-
*/
|
|
58
|
-
class WpDtoMapFieldOptions {
|
|
59
|
-
description;
|
|
60
|
-
required;
|
|
61
|
-
mapValues;
|
|
62
|
-
constructor(description,
|
|
63
|
-
/** Required/optional is explicit because TypeScript erases `?` at runtime. */
|
|
64
|
-
required, mapValues) {
|
|
65
|
-
this.description = description;
|
|
66
|
-
this.required = required;
|
|
67
|
-
this.mapValues = mapValues;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
exports.WpDtoMapFieldOptions = WpDtoMapFieldOptions;
|
|
71
|
-
class WpDtoFieldMetadata {
|
|
72
|
-
propertyKey;
|
|
73
|
-
options;
|
|
74
|
-
constructor(propertyKey, options) {
|
|
75
|
-
this.propertyKey = propertyKey;
|
|
76
|
-
this.options = options;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
exports.WpDtoFieldMetadata = WpDtoFieldMetadata;
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.ApiJsonSchemaValidator = exports.DtoValidationFailure = exports.ObjectSchemaBuilder = exports.ApiJsonSchema = void 0;
|
|
80
20
|
/** The JSON Schema subset emitted for Webpieces API DTOs. */
|
|
81
21
|
class ApiJsonSchema {
|
|
22
|
+
/** One type, or `[T, "null"]` for a nullable field. */
|
|
82
23
|
type;
|
|
83
24
|
description;
|
|
84
25
|
properties;
|
|
@@ -92,8 +33,71 @@ class ApiJsonSchema {
|
|
|
92
33
|
constructor(type) {
|
|
93
34
|
this.type = type;
|
|
94
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* The non-`null` member of the type, which is the one that decides how a value is checked.
|
|
38
|
+
*
|
|
39
|
+
* These three are STATIC and not instance methods on purpose: an instance method would put a
|
|
40
|
+
* function on the structural type, and this class is handed straight to the MCP SDK, whose
|
|
41
|
+
* parameter types are index-signature JSON objects that a class carrying methods no longer
|
|
42
|
+
* satisfies. It stays a data-only structure (`CLAUDE.md` §1) and the helpers live beside it.
|
|
43
|
+
*/
|
|
44
|
+
// webpieces-disable no-function-outside-class -- static reader of this class
|
|
45
|
+
static baseTypeOf(schema) {
|
|
46
|
+
if (schema.type === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
if (typeof schema.type === 'string')
|
|
49
|
+
return schema.type;
|
|
50
|
+
return schema.type.find((one) => one !== 'null');
|
|
51
|
+
}
|
|
52
|
+
/** True when the schema was widened with `"null"`, i.e. the property may hold `null`. */
|
|
53
|
+
// webpieces-disable no-function-outside-class -- static reader of this class
|
|
54
|
+
static allowsNull(schema) {
|
|
55
|
+
return typeof schema.type !== 'string' && (schema.type?.includes('null') ?? false);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The one closure gate for an object schema. Closed means no key can carry an unspecified value:
|
|
59
|
+
* either extra keys are forbidden (`additionalProperties: false`) or every extra key's value is
|
|
60
|
+
* typed (`additionalProperties: <schema>`, a typed map). A missing or `true` value is open.
|
|
61
|
+
* Use this instead of `schema.additionalProperties === false`, which rejects typed maps.
|
|
62
|
+
*/
|
|
63
|
+
// webpieces-disable no-function-outside-class -- static reader of this class
|
|
64
|
+
static isClosed(schema) {
|
|
65
|
+
const extra = schema.additionalProperties;
|
|
66
|
+
return extra === false || (typeof extra === 'object' && extra !== null);
|
|
67
|
+
}
|
|
95
68
|
}
|
|
96
69
|
exports.ApiJsonSchema = ApiJsonSchema;
|
|
70
|
+
/**
|
|
71
|
+
* Builds the CLOSED object schema a hand-written runtime boundary validates against — `@WpStream`'s
|
|
72
|
+
* two event schemas are the whole of its client list today.
|
|
73
|
+
*
|
|
74
|
+
* It exists because the alternative spellings are both worse: a decorator that re-states the field
|
|
75
|
+
* types the compiler already knows is exactly what #984 deleted, and a bare object literal is what
|
|
76
|
+
* `CLAUDE.md` §3 forbids. A generated schema never comes through here — it is read from the build's
|
|
77
|
+
* own artifact.
|
|
78
|
+
*/
|
|
79
|
+
class ObjectSchemaBuilder {
|
|
80
|
+
schema = new ApiJsonSchema('object');
|
|
81
|
+
requiredNames = [];
|
|
82
|
+
constructor() {
|
|
83
|
+
this.schema.properties = {};
|
|
84
|
+
this.schema.additionalProperties = false;
|
|
85
|
+
}
|
|
86
|
+
required(name, type) {
|
|
87
|
+
this.requiredNames.push(name);
|
|
88
|
+
return this.optional(name, type);
|
|
89
|
+
}
|
|
90
|
+
optional(name, type) {
|
|
91
|
+
this.schema.properties[name] = type;
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
build() {
|
|
95
|
+
if (this.requiredNames.length > 0)
|
|
96
|
+
this.schema.required = [...this.requiredNames];
|
|
97
|
+
return this.schema;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
exports.ObjectSchemaBuilder = ObjectSchemaBuilder;
|
|
97
101
|
/** A validation result safe to show to an API caller/model. */
|
|
98
102
|
class DtoValidationFailure {
|
|
99
103
|
field;
|
|
@@ -106,305 +110,110 @@ class DtoValidationFailure {
|
|
|
106
110
|
}
|
|
107
111
|
}
|
|
108
112
|
exports.DtoValidationFailure = DtoValidationFailure;
|
|
109
|
-
/**
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const dtoClass = target.constructor;
|
|
122
|
-
const fields = Reflect.getMetadata(DTO_FIELDS, dtoClass) ?? {};
|
|
123
|
-
const name = String(propertyKey);
|
|
124
|
-
fields[name] = new WpDtoFieldMetadata(name, options);
|
|
125
|
-
Reflect.defineMetadata(DTO_FIELDS, fields, dtoClass);
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
/** Declares an async API method's response DTO because reflection sees Promise, not its generic. */
|
|
129
|
-
// webpieces-disable no-function-outside-class -- decorator factories are inherently module-scope
|
|
130
|
-
function WpResponseDto(responseDto) {
|
|
131
|
-
return (target, propertyKey) => {
|
|
132
|
-
const apiClass = target.constructor;
|
|
133
|
-
const responses = Reflect.getMetadata(RESPONSE_DTOS, apiClass) ?? {};
|
|
134
|
-
responses[String(propertyKey)] = responseDto;
|
|
135
|
-
Reflect.defineMetadata(RESPONSE_DTOS, responses, apiClass);
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
/** Builds and validates the same DTO schema used for MCP input and structured output. */
|
|
139
|
-
class DtoSchemaBuilder {
|
|
140
|
-
requestClassOf(apiClass, methodName) {
|
|
141
|
-
// webpieces-disable no-any-unknown -- reflect-metadata returns erased runtime constructor tokens
|
|
142
|
-
const params = Reflect.getMetadata('design:paramtypes', apiClass.prototype, methodName);
|
|
143
|
-
if (!params || params.length !== 1) {
|
|
144
|
-
throw new Error(`${apiClass.name}.${methodName} must take exactly one @WpDto request class to be an MCP tool.`);
|
|
145
|
-
}
|
|
146
|
-
return params[0];
|
|
147
|
-
}
|
|
148
|
-
responseClassOf(apiClass, methodName) {
|
|
149
|
-
const responses = Reflect.getMetadata(RESPONSE_DTOS, apiClass) ?? {};
|
|
150
|
-
const response = responses[methodName];
|
|
151
|
-
if (!response) {
|
|
152
|
-
throw new Error(`${apiClass.name}.${methodName} must declare @WpResponseDto(() => ResponseClass) before MCP exposure.`);
|
|
153
|
-
}
|
|
154
|
-
return response();
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* The one closure gate for an object schema. Closed means no key can carry an unspecified value:
|
|
158
|
-
* either extra keys are forbidden (`additionalProperties: false`) or every extra key's value is
|
|
159
|
-
* typed (`additionalProperties: <schema>`, a typed map). A missing or `true` value is open.
|
|
160
|
-
* Use this instead of `schema.additionalProperties === false`, which rejects typed maps.
|
|
161
|
-
*/
|
|
162
|
-
isClosedSchema(schema) {
|
|
163
|
-
const extra = schema.additionalProperties;
|
|
164
|
-
return extra === false || (typeof extra === 'object' && extra !== null);
|
|
165
|
-
}
|
|
166
|
-
build(dtoClass) {
|
|
167
|
-
const schema = this.buildAt(dtoClass, new Set());
|
|
168
|
-
this.validateMcpHeaders(schema, new Map());
|
|
169
|
-
return schema;
|
|
170
|
-
}
|
|
171
|
-
validate(dtoClass, value) {
|
|
172
|
-
return this.validateAt(dtoClass, value, '$');
|
|
173
|
-
}
|
|
174
|
-
buildAt(dtoClass, parents) {
|
|
175
|
-
this.requireDto(dtoClass);
|
|
176
|
-
if (parents.has(dtoClass)) {
|
|
177
|
-
throw new Error(`Recursive DTO ${dtoClass.name} cannot use an inline MCP schema.`);
|
|
178
|
-
}
|
|
179
|
-
const nextParents = new Set(parents);
|
|
180
|
-
nextParents.add(dtoClass);
|
|
181
|
-
const schema = new ApiJsonSchema('object');
|
|
182
|
-
schema.properties = {};
|
|
183
|
-
schema.additionalProperties = false;
|
|
184
|
-
const required = [];
|
|
185
|
-
for (const field of this.fields(dtoClass)) {
|
|
186
|
-
schema.properties[field.propertyKey] = this.fieldSchema(dtoClass, field, nextParents);
|
|
187
|
-
if (field.options.required)
|
|
188
|
-
required.push(field.propertyKey);
|
|
189
|
-
}
|
|
190
|
-
if (required.length > 0)
|
|
191
|
-
schema.required = required;
|
|
192
|
-
return schema;
|
|
113
|
+
/**
|
|
114
|
+
* Validates a value against an {@link ApiJsonSchema} — the ONE runtime narrowing boundary for MCP
|
|
115
|
+
* tool arguments, MCP structured output and typed stream events.
|
|
116
|
+
*
|
|
117
|
+
* It reads the SCHEMA, which is the same object the build published to agents, so a document that
|
|
118
|
+
* says a field is required and a server that accepts it missing cannot disagree. The predecessor
|
|
119
|
+
* walked reflect-metadata instead, which meant the published document and the accepted request were
|
|
120
|
+
* derived from two different declarations.
|
|
121
|
+
*/
|
|
122
|
+
class ApiJsonSchemaValidator {
|
|
123
|
+
validate(schema, value) {
|
|
124
|
+
return this.validateAt(schema, value, '$');
|
|
193
125
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
break;
|
|
215
|
-
case 'Boolean':
|
|
216
|
-
schema = new ApiJsonSchema('boolean');
|
|
217
|
-
break;
|
|
218
|
-
case 'Array':
|
|
219
|
-
if (!options.arrayItems) {
|
|
220
|
-
throw new Error(`${dtoClass.name}.${field.propertyKey} must declare arrayItems.`);
|
|
221
|
-
}
|
|
222
|
-
schema = new ApiJsonSchema('array');
|
|
223
|
-
schema.items = this.itemSchema(options.arrayItems, parents);
|
|
224
|
-
break;
|
|
126
|
+
validateAt(schema, value, path) {
|
|
127
|
+
if (value === null) {
|
|
128
|
+
return ApiJsonSchema.allowsNull(schema)
|
|
129
|
+
? undefined
|
|
130
|
+
: new DtoValidationFailure(path, `${path} must not be null`);
|
|
131
|
+
}
|
|
132
|
+
switch (ApiJsonSchema.baseTypeOf(schema)) {
|
|
133
|
+
case 'object':
|
|
134
|
+
return this.validateObject(schema, value, path);
|
|
135
|
+
case 'array':
|
|
136
|
+
return this.validateArray(schema, value, path);
|
|
137
|
+
case 'string':
|
|
138
|
+
return this.validateString(schema, value, path);
|
|
139
|
+
case 'number':
|
|
140
|
+
case 'integer':
|
|
141
|
+
return this.validateNumber(schema, value, path);
|
|
142
|
+
case 'boolean':
|
|
143
|
+
return typeof value === 'boolean'
|
|
144
|
+
? undefined
|
|
145
|
+
: new DtoValidationFailure(path, `${path} must be a boolean`);
|
|
225
146
|
default:
|
|
226
|
-
|
|
147
|
+
return undefined;
|
|
227
148
|
}
|
|
228
|
-
schema.description = options.description;
|
|
229
|
-
if (options.minimum !== undefined)
|
|
230
|
-
schema.minimum = options.minimum;
|
|
231
|
-
if (options.maximum !== undefined)
|
|
232
|
-
schema.maximum = options.maximum;
|
|
233
|
-
if (options.enumValues)
|
|
234
|
-
schema.enum = [...options.enumValues];
|
|
235
|
-
if (options.mcpHeader)
|
|
236
|
-
schema['x-mcp-header'] = options.mcpHeader.name;
|
|
237
|
-
return schema;
|
|
238
149
|
}
|
|
239
|
-
|
|
240
|
-
const label = `${dtoClass.name}.${field.propertyKey}`;
|
|
241
|
-
const options = field.options;
|
|
242
|
-
if (options instanceof WpDtoMapFieldOptions) {
|
|
243
|
-
if (reflected !== Object) {
|
|
244
|
-
throw new Error(`${label} declares mapValues but is not a map (Record<string, V>).`);
|
|
245
|
-
}
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
if (reflected === Object) {
|
|
249
|
-
throw new Error(`${label} has type Object: an interface or Record can never be a @WpDto. ` +
|
|
250
|
-
'For a map, use WpDtoMapFieldOptions.');
|
|
251
|
-
}
|
|
252
|
-
if (reflected.name !== 'Array' && options.arrayItems) {
|
|
253
|
-
throw new Error(`${label} declares arrayItems but is not an array.`);
|
|
254
|
-
}
|
|
255
|
-
if (reflected.name !== 'Number' &&
|
|
256
|
-
(options.integer || options.minimum !== undefined || options.maximum !== undefined)) {
|
|
257
|
-
throw new Error(`${label} declares numeric constraints but is not a number.`);
|
|
258
|
-
}
|
|
259
|
-
if (options.minimum !== undefined &&
|
|
260
|
-
options.maximum !== undefined &&
|
|
261
|
-
options.minimum > options.maximum) {
|
|
262
|
-
throw new Error(`${label} minimum cannot exceed maximum.`);
|
|
263
|
-
}
|
|
264
|
-
if (options.enumValues && reflected.name !== 'String') {
|
|
265
|
-
throw new Error(`${label} enumValues require a string field.`);
|
|
266
|
-
}
|
|
267
|
-
if (options.mcpHeader !== undefined) {
|
|
268
|
-
if (!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(options.mcpHeader.name)) {
|
|
269
|
-
throw new Error(`${label} mcpHeader must be an RFC 9110 token.`);
|
|
270
|
-
}
|
|
271
|
-
if (!['String', 'Number', 'Boolean'].includes(reflected.name)) {
|
|
272
|
-
throw new Error(`${label} mcpHeader requires a primitive field.`);
|
|
273
|
-
}
|
|
274
|
-
if (reflected.name === 'Number' && !options.integer) {
|
|
275
|
-
throw new Error(`${label} mcpHeader cannot mirror a non-integer number (MCP 2026-07-28).`);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
itemSchema(item, parents) {
|
|
280
|
-
if (typeof item !== 'string')
|
|
281
|
-
return this.buildAt(item, parents);
|
|
282
|
-
return new ApiJsonSchema(item === 'integer' ? 'integer' : item);
|
|
283
|
-
}
|
|
284
|
-
validateMcpHeaders(schema, seen) {
|
|
285
|
-
const header = schema['x-mcp-header'];
|
|
286
|
-
if (header) {
|
|
287
|
-
const key = header.toLowerCase();
|
|
288
|
-
const prior = seen.get(key);
|
|
289
|
-
if (prior) {
|
|
290
|
-
throw new Error(`mcpHeader '${header}' duplicates '${prior}' case-insensitively.`);
|
|
291
|
-
}
|
|
292
|
-
seen.set(key, header);
|
|
293
|
-
}
|
|
294
|
-
for (const child of Object.values(schema.properties ?? {})) {
|
|
295
|
-
this.validateMcpHeaders(child, seen);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
validateAt(dtoClass, value, path) {
|
|
299
|
-
this.requireDto(dtoClass);
|
|
150
|
+
validateObject(schema, value, path) {
|
|
300
151
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
301
152
|
return new DtoValidationFailure(path, `${path} must be an object`);
|
|
302
153
|
}
|
|
303
|
-
const
|
|
304
|
-
const
|
|
154
|
+
const properties = schema.properties ?? {};
|
|
155
|
+
const extra = schema.additionalProperties;
|
|
305
156
|
for (const key of Object.keys(value)) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (fieldValue === undefined) {
|
|
312
|
-
if (field.options.required) {
|
|
313
|
-
return new DtoValidationFailure(`${path}.${field.propertyKey}`, `${path}.${field.propertyKey} is required`);
|
|
157
|
+
const entry = Object.getOwnPropertyDescriptor(value, key)?.value;
|
|
158
|
+
const declared = Object.getOwnPropertyDescriptor(properties, key)?.value;
|
|
159
|
+
if (declared === undefined) {
|
|
160
|
+
if (typeof extra !== 'object' || extra === null) {
|
|
161
|
+
return new DtoValidationFailure(`${path}.${key}`, `${path}.${key} is not allowed`);
|
|
314
162
|
}
|
|
163
|
+
const failure = this.validateAt(extra, entry, `${path}.${key}`);
|
|
164
|
+
if (failure)
|
|
165
|
+
return failure;
|
|
315
166
|
continue;
|
|
316
167
|
}
|
|
317
|
-
const failure = this.
|
|
168
|
+
const failure = this.validateAt(declared, entry, `${path}.${key}`);
|
|
318
169
|
if (failure)
|
|
319
170
|
return failure;
|
|
320
171
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
// webpieces-disable no-any-unknown -- reflect-metadata returns an erased constructor token
|
|
325
|
-
const reflected = Reflect.getMetadata('design:type', dtoClass.prototype, field.propertyKey);
|
|
326
|
-
const options = field.options;
|
|
327
|
-
if (options instanceof WpDtoMapFieldOptions)
|
|
328
|
-
return this.validateMap(options, value, path);
|
|
329
|
-
const expected = reflected?.name;
|
|
330
|
-
if (expected === 'Array')
|
|
331
|
-
return this.validateArray(options, value, path);
|
|
332
|
-
if (expected === 'String') {
|
|
333
|
-
if (typeof value !== 'string')
|
|
334
|
-
return new DtoValidationFailure(path, `${path} must be a string`);
|
|
335
|
-
if (options.enumValues && !options.enumValues.includes(value)) {
|
|
336
|
-
return new DtoValidationFailure(path, `${path} must be one of: ${options.enumValues.join(', ')}`);
|
|
337
|
-
}
|
|
338
|
-
return undefined;
|
|
339
|
-
}
|
|
340
|
-
if (expected === 'Number') {
|
|
341
|
-
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
342
|
-
return new DtoValidationFailure(path, `${path} must be a number`);
|
|
343
|
-
}
|
|
344
|
-
if (options.integer && !Number.isInteger(value)) {
|
|
345
|
-
return new DtoValidationFailure(path, `${path} must be an integer`);
|
|
172
|
+
for (const name of schema.required ?? []) {
|
|
173
|
+
if (Object.getOwnPropertyDescriptor(value, name)?.value === undefined) {
|
|
174
|
+
return new DtoValidationFailure(`${path}.${name}`, `${path}.${name} is required`);
|
|
346
175
|
}
|
|
347
|
-
if (options.minimum !== undefined && value < options.minimum) {
|
|
348
|
-
return new DtoValidationFailure(path, `${path} must be >= ${options.minimum}`);
|
|
349
|
-
}
|
|
350
|
-
if (options.maximum !== undefined && value > options.maximum) {
|
|
351
|
-
return new DtoValidationFailure(path, `${path} must be <= ${options.maximum}`);
|
|
352
|
-
}
|
|
353
|
-
return undefined;
|
|
354
176
|
}
|
|
355
|
-
|
|
356
|
-
return typeof value === 'boolean'
|
|
357
|
-
? undefined
|
|
358
|
-
: new DtoValidationFailure(path, `${path} must be a boolean`);
|
|
359
|
-
}
|
|
360
|
-
return this.validateAt(reflected, value, path);
|
|
177
|
+
return undefined;
|
|
361
178
|
}
|
|
362
|
-
validateArray(
|
|
363
|
-
if (!Array.isArray(value))
|
|
179
|
+
validateArray(schema, value, path) {
|
|
180
|
+
if (!Array.isArray(value)) {
|
|
364
181
|
return new DtoValidationFailure(path, `${path} must be an array`);
|
|
365
|
-
|
|
366
|
-
|
|
182
|
+
}
|
|
183
|
+
const items = schema.items;
|
|
184
|
+
if (items === undefined)
|
|
185
|
+
return undefined;
|
|
367
186
|
for (let index = 0; index < value.length; index += 1) {
|
|
368
|
-
const failure = this.
|
|
187
|
+
const failure = this.validateAt(items, value[index], `${path}[${index}]`);
|
|
369
188
|
if (failure)
|
|
370
189
|
return failure;
|
|
371
190
|
}
|
|
372
191
|
return undefined;
|
|
373
192
|
}
|
|
374
|
-
|
|
375
|
-
if (typeof value !== '
|
|
376
|
-
return new DtoValidationFailure(path, `${path} must be
|
|
193
|
+
validateString(schema, value, path) {
|
|
194
|
+
if (typeof value !== 'string') {
|
|
195
|
+
return new DtoValidationFailure(path, `${path} must be a string`);
|
|
377
196
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
return new DtoValidationFailure(path, `${path} must be a plain object map`);
|
|
381
|
-
}
|
|
382
|
-
for (const key of Object.keys(value)) {
|
|
383
|
-
const entry = Object.getOwnPropertyDescriptor(value, key)?.value;
|
|
384
|
-
const failure = this.validateElement(options.mapValues, entry, `${path}.${key}`);
|
|
385
|
-
if (failure)
|
|
386
|
-
return failure;
|
|
197
|
+
if (schema.enum && !schema.enum.includes(value)) {
|
|
198
|
+
return new DtoValidationFailure(path, `${path} must be one of: ${schema.enum.join(', ')}`);
|
|
387
199
|
}
|
|
388
200
|
return undefined;
|
|
389
201
|
}
|
|
390
|
-
|
|
391
|
-
if (typeof
|
|
392
|
-
return
|
|
393
|
-
const wanted = type === 'integer' ? 'number' : type;
|
|
394
|
-
if (typeof item !== wanted || (type === 'integer' && !Number.isInteger(item))) {
|
|
395
|
-
return new DtoValidationFailure(path, `${path} must be ${type}`);
|
|
202
|
+
validateNumber(schema, value, path) {
|
|
203
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
204
|
+
return new DtoValidationFailure(path, `${path} must be a number`);
|
|
396
205
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
fields(dtoClass) {
|
|
400
|
-
const fields = Reflect.getMetadata(DTO_FIELDS, dtoClass) ?? {};
|
|
401
|
-
return Object.values(fields);
|
|
402
|
-
}
|
|
403
|
-
requireDto(dtoClass) {
|
|
404
|
-
if (!Reflect.getMetadata(DTO_MARKER, dtoClass)) {
|
|
405
|
-
throw new Error(`${dtoClass.name} must be decorated with @WpDto() for schema generation.`);
|
|
206
|
+
if (ApiJsonSchema.baseTypeOf(schema) === 'integer' && !Number.isInteger(value)) {
|
|
207
|
+
return new DtoValidationFailure(path, `${path} must be an integer`);
|
|
406
208
|
}
|
|
209
|
+
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
210
|
+
return new DtoValidationFailure(path, `${path} must be >= ${schema.minimum}`);
|
|
211
|
+
}
|
|
212
|
+
if (schema.maximum !== undefined && value > schema.maximum) {
|
|
213
|
+
return new DtoValidationFailure(path, `${path} must be <= ${schema.maximum}`);
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
407
216
|
}
|
|
408
217
|
}
|
|
409
|
-
exports.
|
|
218
|
+
exports.ApiJsonSchemaValidator = ApiJsonSchemaValidator;
|
|
410
219
|
//# sourceMappingURL=DtoSchema.js.map
|