@optimizely-opal/opal-tool-ocp-sdk 1.0.0-beta.1 → 1.0.0-beta.10
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/README.md +169 -3
- package/dist/auth/AuthUtils.d.ts +12 -5
- package/dist/auth/AuthUtils.d.ts.map +1 -1
- package/dist/auth/AuthUtils.js +80 -25
- package/dist/auth/AuthUtils.js.map +1 -1
- package/dist/auth/AuthUtils.test.js +161 -117
- package/dist/auth/AuthUtils.test.js.map +1 -1
- package/dist/function/GlobalToolFunction.d.ts +5 -3
- package/dist/function/GlobalToolFunction.d.ts.map +1 -1
- package/dist/function/GlobalToolFunction.js +32 -8
- package/dist/function/GlobalToolFunction.js.map +1 -1
- package/dist/function/GlobalToolFunction.test.js +73 -12
- package/dist/function/GlobalToolFunction.test.js.map +1 -1
- package/dist/function/ToolFunction.d.ts +11 -4
- package/dist/function/ToolFunction.d.ts.map +1 -1
- package/dist/function/ToolFunction.js +45 -9
- package/dist/function/ToolFunction.js.map +1 -1
- package/dist/function/ToolFunction.test.js +278 -11
- package/dist/function/ToolFunction.test.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/logging/ToolLogger.d.ts +42 -0
- package/dist/logging/ToolLogger.d.ts.map +1 -0
- package/dist/logging/ToolLogger.js +255 -0
- package/dist/logging/ToolLogger.js.map +1 -0
- package/dist/logging/ToolLogger.test.d.ts +2 -0
- package/dist/logging/ToolLogger.test.d.ts.map +1 -0
- package/dist/logging/ToolLogger.test.js +864 -0
- package/dist/logging/ToolLogger.test.js.map +1 -0
- package/dist/service/Service.d.ts +88 -2
- package/dist/service/Service.d.ts.map +1 -1
- package/dist/service/Service.js +228 -39
- package/dist/service/Service.js.map +1 -1
- package/dist/service/Service.test.js +558 -22
- package/dist/service/Service.test.js.map +1 -1
- package/dist/types/Models.d.ts +7 -1
- package/dist/types/Models.d.ts.map +1 -1
- package/dist/types/Models.js +5 -1
- package/dist/types/Models.js.map +1 -1
- package/dist/types/ToolError.d.ts +72 -0
- package/dist/types/ToolError.d.ts.map +1 -0
- package/dist/types/ToolError.js +107 -0
- package/dist/types/ToolError.js.map +1 -0
- package/dist/types/ToolError.test.d.ts +2 -0
- package/dist/types/ToolError.test.d.ts.map +1 -0
- package/dist/types/ToolError.test.js +185 -0
- package/dist/types/ToolError.test.js.map +1 -0
- package/dist/validation/ParameterValidator.d.ts +31 -0
- package/dist/validation/ParameterValidator.d.ts.map +1 -0
- package/dist/validation/ParameterValidator.js +129 -0
- package/dist/validation/ParameterValidator.js.map +1 -0
- package/dist/validation/ParameterValidator.test.d.ts +2 -0
- package/dist/validation/ParameterValidator.test.d.ts.map +1 -0
- package/dist/validation/ParameterValidator.test.js +323 -0
- package/dist/validation/ParameterValidator.test.js.map +1 -0
- package/package.json +3 -3
- package/src/auth/AuthUtils.test.ts +176 -157
- package/src/auth/AuthUtils.ts +96 -33
- package/src/function/GlobalToolFunction.test.ts +78 -14
- package/src/function/GlobalToolFunction.ts +46 -11
- package/src/function/ToolFunction.test.ts +298 -13
- package/src/function/ToolFunction.ts +61 -13
- package/src/index.ts +2 -1
- package/src/logging/ToolLogger.test.ts +1020 -0
- package/src/logging/ToolLogger.ts +292 -0
- package/src/service/Service.test.ts +712 -28
- package/src/service/Service.ts +288 -38
- package/src/types/Models.ts +8 -1
- package/src/types/ToolError.test.ts +222 -0
- package/src/types/ToolError.ts +125 -0
- package/src/validation/ParameterValidator.test.ts +371 -0
- package/src/validation/ParameterValidator.ts +150 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation error details
|
|
3
|
+
*/
|
|
4
|
+
export interface ValidationError {
|
|
5
|
+
field: string;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Custom error class for tool functions that supports RFC 9457 Problem Details format
|
|
11
|
+
*
|
|
12
|
+
* The error message includes status code and details for better logging:
|
|
13
|
+
* - No detail: "[status] message"
|
|
14
|
+
* - With detail: "[status] message: detail"
|
|
15
|
+
* - With errors: "[status] message: field1 (error1); field2 (error2)"
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* // Throw a 404 error
|
|
20
|
+
* // Message: "[404] Resource not found: The requested task does not exist"
|
|
21
|
+
* throw new ToolError('Resource not found', 404, 'The requested task does not exist');
|
|
22
|
+
*
|
|
23
|
+
* // Throw a 400 error
|
|
24
|
+
* // Message: "[400] Invalid input: The priority must be "low", "medium", or "high""
|
|
25
|
+
* throw new ToolError('Invalid input', 400, 'The priority must be "low", "medium", or "high"');
|
|
26
|
+
*
|
|
27
|
+
* // Throw a 500 error (default)
|
|
28
|
+
* // Message: "[500] Database connection failed"
|
|
29
|
+
* throw new ToolError('Database connection failed');
|
|
30
|
+
*
|
|
31
|
+
* // Throw a validation error with multiple field errors
|
|
32
|
+
* // Message: "[400] Validation failed: email (Invalid email format); age (Must be a positive number)"
|
|
33
|
+
* throw new ToolError('Validation failed', 400, "See 'errors' field for details.", [
|
|
34
|
+
* { field: 'email', message: 'Invalid email format' },
|
|
35
|
+
* { field: 'age', message: 'Must be a positive number' }
|
|
36
|
+
* ]);
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export class ToolError extends Error {
|
|
40
|
+
/**
|
|
41
|
+
* HTTP status code for the error response
|
|
42
|
+
*/
|
|
43
|
+
public readonly status: number;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Detailed error description (optional)
|
|
47
|
+
*/
|
|
48
|
+
public readonly detail?: string;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Array of validation errors (optional)
|
|
52
|
+
*/
|
|
53
|
+
public readonly errors?: ValidationError[];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The title field for RFC 9457 format (same as message when no detail is provided)
|
|
57
|
+
*/
|
|
58
|
+
public readonly title: string;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create a new ToolError
|
|
62
|
+
*
|
|
63
|
+
* @param message - Error message (used as RFC 9457 "title" field)
|
|
64
|
+
* @param status - HTTP status code (default: 500)
|
|
65
|
+
* @param detail - Detailed error description (optional)
|
|
66
|
+
* @param errors - Array of validation errors (optional)
|
|
67
|
+
*/
|
|
68
|
+
public constructor(
|
|
69
|
+
message: string,
|
|
70
|
+
status: number = 500,
|
|
71
|
+
detail?: string,
|
|
72
|
+
errors?: ValidationError[]
|
|
73
|
+
) {
|
|
74
|
+
// Build comprehensive error message for logging/debugging
|
|
75
|
+
// Format: [status] message: details
|
|
76
|
+
let fullMessage = `[${status}] ${message}`;
|
|
77
|
+
|
|
78
|
+
if (errors && errors.length > 0) {
|
|
79
|
+
// Errors take precedence: field1 (message1); field2 (message2)
|
|
80
|
+
const errorDetails = errors
|
|
81
|
+
.map((err) => `${err.field} (${err.message})`)
|
|
82
|
+
.join('; ');
|
|
83
|
+
fullMessage += `: ${errorDetails}`;
|
|
84
|
+
} else if (detail) {
|
|
85
|
+
// Include detail if no errors
|
|
86
|
+
fullMessage += `: ${detail}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
super(fullMessage);
|
|
90
|
+
this.name = 'ToolError';
|
|
91
|
+
this.title = message;
|
|
92
|
+
this.status = status;
|
|
93
|
+
this.detail = detail;
|
|
94
|
+
this.errors = errors;
|
|
95
|
+
|
|
96
|
+
// Maintains proper stack trace for where error was thrown (V8 engines only)
|
|
97
|
+
if (Error.captureStackTrace) {
|
|
98
|
+
Error.captureStackTrace(this, ToolError);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Convert error to RFC 9457 Problem Details format
|
|
104
|
+
*
|
|
105
|
+
* @param instance - URI reference identifying the specific occurrence of the problem
|
|
106
|
+
* @returns RFC 9457 compliant error object
|
|
107
|
+
*/
|
|
108
|
+
public toProblemDetails(instance: string): Record<string, unknown> {
|
|
109
|
+
const problemDetails: Record<string, unknown> = {
|
|
110
|
+
title: this.title,
|
|
111
|
+
status: this.status,
|
|
112
|
+
instance
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (this.detail) {
|
|
116
|
+
problemDetails.detail = this.detail;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (this.errors && this.errors.length > 0) {
|
|
120
|
+
problemDetails.errors = this.errors;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return problemDetails;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { ParameterValidator } from './ParameterValidator';
|
|
2
|
+
import { Parameter, ParameterType } from '../types/Models';
|
|
3
|
+
import { ToolError } from '../types/ToolError';
|
|
4
|
+
|
|
5
|
+
describe('ParameterValidator', () => {
|
|
6
|
+
describe('validate', () => {
|
|
7
|
+
it('should pass validation for valid parameters', () => {
|
|
8
|
+
const paramDefs = [
|
|
9
|
+
new Parameter('name', ParameterType.String, 'User name', true),
|
|
10
|
+
new Parameter('age', ParameterType.Integer, 'User age', false),
|
|
11
|
+
new Parameter('score', ParameterType.Number, 'User score', false),
|
|
12
|
+
new Parameter('active', ParameterType.Boolean, 'Is active', false),
|
|
13
|
+
new Parameter('tags', ParameterType.List, 'User tags', false),
|
|
14
|
+
new Parameter('config', ParameterType.Dictionary, 'User config', false)
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const validParams = {
|
|
18
|
+
name: 'John Doe',
|
|
19
|
+
age: 25,
|
|
20
|
+
score: 85.5,
|
|
21
|
+
active: true,
|
|
22
|
+
tags: ['admin', 'user'],
|
|
23
|
+
config: { theme: 'dark', language: 'en' }
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
expect(() => {
|
|
27
|
+
ParameterValidator.validate(validParams, paramDefs, '/test-endpoint');
|
|
28
|
+
}).not.toThrow();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should throw ToolError for missing required parameters', () => {
|
|
32
|
+
const paramDefs = [
|
|
33
|
+
new Parameter('name', ParameterType.String, 'User name', true),
|
|
34
|
+
new Parameter('email', ParameterType.String, 'User email', true)
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const params = {
|
|
38
|
+
name: 'John Doe'
|
|
39
|
+
// email is missing
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
expect(() => {
|
|
43
|
+
ParameterValidator.validate(params, paramDefs, '/test-endpoint');
|
|
44
|
+
}).toThrow(ToolError);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
ParameterValidator.validate(params, paramDefs, '/test-endpoint');
|
|
48
|
+
} catch (error) {
|
|
49
|
+
expect(error).toBeInstanceOf(ToolError);
|
|
50
|
+
const toolError = error as ToolError;
|
|
51
|
+
expect(toolError.status).toBe(400);
|
|
52
|
+
expect(toolError.message).toBe(
|
|
53
|
+
"[400] One or more validation errors occurred.: email (Required parameter 'email' is missing)"
|
|
54
|
+
);
|
|
55
|
+
expect(toolError.title).toBe('One or more validation errors occurred.');
|
|
56
|
+
expect(toolError.detail).toBe("See 'errors' field for details.");
|
|
57
|
+
expect(toolError.errors).toHaveLength(1);
|
|
58
|
+
expect(toolError.errors).toEqual([{
|
|
59
|
+
field: 'email',
|
|
60
|
+
message: "Required parameter 'email' is missing"
|
|
61
|
+
}]);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should throw ToolError for wrong parameter types', () => {
|
|
66
|
+
const paramDefs = [
|
|
67
|
+
new Parameter('name', ParameterType.String, 'User name', true),
|
|
68
|
+
new Parameter('age', ParameterType.Integer, 'User age', true),
|
|
69
|
+
new Parameter('active', ParameterType.Boolean, 'Is active', true),
|
|
70
|
+
new Parameter('tags', ParameterType.List, 'User tags', true),
|
|
71
|
+
new Parameter('config', ParameterType.Dictionary, 'User config', true)
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const invalidParams = {
|
|
75
|
+
name: 123, // should be string
|
|
76
|
+
age: '25', // should be integer
|
|
77
|
+
active: 'true', // should be boolean
|
|
78
|
+
tags: 'tag1,tag2', // should be array
|
|
79
|
+
config: 'invalid' // should be object
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
ParameterValidator.validate(invalidParams, paramDefs, '/test-endpoint');
|
|
84
|
+
fail('Should have thrown ToolError');
|
|
85
|
+
} catch (error) {
|
|
86
|
+
expect(error).toBeInstanceOf(ToolError);
|
|
87
|
+
const toolError = error as ToolError;
|
|
88
|
+
expect(toolError.status).toBe(400);
|
|
89
|
+
expect(toolError.errors).toHaveLength(5);
|
|
90
|
+
|
|
91
|
+
expect(toolError.errors![0].field).toBe('name');
|
|
92
|
+
expect(toolError.errors![0].message).toContain('must be a string');
|
|
93
|
+
|
|
94
|
+
expect(toolError.errors![1].field).toBe('age');
|
|
95
|
+
expect(toolError.errors![1].message).toContain('must be an integer');
|
|
96
|
+
|
|
97
|
+
expect(toolError.errors![2].field).toBe('active');
|
|
98
|
+
expect(toolError.errors![2].message).toContain('must be a boolean');
|
|
99
|
+
|
|
100
|
+
expect(toolError.errors![3].field).toBe('tags');
|
|
101
|
+
expect(toolError.errors![3].message).toContain('must be an array');
|
|
102
|
+
|
|
103
|
+
expect(toolError.errors![4].field).toBe('config');
|
|
104
|
+
expect(toolError.errors![4].message).toContain('must be an object');
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should throw ToolError when params is null with required parameters', () => {
|
|
109
|
+
const paramDefs = [
|
|
110
|
+
new Parameter('required', ParameterType.String, 'Required param', true),
|
|
111
|
+
new Parameter('optional', ParameterType.String, 'Optional param', false)
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
ParameterValidator.validate(null, paramDefs, '/test-endpoint');
|
|
116
|
+
fail('Should have thrown ToolError');
|
|
117
|
+
} catch (error) {
|
|
118
|
+
expect(error).toBeInstanceOf(ToolError);
|
|
119
|
+
const toolError = error as ToolError;
|
|
120
|
+
expect(toolError.errors).toHaveLength(1);
|
|
121
|
+
expect(toolError.errors![0].field).toBe('required');
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should throw ToolError when params is undefined with required parameters', () => {
|
|
126
|
+
const paramDefs = [
|
|
127
|
+
new Parameter('required', ParameterType.String, 'Required param', true),
|
|
128
|
+
new Parameter('optional', ParameterType.String, 'Optional param', false)
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
ParameterValidator.validate(undefined, paramDefs, '/test-endpoint');
|
|
133
|
+
fail('Should have thrown ToolError');
|
|
134
|
+
} catch (error) {
|
|
135
|
+
expect(error).toBeInstanceOf(ToolError);
|
|
136
|
+
const toolError = error as ToolError;
|
|
137
|
+
expect(toolError.errors).toHaveLength(1);
|
|
138
|
+
expect(toolError.errors![0].field).toBe('required');
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should allow optional parameters to be missing', () => {
|
|
143
|
+
const paramDefs = [
|
|
144
|
+
new Parameter('name', ParameterType.String, 'User name', true),
|
|
145
|
+
new Parameter('age', ParameterType.Integer, 'User age', false)
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
const params = {
|
|
149
|
+
name: 'John Doe'
|
|
150
|
+
// age is optional and missing
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
expect(() => {
|
|
154
|
+
ParameterValidator.validate(params, paramDefs, '/test-endpoint');
|
|
155
|
+
}).not.toThrow();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should distinguish between integer and number types', () => {
|
|
159
|
+
const paramDefs = [
|
|
160
|
+
new Parameter('count', ParameterType.Integer, 'Item count', true),
|
|
161
|
+
new Parameter('score', ParameterType.Number, 'Score value', true)
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
const params1 = {
|
|
165
|
+
count: 25.5, // should be integer, not float
|
|
166
|
+
score: 85.5 // number is fine
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
ParameterValidator.validate(params1, paramDefs, '/test-endpoint');
|
|
171
|
+
fail('Should have thrown ToolError');
|
|
172
|
+
} catch (error) {
|
|
173
|
+
expect(error).toBeInstanceOf(ToolError);
|
|
174
|
+
const toolError = error as ToolError;
|
|
175
|
+
expect(toolError.errors).toHaveLength(1);
|
|
176
|
+
expect(toolError.errors![0].field).toBe('count');
|
|
177
|
+
expect(toolError.errors![0].message).toContain('must be an integer');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const params2 = {
|
|
181
|
+
count: 25, // integer is fine
|
|
182
|
+
score: 85.5 // number is fine
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
expect(() => {
|
|
186
|
+
ParameterValidator.validate(params2, paramDefs, '/test-endpoint');
|
|
187
|
+
}).not.toThrow();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should handle array vs object distinction', () => {
|
|
191
|
+
const paramDefs = [
|
|
192
|
+
new Parameter('tags', ParameterType.List, 'Tags', true),
|
|
193
|
+
new Parameter('config', ParameterType.Dictionary, 'Config', true)
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
const params = {
|
|
197
|
+
tags: { key: 'value' }, // should be array, not object
|
|
198
|
+
config: ['item1', 'item2'] // should be object, not array
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
ParameterValidator.validate(params, paramDefs, '/test-endpoint');
|
|
203
|
+
fail('Should have thrown ToolError');
|
|
204
|
+
} catch (error) {
|
|
205
|
+
expect(error).toBeInstanceOf(ToolError);
|
|
206
|
+
const toolError = error as ToolError;
|
|
207
|
+
expect(toolError.errors).toHaveLength(2);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should handle null values for optional parameters', () => {
|
|
212
|
+
const paramDefs = [
|
|
213
|
+
new Parameter('name', ParameterType.String, 'User name', true),
|
|
214
|
+
new Parameter('nickname', ParameterType.String, 'Nickname', false)
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
const params = {
|
|
218
|
+
name: 'John Doe',
|
|
219
|
+
nickname: null // optional param can be null
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
expect(() => {
|
|
223
|
+
ParameterValidator.validate(params, paramDefs, '/test-endpoint');
|
|
224
|
+
}).not.toThrow();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should handle edge cases for number validation', () => {
|
|
228
|
+
const paramDefs = [
|
|
229
|
+
new Parameter('value', ParameterType.Number, 'Number value', true)
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
// NaN should fail
|
|
233
|
+
try {
|
|
234
|
+
ParameterValidator.validate({ value: NaN }, paramDefs, '/test-endpoint');
|
|
235
|
+
fail('Should have thrown ToolError for NaN');
|
|
236
|
+
} catch (error) {
|
|
237
|
+
expect(error).toBeInstanceOf(ToolError);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Infinity should pass (it's a number)
|
|
241
|
+
expect(() => {
|
|
242
|
+
ParameterValidator.validate({ value: Infinity }, paramDefs, '/test-endpoint');
|
|
243
|
+
}).not.toThrow();
|
|
244
|
+
|
|
245
|
+
// Negative numbers should pass
|
|
246
|
+
expect(() => {
|
|
247
|
+
ParameterValidator.validate({ value: -42.5 }, paramDefs, '/test-endpoint');
|
|
248
|
+
}).not.toThrow();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should handle edge cases for integer validation', () => {
|
|
252
|
+
const paramDefs = [
|
|
253
|
+
new Parameter('count', ParameterType.Integer, 'Count', true)
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
// Zero should pass
|
|
257
|
+
expect(() => {
|
|
258
|
+
ParameterValidator.validate({ count: 0 }, paramDefs, '/test-endpoint');
|
|
259
|
+
}).not.toThrow();
|
|
260
|
+
|
|
261
|
+
// Negative integer should pass
|
|
262
|
+
expect(() => {
|
|
263
|
+
ParameterValidator.validate({ count: -10 }, paramDefs, '/test-endpoint');
|
|
264
|
+
}).not.toThrow();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should handle empty arrays and objects', () => {
|
|
268
|
+
const paramDefs = [
|
|
269
|
+
new Parameter('tags', ParameterType.List, 'Tags', true),
|
|
270
|
+
new Parameter('config', ParameterType.Dictionary, 'Config', true)
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
const params = {
|
|
274
|
+
tags: [],
|
|
275
|
+
config: {}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
expect(() => {
|
|
279
|
+
ParameterValidator.validate(params, paramDefs, '/test-endpoint');
|
|
280
|
+
}).not.toThrow();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('should handle special string values', () => {
|
|
284
|
+
const paramDefs = [
|
|
285
|
+
new Parameter('value', ParameterType.String, 'String value', true)
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
// Empty string should pass
|
|
289
|
+
expect(() => {
|
|
290
|
+
ParameterValidator.validate({ value: '' }, paramDefs, '/test-endpoint');
|
|
291
|
+
}).not.toThrow();
|
|
292
|
+
|
|
293
|
+
// String with only whitespace should pass
|
|
294
|
+
expect(() => {
|
|
295
|
+
ParameterValidator.validate({ value: ' ' }, paramDefs, '/test-endpoint');
|
|
296
|
+
}).not.toThrow();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should collect multiple validation errors', () => {
|
|
300
|
+
const paramDefs = [
|
|
301
|
+
new Parameter('name', ParameterType.String, 'Name', true),
|
|
302
|
+
new Parameter('age', ParameterType.Integer, 'Age', true),
|
|
303
|
+
new Parameter('email', ParameterType.String, 'Email', true)
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
const params = {
|
|
307
|
+
// name is missing
|
|
308
|
+
age: 'invalid' // wrong type
|
|
309
|
+
// email is missing
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
ParameterValidator.validate(params, paramDefs, '/test-endpoint');
|
|
314
|
+
fail('Should have thrown ToolError');
|
|
315
|
+
} catch (error) {
|
|
316
|
+
expect(error).toBeInstanceOf(ToolError);
|
|
317
|
+
const toolError = error as ToolError;
|
|
318
|
+
expect(toolError.errors).toHaveLength(3);
|
|
319
|
+
|
|
320
|
+
expect(toolError.errors!.some((e) => e.field === 'name')).toBe(true);
|
|
321
|
+
expect(toolError.errors!.some((e) => e.field === 'age')).toBe(true);
|
|
322
|
+
expect(toolError.errors!.some((e) => e.field === 'email')).toBe(true);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should handle tools with no parameter definitions', () => {
|
|
327
|
+
expect(() => {
|
|
328
|
+
ParameterValidator.validate({ anyParam: 'value' }, [], '/test-endpoint');
|
|
329
|
+
}).not.toThrow();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('should handle extra parameters not in definition', () => {
|
|
333
|
+
const paramDefs = [
|
|
334
|
+
new Parameter('name', ParameterType.String, 'Name', true)
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
const params = {
|
|
338
|
+
name: 'John',
|
|
339
|
+
extraParam: 'ignored', // not in definition
|
|
340
|
+
anotherExtra: 123
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// Extra parameters should be ignored
|
|
344
|
+
expect(() => {
|
|
345
|
+
ParameterValidator.validate(params, paramDefs, '/test-endpoint');
|
|
346
|
+
}).not.toThrow();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('should handle nested objects and arrays', () => {
|
|
350
|
+
const paramDefs = [
|
|
351
|
+
new Parameter('config', ParameterType.Dictionary, 'Config', true),
|
|
352
|
+
new Parameter('items', ParameterType.List, 'Items', true)
|
|
353
|
+
];
|
|
354
|
+
|
|
355
|
+
const params = {
|
|
356
|
+
config: {
|
|
357
|
+
nested: {
|
|
358
|
+
deeply: {
|
|
359
|
+
nested: 'value'
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
items: [1, 2, [3, 4, [5, 6]]]
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
expect(() => {
|
|
367
|
+
ParameterValidator.validate(params, paramDefs, '/test-endpoint');
|
|
368
|
+
}).not.toThrow();
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Parameter, ParameterType } from '../types/Models';
|
|
2
|
+
import { ToolError, ValidationError } from '../types/ToolError';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parameter validator for tool inputs
|
|
6
|
+
*/
|
|
7
|
+
export class ParameterValidator {
|
|
8
|
+
/**
|
|
9
|
+
* Validate parameters against their definitions
|
|
10
|
+
* Throws ToolError with status 400 if validation fails
|
|
11
|
+
*
|
|
12
|
+
* @param params The actual parameters received
|
|
13
|
+
* @param parameterDefinitions The expected parameter definitions
|
|
14
|
+
* @param _endpoint The endpoint being validated (reserved for future use)
|
|
15
|
+
* @throws {ToolError} When validation fails
|
|
16
|
+
*/
|
|
17
|
+
public static validate(
|
|
18
|
+
params: any,
|
|
19
|
+
parameterDefinitions: Parameter[],
|
|
20
|
+
_endpoint: string
|
|
21
|
+
): void {
|
|
22
|
+
const errors: ValidationError[] = [];
|
|
23
|
+
|
|
24
|
+
// Validate each defined parameter
|
|
25
|
+
for (const paramDef of parameterDefinitions) {
|
|
26
|
+
const value = params ? params[paramDef.name] : undefined;
|
|
27
|
+
|
|
28
|
+
// Check if required parameter is missing
|
|
29
|
+
if (paramDef.required && (value === undefined || value === null)) {
|
|
30
|
+
errors.push({
|
|
31
|
+
field: paramDef.name,
|
|
32
|
+
message: `Required parameter '${paramDef.name}' is missing`
|
|
33
|
+
});
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Skip validation if parameter is optional and not provided
|
|
38
|
+
if (!paramDef.required && (value === undefined || value === null)) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Validate parameter type
|
|
43
|
+
const typeError = this.validateParameterType(paramDef.name, value, paramDef.type);
|
|
44
|
+
if (typeError) {
|
|
45
|
+
errors.push(typeError);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Throw ToolError if validation failed
|
|
50
|
+
if (errors.length > 0) {
|
|
51
|
+
throw new ToolError(
|
|
52
|
+
'One or more validation errors occurred.',
|
|
53
|
+
400,
|
|
54
|
+
"See 'errors' field for details.",
|
|
55
|
+
errors
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Validate a single parameter's type
|
|
62
|
+
* @param paramName Parameter name
|
|
63
|
+
* @param value Parameter value
|
|
64
|
+
* @param expectedType Expected parameter type
|
|
65
|
+
* @returns Validation error if invalid, null if valid
|
|
66
|
+
*/
|
|
67
|
+
private static validateParameterType(
|
|
68
|
+
paramName: string,
|
|
69
|
+
value: any,
|
|
70
|
+
expectedType: ParameterType
|
|
71
|
+
): ValidationError | null {
|
|
72
|
+
const actualType = this.getActualType(value);
|
|
73
|
+
|
|
74
|
+
switch (expectedType) {
|
|
75
|
+
case ParameterType.String:
|
|
76
|
+
if (typeof value !== 'string') {
|
|
77
|
+
return {
|
|
78
|
+
field: paramName,
|
|
79
|
+
message: `Parameter '${paramName}' must be a string, but received ${actualType}`
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
|
|
84
|
+
case ParameterType.Integer:
|
|
85
|
+
if (!Number.isInteger(value)) {
|
|
86
|
+
return {
|
|
87
|
+
field: paramName,
|
|
88
|
+
message: `Parameter '${paramName}' must be an integer, but received ${actualType}`
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
|
|
93
|
+
case ParameterType.Number:
|
|
94
|
+
if (typeof value !== 'number' || isNaN(value)) {
|
|
95
|
+
return {
|
|
96
|
+
field: paramName,
|
|
97
|
+
message: `Parameter '${paramName}' must be a number, but received ${actualType}`
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
|
|
102
|
+
case ParameterType.Boolean:
|
|
103
|
+
if (typeof value !== 'boolean') {
|
|
104
|
+
return {
|
|
105
|
+
field: paramName,
|
|
106
|
+
message: `Parameter '${paramName}' must be a boolean, but received ${actualType}`
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
|
|
111
|
+
case ParameterType.List:
|
|
112
|
+
if (!Array.isArray(value)) {
|
|
113
|
+
return {
|
|
114
|
+
field: paramName,
|
|
115
|
+
message: `Parameter '${paramName}' must be an array, but received ${actualType}`
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
case ParameterType.Dictionary:
|
|
121
|
+
if (typeof value !== 'object' || Array.isArray(value) || value === null) {
|
|
122
|
+
return {
|
|
123
|
+
field: paramName,
|
|
124
|
+
message: `Parameter '${paramName}' must be an object, but received ${actualType}`
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
|
|
129
|
+
default:
|
|
130
|
+
return {
|
|
131
|
+
field: paramName,
|
|
132
|
+
message: `Parameter '${paramName}' has unknown expected type: ${String(expectedType)}`
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get a human-readable description of the actual type
|
|
141
|
+
* @param value The value to check
|
|
142
|
+
* @returns String description of the type
|
|
143
|
+
*/
|
|
144
|
+
private static getActualType(value: any): string {
|
|
145
|
+
if (value === null) return 'null';
|
|
146
|
+
if (value === undefined) return 'undefined';
|
|
147
|
+
if (Array.isArray(value)) return 'array';
|
|
148
|
+
return typeof value;
|
|
149
|
+
}
|
|
150
|
+
}
|