@terreno/api 0.0.18 → 0.1.0
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/.claude/CLAUDE.local.md +204 -0
- package/.cursor/rules/00-root.mdc +338 -0
- package/.github/copilot-instructions.md +333 -0
- package/AGENTS.md +23 -3
- package/README.md +73 -3
- package/dist/api.d.ts +68 -1
- package/dist/api.js +139 -4
- package/dist/api.test.js +906 -2
- package/dist/auth.js +3 -1
- package/dist/errors.js +14 -11
- package/dist/example.js +7 -7
- package/dist/githubAuth.test.js +3 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/openApi.test.js +8 -5
- package/dist/openApiBuilder.d.ts +69 -1
- package/dist/openApiBuilder.js +109 -5
- package/dist/openApiValidator.d.ts +296 -0
- package/dist/openApiValidator.js +698 -0
- package/dist/openApiValidator.test.d.ts +1 -0
- package/dist/openApiValidator.test.js +346 -0
- package/dist/plugins.test.js +3 -3
- package/dist/terrenoPlugin.d.ts +4 -0
- package/dist/terrenoPlugin.js +2 -0
- package/dist/tests.js +34 -24
- package/package.json +4 -1
- package/src/__snapshots__/openApi.test.ts.snap +399 -0
- package/src/__snapshots__/openApiBuilder.test.ts.snap +108 -0
- package/src/api.test.ts +743 -2
- package/src/api.ts +209 -3
- package/src/auth.ts +3 -1
- package/src/errors.ts +14 -11
- package/src/example.ts +7 -7
- package/src/githubAuth.test.ts +3 -3
- package/src/index.ts +2 -0
- package/src/openApi.test.ts +8 -5
- package/src/openApiBuilder.ts +188 -15
- package/src/openApiValidator.test.ts +241 -0
- package/src/openApiValidator.ts +860 -0
- package/src/plugins.test.ts +3 -3
- package/src/terrenoPlugin.ts +5 -0
- package/src/tests.ts +34 -24
- package/.cursorrules +0 -107
- package/.windsurfrules +0 -107
- package/dist/response.d.ts +0 -0
- package/dist/response.js +0 -1
- package/index.ts +0 -1
- package/src/response.ts +0 -0
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* OpenAPI Request Validator
|
|
4
|
+
*
|
|
5
|
+
* Provides runtime validation of incoming requests against OpenAPI schemas.
|
|
6
|
+
* Uses AJV for JSON Schema validation with OpenAPI-compatible settings.
|
|
7
|
+
*
|
|
8
|
+
* Validation is always installed as middleware but only activates after
|
|
9
|
+
* `configureOpenApiValidator()` is called. This makes it safe to include
|
|
10
|
+
* in modelRouter by default.
|
|
11
|
+
*
|
|
12
|
+
* @module openApiValidator
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* // Enable validation globally at server startup
|
|
19
|
+
* configureOpenApiValidator({
|
|
20
|
+
* removeAdditional: true,
|
|
21
|
+
* onAdditionalPropertiesRemoved: (props, req) => {
|
|
22
|
+
* logger.warn(`Stripped: ${props.join(", ")} on ${req.method} ${req.path}`);
|
|
23
|
+
* },
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* // modelRouter automatically validates when configured
|
|
27
|
+
* modelRouter(Todo, {
|
|
28
|
+
* permissions: {...},
|
|
29
|
+
* validation: {
|
|
30
|
+
* validateCreate: true,
|
|
31
|
+
* validateUpdate: true,
|
|
32
|
+
* validateQuery: true,
|
|
33
|
+
* },
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
var __assign = (this && this.__assign) || function () {
|
|
38
|
+
__assign = Object.assign || function(t) {
|
|
39
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
40
|
+
s = arguments[i];
|
|
41
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
42
|
+
t[p] = s[p];
|
|
43
|
+
}
|
|
44
|
+
return t;
|
|
45
|
+
};
|
|
46
|
+
return __assign.apply(this, arguments);
|
|
47
|
+
};
|
|
48
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
49
|
+
var t = {};
|
|
50
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
51
|
+
t[p] = s[p];
|
|
52
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
53
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
54
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
55
|
+
t[p[i]] = s[p[i]];
|
|
56
|
+
}
|
|
57
|
+
return t;
|
|
58
|
+
};
|
|
59
|
+
var __values = (this && this.__values) || function(o) {
|
|
60
|
+
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
|
|
61
|
+
if (m) return m.call(o);
|
|
62
|
+
if (o && typeof o.length === "number") return {
|
|
63
|
+
next: function () {
|
|
64
|
+
if (o && i >= o.length) o = void 0;
|
|
65
|
+
return { value: o && o[i++], done: !o };
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
|
|
69
|
+
};
|
|
70
|
+
var __read = (this && this.__read) || function (o, n) {
|
|
71
|
+
var m = typeof Symbol === "function" && o[Symbol.iterator];
|
|
72
|
+
if (!m) return o;
|
|
73
|
+
var i = m.call(o), r, ar = [], e;
|
|
74
|
+
try {
|
|
75
|
+
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
|
|
76
|
+
}
|
|
77
|
+
catch (error) { e = { error: error }; }
|
|
78
|
+
finally {
|
|
79
|
+
try {
|
|
80
|
+
if (r && !r.done && (m = i["return"])) m.call(i);
|
|
81
|
+
}
|
|
82
|
+
finally { if (e) throw e.error; }
|
|
83
|
+
}
|
|
84
|
+
return ar;
|
|
85
|
+
};
|
|
86
|
+
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
87
|
+
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
88
|
+
if (ar || !(i in from)) {
|
|
89
|
+
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
90
|
+
ar[i] = from[i];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return to.concat(ar || Array.prototype.slice.call(from));
|
|
94
|
+
};
|
|
95
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
96
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
97
|
+
};
|
|
98
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
99
|
+
exports.isOpenApiValidatorConfigured = isOpenApiValidatorConfigured;
|
|
100
|
+
exports.configureOpenApiValidator = configureOpenApiValidator;
|
|
101
|
+
exports.getOpenApiValidatorConfig = getOpenApiValidatorConfig;
|
|
102
|
+
exports.resetOpenApiValidatorConfig = resetOpenApiValidatorConfig;
|
|
103
|
+
exports.validateRequestBody = validateRequestBody;
|
|
104
|
+
exports.validateQueryParams = validateQueryParams;
|
|
105
|
+
exports.createValidator = createValidator;
|
|
106
|
+
exports.validateResponseData = validateResponseData;
|
|
107
|
+
exports.getSchemaFromModel = getSchemaFromModel;
|
|
108
|
+
exports.validateModelRequestBody = validateModelRequestBody;
|
|
109
|
+
exports.createModelValidators = createModelValidators;
|
|
110
|
+
exports.buildQuerySchemaFromFields = buildQuerySchemaFromFields;
|
|
111
|
+
var ajv_1 = __importDefault(require("ajv"));
|
|
112
|
+
var ajv_formats_1 = __importDefault(require("ajv-formats"));
|
|
113
|
+
var mongoose_to_swagger_1 = __importDefault(require("mongoose-to-swagger"));
|
|
114
|
+
var errors_1 = require("./errors");
|
|
115
|
+
var logger_1 = require("./logger");
|
|
116
|
+
// Whether configureOpenApiValidator() has been called
|
|
117
|
+
var isConfigured = false;
|
|
118
|
+
// Global validator configuration - can be modified at runtime
|
|
119
|
+
var globalConfig = {
|
|
120
|
+
coerceTypes: true,
|
|
121
|
+
logValidationErrors: true,
|
|
122
|
+
removeAdditional: true,
|
|
123
|
+
validateRequests: true,
|
|
124
|
+
validateResponses: false,
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Check whether `configureOpenApiValidator()` has been called.
|
|
128
|
+
* Validation middleware is a no-op when this returns false.
|
|
129
|
+
*/
|
|
130
|
+
function isOpenApiValidatorConfigured() {
|
|
131
|
+
return isConfigured;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Configure the global OpenAPI validator settings.
|
|
135
|
+
* Calling this function activates validation — middleware that was previously
|
|
136
|
+
* installed as a no-op will begin validating requests.
|
|
137
|
+
*
|
|
138
|
+
* @param config - Configuration options to merge with existing config
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```typescript
|
|
142
|
+
* configureOpenApiValidator({
|
|
143
|
+
* removeAdditional: true,
|
|
144
|
+
* onAdditionalPropertiesRemoved: (props, req) => {
|
|
145
|
+
* Sentry.captureMessage(`Stripped: ${props.join(", ")} on ${req.method} ${req.path}`);
|
|
146
|
+
* },
|
|
147
|
+
* });
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
function configureOpenApiValidator(config) {
|
|
151
|
+
if (config === void 0) { config = {}; }
|
|
152
|
+
isConfigured = true;
|
|
153
|
+
globalConfig = __assign(__assign({}, globalConfig), config);
|
|
154
|
+
// Clear cached AJV instances so new config takes effect
|
|
155
|
+
ajvCache.clear();
|
|
156
|
+
validatorCache.clear();
|
|
157
|
+
logger_1.logger.debug("OpenAPI validator configured: ".concat(JSON.stringify(globalConfig)));
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get the current global validator configuration.
|
|
161
|
+
*/
|
|
162
|
+
function getOpenApiValidatorConfig() {
|
|
163
|
+
return __assign({}, globalConfig);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Reset the global validator configuration to defaults.
|
|
167
|
+
* Also resets `isConfigured` to false.
|
|
168
|
+
* Useful for testing.
|
|
169
|
+
*/
|
|
170
|
+
function resetOpenApiValidatorConfig() {
|
|
171
|
+
isConfigured = false;
|
|
172
|
+
globalConfig = {
|
|
173
|
+
coerceTypes: true,
|
|
174
|
+
logValidationErrors: true,
|
|
175
|
+
removeAdditional: true,
|
|
176
|
+
validateRequests: true,
|
|
177
|
+
validateResponses: false,
|
|
178
|
+
};
|
|
179
|
+
ajvCache.clear();
|
|
180
|
+
validatorCache.clear();
|
|
181
|
+
}
|
|
182
|
+
// Lazy AJV instance cache keyed by coerceTypes + removeAdditional
|
|
183
|
+
var ajvCache = new Map();
|
|
184
|
+
/**
|
|
185
|
+
* Get or create an AJV instance with the current config settings.
|
|
186
|
+
*/
|
|
187
|
+
function getAjvInstance() {
|
|
188
|
+
var _a, _b, _c, _d;
|
|
189
|
+
var key = "coerce:".concat((_a = globalConfig.coerceTypes) !== null && _a !== void 0 ? _a : true, ",remove:").concat((_b = globalConfig.removeAdditional) !== null && _b !== void 0 ? _b : true);
|
|
190
|
+
var instance = ajvCache.get(key);
|
|
191
|
+
if (!instance) {
|
|
192
|
+
instance = new ajv_1.default({
|
|
193
|
+
allErrors: true,
|
|
194
|
+
coerceTypes: (_c = globalConfig.coerceTypes) !== null && _c !== void 0 ? _c : true,
|
|
195
|
+
removeAdditional: (_d = globalConfig.removeAdditional) !== null && _d !== void 0 ? _d : true,
|
|
196
|
+
strict: false,
|
|
197
|
+
useDefaults: true,
|
|
198
|
+
validateSchema: false,
|
|
199
|
+
});
|
|
200
|
+
(0, ajv_formats_1.default)(instance);
|
|
201
|
+
ajvCache.set(key, instance);
|
|
202
|
+
}
|
|
203
|
+
return instance;
|
|
204
|
+
}
|
|
205
|
+
// Cache compiled validators by schema hash + config key
|
|
206
|
+
var validatorCache = new Map();
|
|
207
|
+
/**
|
|
208
|
+
* Generate a simple hash for a schema to use as a cache key.
|
|
209
|
+
*/
|
|
210
|
+
function hashSchema(schema) {
|
|
211
|
+
return JSON.stringify(schema);
|
|
212
|
+
}
|
|
213
|
+
var VALID_JSON_SCHEMA_TYPES = new Set([
|
|
214
|
+
"string",
|
|
215
|
+
"number",
|
|
216
|
+
"integer",
|
|
217
|
+
"boolean",
|
|
218
|
+
"array",
|
|
219
|
+
"object",
|
|
220
|
+
"null",
|
|
221
|
+
]);
|
|
222
|
+
// mongoose-to-swagger emits non-standard type strings for some Mongoose types
|
|
223
|
+
var MONGOOSE_TYPE_MAP = {
|
|
224
|
+
dateonly: { format: "date", type: "string" },
|
|
225
|
+
schemaobjectid: { type: "string" },
|
|
226
|
+
};
|
|
227
|
+
/**
|
|
228
|
+
* Recursively replace non-standard mongoose-to-swagger types with valid JSON Schema types
|
|
229
|
+
* so AJV can compile the schema.
|
|
230
|
+
*/
|
|
231
|
+
function sanitizeSchemaForAjv(schema) {
|
|
232
|
+
var e_1, _a;
|
|
233
|
+
if (!schema || typeof schema !== "object") {
|
|
234
|
+
return schema;
|
|
235
|
+
}
|
|
236
|
+
var result = __assign({}, schema);
|
|
237
|
+
if (typeof result.type === "string" && !VALID_JSON_SCHEMA_TYPES.has(result.type)) {
|
|
238
|
+
var mapped = MONGOOSE_TYPE_MAP[result.type];
|
|
239
|
+
if (mapped) {
|
|
240
|
+
result.type = mapped.type;
|
|
241
|
+
if (mapped.format && !result.format) {
|
|
242
|
+
result.format = mapped.format;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
result.type = "string";
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (result.items && typeof result.items === "object") {
|
|
250
|
+
result.items = sanitizeSchemaForAjv(result.items);
|
|
251
|
+
}
|
|
252
|
+
if (result.properties && typeof result.properties === "object") {
|
|
253
|
+
var sanitizedProps = {};
|
|
254
|
+
try {
|
|
255
|
+
for (var _b = __values(Object.entries(result.properties)), _c = _b.next(); !_c.done; _c = _b.next()) {
|
|
256
|
+
var _d = __read(_c.value, 2), key = _d[0], value = _d[1];
|
|
257
|
+
sanitizedProps[key] =
|
|
258
|
+
typeof value === "object" && value !== null
|
|
259
|
+
? sanitizeSchemaForAjv(value)
|
|
260
|
+
: value;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
264
|
+
finally {
|
|
265
|
+
try {
|
|
266
|
+
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
|
|
267
|
+
}
|
|
268
|
+
finally { if (e_1) throw e_1.error; }
|
|
269
|
+
}
|
|
270
|
+
result.properties = sanitizedProps;
|
|
271
|
+
}
|
|
272
|
+
if (result.additionalProperties && typeof result.additionalProperties === "object") {
|
|
273
|
+
result.additionalProperties = sanitizeSchemaForAjv(result.additionalProperties);
|
|
274
|
+
}
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Get or create a compiled validator for a schema.
|
|
279
|
+
* Uses the current config so changes take effect on next call.
|
|
280
|
+
* Sanitizes non-standard mongoose-to-swagger types before compilation.
|
|
281
|
+
* Returns null if the schema still cannot be compiled after sanitization.
|
|
282
|
+
*/
|
|
283
|
+
function getValidator(schema) {
|
|
284
|
+
var _a, _b;
|
|
285
|
+
var ajv = getAjvInstance();
|
|
286
|
+
var configKey = "coerce:".concat((_a = globalConfig.coerceTypes) !== null && _a !== void 0 ? _a : true, ",remove:").concat((_b = globalConfig.removeAdditional) !== null && _b !== void 0 ? _b : true);
|
|
287
|
+
var hash = "".concat(configKey, ":").concat(hashSchema(schema));
|
|
288
|
+
var cached = validatorCache.get(hash);
|
|
289
|
+
if (cached !== undefined) {
|
|
290
|
+
return cached;
|
|
291
|
+
}
|
|
292
|
+
var sanitized = sanitizeSchemaForAjv(schema);
|
|
293
|
+
try {
|
|
294
|
+
var validator = ajv.compile(sanitized);
|
|
295
|
+
validatorCache.set(hash, validator);
|
|
296
|
+
return validator;
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
logger_1.logger.debug("Could not compile validation schema after sanitization: ".concat(err.message));
|
|
300
|
+
validatorCache.set(hash, null);
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Format AJV errors into a human-readable string.
|
|
306
|
+
*/
|
|
307
|
+
function formatValidationErrors(errors) {
|
|
308
|
+
return errors
|
|
309
|
+
.map(function (err) {
|
|
310
|
+
var path = err.instancePath || "/";
|
|
311
|
+
var message = err.message || "validation failed";
|
|
312
|
+
return "".concat(path, ": ").concat(message);
|
|
313
|
+
})
|
|
314
|
+
.join("; ");
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Convert OpenApiSchemaProperty to a full OpenApiSchema suitable for AJV.
|
|
318
|
+
* Strips `required` from individual properties (OpenAPI-style) and moves it
|
|
319
|
+
* to the schema-level `required` array (JSON Schema-style) for AJV compatibility.
|
|
320
|
+
*/
|
|
321
|
+
function propertiesToSchema(properties, requiredFields) {
|
|
322
|
+
var e_2, _a;
|
|
323
|
+
// Extract required fields from properties that have required: true
|
|
324
|
+
var autoRequired = Object.entries(properties)
|
|
325
|
+
.filter(function (_a) {
|
|
326
|
+
var _b = __read(_a, 2), _ = _b[0], prop = _b[1];
|
|
327
|
+
return prop.required;
|
|
328
|
+
})
|
|
329
|
+
.map(function (_a) {
|
|
330
|
+
var _b = __read(_a, 1), key = _b[0];
|
|
331
|
+
return key;
|
|
332
|
+
});
|
|
333
|
+
var allRequired = __spreadArray([], __read(new Set(__spreadArray(__spreadArray([], __read((requiredFields !== null && requiredFields !== void 0 ? requiredFields : [])), false), __read(autoRequired), false))), false);
|
|
334
|
+
// Strip `required` from individual properties — AJV only accepts `required` at schema level
|
|
335
|
+
var cleanedProperties = {};
|
|
336
|
+
try {
|
|
337
|
+
for (var _b = __values(Object.entries(properties)), _c = _b.next(); !_c.done; _c = _b.next()) {
|
|
338
|
+
var _d = __read(_c.value, 2), key = _d[0], prop = _d[1];
|
|
339
|
+
var _1 = prop.required, rest = __rest(prop, ["required"]);
|
|
340
|
+
cleanedProperties[key] = rest;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
catch (e_2_1) { e_2 = { error: e_2_1 }; }
|
|
344
|
+
finally {
|
|
345
|
+
try {
|
|
346
|
+
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
|
|
347
|
+
}
|
|
348
|
+
finally { if (e_2) throw e_2.error; }
|
|
349
|
+
}
|
|
350
|
+
var schema = {
|
|
351
|
+
properties: cleanedProperties,
|
|
352
|
+
required: allRequired.length > 0 ? allRequired : undefined,
|
|
353
|
+
type: "object",
|
|
354
|
+
};
|
|
355
|
+
// When removeAdditional is enabled, set additionalProperties: false
|
|
356
|
+
// so AJV knows to strip unknown properties
|
|
357
|
+
if (globalConfig.removeAdditional) {
|
|
358
|
+
schema.additionalProperties = false;
|
|
359
|
+
}
|
|
360
|
+
return schema;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Creates middleware that validates the request body against an OpenAPI schema.
|
|
364
|
+
*
|
|
365
|
+
* The middleware checks `isConfigured` at request time — if `configureOpenApiValidator()`
|
|
366
|
+
* has not been called, the middleware is a no-op.
|
|
367
|
+
*
|
|
368
|
+
* @param schema - The schema to validate against (same format as withRequestBody)
|
|
369
|
+
* @param options - Optional configuration for this validator
|
|
370
|
+
* @returns Express middleware function
|
|
371
|
+
*/
|
|
372
|
+
function validateRequestBody(schema, options) {
|
|
373
|
+
var fullSchema = propertiesToSchema(schema, options === null || options === void 0 ? void 0 : options.required);
|
|
374
|
+
return function (req, _res, next) {
|
|
375
|
+
var _a, _b, _c;
|
|
376
|
+
// No-op if not configured
|
|
377
|
+
if (!isConfigured) {
|
|
378
|
+
next();
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
// Check if validation is enabled (route override takes precedence)
|
|
382
|
+
var isEnabled = (_a = options === null || options === void 0 ? void 0 : options.enabled) !== null && _a !== void 0 ? _a : globalConfig.validateRequests;
|
|
383
|
+
if (!isEnabled) {
|
|
384
|
+
next();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
// Capture keys before validation for removeAdditional detection
|
|
388
|
+
var keysBefore = req.body && typeof req.body === "object" ? Object.keys(req.body) : [];
|
|
389
|
+
// Get validator at request time so config changes take effect
|
|
390
|
+
var validator = getValidator(fullSchema);
|
|
391
|
+
// If schema couldn't be compiled (e.g., non-standard types), skip validation
|
|
392
|
+
if (!validator) {
|
|
393
|
+
next();
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
// Clone body if we might modify it (coercion or removeAdditional)
|
|
397
|
+
var bodyToValidate = __assign({}, req.body);
|
|
398
|
+
var valid = validator(bodyToValidate);
|
|
399
|
+
if (!valid && validator.errors) {
|
|
400
|
+
var errors = validator.errors;
|
|
401
|
+
if (globalConfig.logValidationErrors) {
|
|
402
|
+
logger_1.logger.warn("Request body validation failed for ".concat(req.method, " ").concat(req.path, ": ").concat(formatValidationErrors(errors)));
|
|
403
|
+
}
|
|
404
|
+
// Use custom error handler if provided
|
|
405
|
+
var errorHandler = (_b = options === null || options === void 0 ? void 0 : options.onError) !== null && _b !== void 0 ? _b : globalConfig.onValidationError;
|
|
406
|
+
if (errorHandler) {
|
|
407
|
+
errorHandler(errors, req);
|
|
408
|
+
next();
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
// Default: throw APIError
|
|
412
|
+
throw new errors_1.APIError({
|
|
413
|
+
detail: formatValidationErrors(errors),
|
|
414
|
+
disableExternalErrorTracking: true,
|
|
415
|
+
meta: {
|
|
416
|
+
validationErrors: JSON.stringify(errors.map(function (e) { return ({
|
|
417
|
+
message: e.message,
|
|
418
|
+
params: e.params,
|
|
419
|
+
path: e.instancePath,
|
|
420
|
+
}); })),
|
|
421
|
+
},
|
|
422
|
+
source: {
|
|
423
|
+
pointer: "/body",
|
|
424
|
+
},
|
|
425
|
+
status: 400,
|
|
426
|
+
title: "Request validation failed",
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
// Update req.body with coerced/stripped values
|
|
430
|
+
if (valid) {
|
|
431
|
+
// Detect removed properties (top-level only)
|
|
432
|
+
if (globalConfig.removeAdditional) {
|
|
433
|
+
var keysAfter_1 = Object.keys(bodyToValidate);
|
|
434
|
+
var removedProperties = keysBefore.filter(function (k) { return !keysAfter_1.includes(k); });
|
|
435
|
+
if (removedProperties.length > 0) {
|
|
436
|
+
var hook = (_c = options === null || options === void 0 ? void 0 : options.onAdditionalPropertiesRemoved) !== null && _c !== void 0 ? _c : globalConfig.onAdditionalPropertiesRemoved;
|
|
437
|
+
if (hook) {
|
|
438
|
+
hook(removedProperties, req);
|
|
439
|
+
}
|
|
440
|
+
if (globalConfig.logValidationErrors) {
|
|
441
|
+
logger_1.logger.debug("Stripped additional properties from ".concat(req.method, " ").concat(req.path, ": ").concat(removedProperties.join(", ")));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
req.body = bodyToValidate;
|
|
446
|
+
}
|
|
447
|
+
next();
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Creates middleware that validates query parameters against an OpenAPI schema.
|
|
452
|
+
*
|
|
453
|
+
* @param schema - The schema to validate against
|
|
454
|
+
* @param options - Optional configuration for this validator
|
|
455
|
+
* @returns Express middleware function
|
|
456
|
+
*/
|
|
457
|
+
function validateQueryParams(schema, options) {
|
|
458
|
+
var fullSchema = propertiesToSchema(schema);
|
|
459
|
+
return function (req, _res, next) {
|
|
460
|
+
var _a, _b, _c, _d;
|
|
461
|
+
// No-op if not configured
|
|
462
|
+
if (!isConfigured) {
|
|
463
|
+
next();
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
var isEnabled = (_a = options === null || options === void 0 ? void 0 : options.enabled) !== null && _a !== void 0 ? _a : globalConfig.validateRequests;
|
|
467
|
+
if (!isEnabled) {
|
|
468
|
+
next();
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
// Get validator at request time
|
|
472
|
+
var validator = getValidator(fullSchema);
|
|
473
|
+
// If schema couldn't be compiled, skip validation
|
|
474
|
+
if (!validator) {
|
|
475
|
+
next();
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
var queryToValidate = globalConfig.coerceTypes ? __assign({}, req.query) : req.query;
|
|
479
|
+
var valid = validator(queryToValidate);
|
|
480
|
+
if (!valid && validator.errors) {
|
|
481
|
+
var errors = validator.errors;
|
|
482
|
+
if (globalConfig.logValidationErrors) {
|
|
483
|
+
logger_1.logger.warn("Query parameter validation failed for ".concat(req.method, " ").concat(req.path, ": ").concat(formatValidationErrors(errors)));
|
|
484
|
+
}
|
|
485
|
+
var errorHandler = (_b = options === null || options === void 0 ? void 0 : options.onError) !== null && _b !== void 0 ? _b : globalConfig.onValidationError;
|
|
486
|
+
if (errorHandler) {
|
|
487
|
+
errorHandler(errors, req);
|
|
488
|
+
next();
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
throw new errors_1.APIError({
|
|
492
|
+
detail: formatValidationErrors(errors),
|
|
493
|
+
disableExternalErrorTracking: true,
|
|
494
|
+
meta: {
|
|
495
|
+
validationErrors: JSON.stringify(errors.map(function (e) { return ({
|
|
496
|
+
message: e.message,
|
|
497
|
+
params: e.params,
|
|
498
|
+
path: e.instancePath,
|
|
499
|
+
}); })),
|
|
500
|
+
},
|
|
501
|
+
source: {
|
|
502
|
+
parameter: ((_d = (_c = errors[0]) === null || _c === void 0 ? void 0 : _c.instancePath) === null || _d === void 0 ? void 0 : _d.replace("/", "")) || "unknown",
|
|
503
|
+
},
|
|
504
|
+
status: 400,
|
|
505
|
+
title: "Query parameter validation failed",
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
if (globalConfig.coerceTypes && valid) {
|
|
509
|
+
// Note: req.query is read-only in some Express versions,
|
|
510
|
+
// so we may need to work around this
|
|
511
|
+
Object.assign(req.query, queryToValidate);
|
|
512
|
+
}
|
|
513
|
+
next();
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Creates a combined validation middleware for both body and query parameters.
|
|
518
|
+
*
|
|
519
|
+
* @param options - Configuration for what to validate
|
|
520
|
+
* @returns Express middleware function
|
|
521
|
+
*
|
|
522
|
+
* @example
|
|
523
|
+
* ```typescript
|
|
524
|
+
* router.post("/search", [
|
|
525
|
+
* openApiMiddleware,
|
|
526
|
+
* createValidator({
|
|
527
|
+
* body: {query: {type: "string", required: true}},
|
|
528
|
+
* query: {limit: {type: "number"}},
|
|
529
|
+
* }),
|
|
530
|
+
* ], handler);
|
|
531
|
+
* ```
|
|
532
|
+
*/
|
|
533
|
+
function createValidator(options) {
|
|
534
|
+
var bodyValidator = options.body
|
|
535
|
+
? validateRequestBody(options.body, { enabled: options.enabled })
|
|
536
|
+
: null;
|
|
537
|
+
var queryValidator = options.query
|
|
538
|
+
? validateQueryParams(options.query, { enabled: options.enabled })
|
|
539
|
+
: null;
|
|
540
|
+
return function (req, res, next) {
|
|
541
|
+
// Run body validation first
|
|
542
|
+
if (bodyValidator) {
|
|
543
|
+
bodyValidator(req, res, (function (err) {
|
|
544
|
+
if (err) {
|
|
545
|
+
next(err);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
// Then run query validation
|
|
549
|
+
if (queryValidator) {
|
|
550
|
+
queryValidator(req, res, next);
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
next();
|
|
554
|
+
}
|
|
555
|
+
}));
|
|
556
|
+
}
|
|
557
|
+
else if (queryValidator) {
|
|
558
|
+
queryValidator(req, res, next);
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
next();
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Validates response data against a schema.
|
|
567
|
+
* This is primarily for development/testing to ensure responses match documentation.
|
|
568
|
+
*
|
|
569
|
+
* @param data - The response data to validate
|
|
570
|
+
* @param schema - The expected schema
|
|
571
|
+
* @returns Object with valid flag and any errors
|
|
572
|
+
*/
|
|
573
|
+
function validateResponseData(data, schema) {
|
|
574
|
+
if (!globalConfig.validateResponses) {
|
|
575
|
+
return { valid: true };
|
|
576
|
+
}
|
|
577
|
+
var fullSchema = propertiesToSchema(schema);
|
|
578
|
+
var validator = getValidator(fullSchema);
|
|
579
|
+
if (!validator) {
|
|
580
|
+
return { valid: true };
|
|
581
|
+
}
|
|
582
|
+
var valid = validator(data);
|
|
583
|
+
if (!valid && validator.errors) {
|
|
584
|
+
if (globalConfig.logValidationErrors) {
|
|
585
|
+
logger_1.logger.warn("Response validation failed: ".concat(formatValidationErrors(validator.errors)));
|
|
586
|
+
}
|
|
587
|
+
return { errors: validator.errors, valid: false };
|
|
588
|
+
}
|
|
589
|
+
return { valid: true };
|
|
590
|
+
}
|
|
591
|
+
var m2sOptions = {
|
|
592
|
+
props: ["readOnly", "required", "enum", "default"],
|
|
593
|
+
};
|
|
594
|
+
/**
|
|
595
|
+
* Extract an OpenAPI-compatible schema from a Mongoose model.
|
|
596
|
+
* This allows you to use the same schema definitions for both documentation
|
|
597
|
+
* and runtime validation.
|
|
598
|
+
*
|
|
599
|
+
* @param model - A Mongoose model
|
|
600
|
+
* @returns Schema properties suitable for validation
|
|
601
|
+
*/
|
|
602
|
+
function getSchemaFromModel(model) {
|
|
603
|
+
var modelSwagger = (0, mongoose_to_swagger_1.default)(model, m2sOptions);
|
|
604
|
+
return modelSwagger.properties;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Extract required field names from a Mongoose model's swagger schema.
|
|
608
|
+
*/
|
|
609
|
+
function getRequiredFieldsFromModel(model) {
|
|
610
|
+
var _a;
|
|
611
|
+
var modelSwagger = (0, mongoose_to_swagger_1.default)(model, m2sOptions);
|
|
612
|
+
return (_a = modelSwagger.required) !== null && _a !== void 0 ? _a : [];
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Creates a request body validator middleware from a Mongoose model.
|
|
616
|
+
* This is a convenience function that combines getSchemaFromModel and validateRequestBody.
|
|
617
|
+
*
|
|
618
|
+
* @param model - A Mongoose model to derive the schema from
|
|
619
|
+
* @param options - Optional configuration for the validator
|
|
620
|
+
* @returns Express middleware function
|
|
621
|
+
*/
|
|
622
|
+
function validateModelRequestBody(model, options) {
|
|
623
|
+
var _a, _b;
|
|
624
|
+
var schema = getSchemaFromModel(model);
|
|
625
|
+
var requiredFields = getRequiredFieldsFromModel(model);
|
|
626
|
+
if ((_a = options === null || options === void 0 ? void 0 : options.excludeFields) === null || _a === void 0 ? void 0 : _a.length) {
|
|
627
|
+
var excluded_1 = new Set(options.excludeFields);
|
|
628
|
+
schema = Object.fromEntries(Object.entries(schema).filter(function (_a) {
|
|
629
|
+
var _b = __read(_a, 1), key = _b[0];
|
|
630
|
+
return !excluded_1.has(key);
|
|
631
|
+
}));
|
|
632
|
+
requiredFields = requiredFields.filter(function (f) { return !excluded_1.has(f); });
|
|
633
|
+
}
|
|
634
|
+
return validateRequestBody(schema, __assign(__assign({}, options), { required: __spreadArray(__spreadArray([], __read(((_b = options === null || options === void 0 ? void 0 : options.required) !== null && _b !== void 0 ? _b : [])), false), __read(requiredFields), false) }));
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Creates validation middleware for use with modelRouter.
|
|
638
|
+
* Returns an object with middleware for each operation type.
|
|
639
|
+
*
|
|
640
|
+
* @param model - The Mongoose model
|
|
641
|
+
* @param options - Configuration options
|
|
642
|
+
* @returns Object with create and update validation middleware
|
|
643
|
+
*/
|
|
644
|
+
function createModelValidators(model, options) {
|
|
645
|
+
var schema = getSchemaFromModel(model);
|
|
646
|
+
return {
|
|
647
|
+
create: validateRequestBody(schema, {
|
|
648
|
+
enabled: options === null || options === void 0 ? void 0 : options.validateCreate,
|
|
649
|
+
onAdditionalPropertiesRemoved: options === null || options === void 0 ? void 0 : options.onAdditionalPropertiesRemoved,
|
|
650
|
+
onError: options === null || options === void 0 ? void 0 : options.onError,
|
|
651
|
+
}),
|
|
652
|
+
update: validateRequestBody(schema, {
|
|
653
|
+
enabled: options === null || options === void 0 ? void 0 : options.validateUpdate,
|
|
654
|
+
onAdditionalPropertiesRemoved: options === null || options === void 0 ? void 0 : options.onAdditionalPropertiesRemoved,
|
|
655
|
+
onError: options === null || options === void 0 ? void 0 : options.onError,
|
|
656
|
+
}),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Build a query parameter schema from a model's Mongoose schema and queryFields array.
|
|
661
|
+
* Always includes pagination parameters (limit, page, sort).
|
|
662
|
+
*
|
|
663
|
+
* @param model - A Mongoose model
|
|
664
|
+
* @param queryFields - Array of field names allowed for querying
|
|
665
|
+
* @returns Schema properties suitable for query validation
|
|
666
|
+
*/
|
|
667
|
+
function buildQuerySchemaFromFields(model, queryFields) {
|
|
668
|
+
var e_3, _a;
|
|
669
|
+
if (queryFields === void 0) { queryFields = []; }
|
|
670
|
+
var modelSchema = getSchemaFromModel(model);
|
|
671
|
+
var querySchema = {
|
|
672
|
+
limit: { type: "number" },
|
|
673
|
+
page: { type: "number" },
|
|
674
|
+
sort: { type: "string" },
|
|
675
|
+
};
|
|
676
|
+
try {
|
|
677
|
+
for (var queryFields_1 = __values(queryFields), queryFields_1_1 = queryFields_1.next(); !queryFields_1_1.done; queryFields_1_1 = queryFields_1.next()) {
|
|
678
|
+
var field = queryFields_1_1.value;
|
|
679
|
+
var modelField = modelSchema[field];
|
|
680
|
+
if (modelField) {
|
|
681
|
+
// Use the model's type info, but mark as not required for queries
|
|
682
|
+
querySchema[field] = __assign(__assign({}, modelField), { required: false });
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
// Field not in model schema — allow as string
|
|
686
|
+
querySchema[field] = { type: "string" };
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
catch (e_3_1) { e_3 = { error: e_3_1 }; }
|
|
691
|
+
finally {
|
|
692
|
+
try {
|
|
693
|
+
if (queryFields_1_1 && !queryFields_1_1.done && (_a = queryFields_1.return)) _a.call(queryFields_1);
|
|
694
|
+
}
|
|
695
|
+
finally { if (e_3) throw e_3.error; }
|
|
696
|
+
}
|
|
697
|
+
return querySchema;
|
|
698
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|