@tsoa-next/runtime 7.1.0 → 7.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.MD +8 -2
  2. package/dist/config.d.ts +11 -4
  3. package/dist/decorators/example.d.ts +1 -1
  4. package/dist/decorators/example.js +1 -1
  5. package/dist/decorators/example.js.map +1 -1
  6. package/dist/decorators/methods.d.ts +7 -7
  7. package/dist/decorators/methods.js +7 -7
  8. package/dist/decorators/methods.js.map +1 -1
  9. package/dist/decorators/middlewares.d.ts +2 -1
  10. package/dist/decorators/middlewares.js +9 -6
  11. package/dist/decorators/middlewares.js.map +1 -1
  12. package/dist/decorators/operationid.d.ts +1 -1
  13. package/dist/decorators/operationid.js +1 -1
  14. package/dist/decorators/operationid.js.map +1 -1
  15. package/dist/decorators/parameter.d.ts +9 -9
  16. package/dist/decorators/parameter.js +9 -9
  17. package/dist/decorators/parameter.js.map +1 -1
  18. package/dist/decorators/response.d.ts +3 -3
  19. package/dist/decorators/response.js +3 -3
  20. package/dist/decorators/response.js.map +1 -1
  21. package/dist/decorators/route.d.ts +1 -1
  22. package/dist/decorators/route.js +1 -1
  23. package/dist/decorators/route.js.map +1 -1
  24. package/dist/decorators/security.d.ts +2 -2
  25. package/dist/decorators/security.js +1 -1
  26. package/dist/decorators/security.js.map +1 -1
  27. package/dist/decorators/tags.d.ts +1 -1
  28. package/dist/decorators/tags.js +1 -1
  29. package/dist/decorators/tags.js.map +1 -1
  30. package/dist/decorators/validate.d.ts +9 -0
  31. package/dist/decorators/validate.js +112 -0
  32. package/dist/decorators/validate.js.map +1 -0
  33. package/dist/index.d.ts +13 -11
  34. package/dist/index.js +13 -11
  35. package/dist/index.js.map +1 -1
  36. package/dist/interfaces/iocModule.d.ts +4 -2
  37. package/dist/interfaces/response.d.ts +1 -1
  38. package/dist/metadataGeneration/tsoa.d.ts +41 -9
  39. package/dist/routeGeneration/additionalProps.d.ts +2 -0
  40. package/dist/routeGeneration/externalValidation.d.ts +13 -0
  41. package/dist/routeGeneration/externalValidation.js +232 -0
  42. package/dist/routeGeneration/externalValidation.js.map +1 -0
  43. package/dist/routeGeneration/templateHelpers.d.ts +98 -21
  44. package/dist/routeGeneration/templateHelpers.js +305 -113
  45. package/dist/routeGeneration/templateHelpers.js.map +1 -1
  46. package/dist/routeGeneration/templates/express/expressTemplateService.d.ts +8 -4
  47. package/dist/routeGeneration/templates/express/expressTemplateService.js +32 -15
  48. package/dist/routeGeneration/templates/express/expressTemplateService.js.map +1 -1
  49. package/dist/routeGeneration/templates/hapi/hapiTemplateService.d.ts +22 -8
  50. package/dist/routeGeneration/templates/hapi/hapiTemplateService.js +67 -19
  51. package/dist/routeGeneration/templates/hapi/hapiTemplateService.js.map +1 -1
  52. package/dist/routeGeneration/templates/index.d.ts +1 -1
  53. package/dist/routeGeneration/templates/index.js +1 -1
  54. package/dist/routeGeneration/templates/index.js.map +1 -1
  55. package/dist/routeGeneration/templates/koa/koaTemplateService.d.ts +15 -6
  56. package/dist/routeGeneration/templates/koa/koaTemplateService.js +63 -22
  57. package/dist/routeGeneration/templates/koa/koaTemplateService.js.map +1 -1
  58. package/dist/routeGeneration/templates/templateService.d.ts +9 -4
  59. package/dist/routeGeneration/templates/templateService.js +39 -1
  60. package/dist/routeGeneration/templates/templateService.js.map +1 -1
  61. package/dist/routeGeneration/tsoa-route.d.ts +5 -0
  62. package/dist/swagger/swagger.d.ts +42 -41
  63. package/dist/swagger/swagger.js +4 -0
  64. package/dist/swagger/swagger.js.map +1 -1
  65. package/package.json +35 -6
@@ -6,11 +6,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.ValidateError = exports.ValidationService = void 0;
7
7
  exports.ValidateParam = ValidateParam;
8
8
  const validator_1 = __importDefault(require("validator"));
9
+ const validate_1 = require("../decorators/validate");
9
10
  const assertNever_1 = require("../utils/assertNever");
11
+ const externalValidation_1 = require("./externalValidation");
10
12
  const tsoa_route_1 = require("./tsoa-route");
11
- // for backwards compatibility with custom templates
12
- function ValidateParam(property, value, generatedModels, name = '', fieldErrors, isBodyParam, parent = '', config) {
13
- return new ValidationService(generatedModels, config).ValidateParam(property, value, name, fieldErrors, isBodyParam, parent);
13
+ function ValidateParam(property, value, generatedModels, name = '', fieldErrors, isBodyParam, parent = '', config, metadata) {
14
+ return new ValidationService(generatedModels, config).ValidateParam(property, value, name, fieldErrors, isBodyParam, parent, metadata);
14
15
  }
15
16
  class ValidationService {
16
17
  models;
@@ -20,36 +21,70 @@ class ValidationService {
20
21
  this.models = models;
21
22
  this.config = config;
22
23
  }
23
- ValidateParam(property, rawValue, name = '', fieldErrors, isBodyParam, parent = '') {
24
- let value = rawValue;
25
- // If undefined is allowed type, we can move to value validation
26
- if (value === undefined && property.dataType !== 'undefined') {
27
- // If there's either default value or datatype is union with undefined valid, we can just set it and move to validation
28
- if (property.default !== undefined || (property.dataType === 'union' && property.subSchemas?.some(p => p.dataType === 'undefined'))) {
29
- value = property.default;
30
- }
31
- else if (property.required) {
32
- // If value can be typed as undefined, there's no need to check mandatoriness here.
33
- let message = `'${name}' is required`;
34
- if (property.validators) {
35
- const validators = property.validators;
36
- Object.keys(validators).forEach((key) => {
37
- const errorMsg = validators[key]?.errorMsg;
38
- if (key.startsWith('is') && errorMsg) {
39
- message = errorMsg;
40
- }
41
- });
42
- }
43
- fieldErrors[parent + name] = {
44
- message,
45
- value,
46
- };
47
- return;
48
- }
49
- else {
50
- return value;
51
- }
24
+ isRecord(value) {
25
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
26
+ }
27
+ buildChildPath(parent, name) {
28
+ const fieldPath = parent + name;
29
+ return fieldPath ? `${fieldPath}.` : '';
30
+ }
31
+ ValidateParam(property, rawValue, name, fieldErrors, isBodyParam, parent, metadata) {
32
+ const resolvedName = name ?? '';
33
+ const resolvedParent = parent ?? '';
34
+ const handledUndefined = this.handleUndefinedValue({
35
+ property,
36
+ rawValue,
37
+ name: resolvedName,
38
+ fieldErrors,
39
+ parent: resolvedParent,
40
+ });
41
+ if (handledUndefined.handled) {
42
+ return handledUndefined.value;
43
+ }
44
+ const value = handledUndefined.value;
45
+ if (property.validationStrategy === 'external' && property.externalValidator) {
46
+ return this.validateExternal(resolvedName, value, fieldErrors, property, resolvedParent, metadata);
52
47
  }
48
+ return this.validateResolvedProperty({
49
+ property,
50
+ value,
51
+ name: resolvedName,
52
+ fieldErrors,
53
+ isBodyParam,
54
+ parent: resolvedParent,
55
+ metadata,
56
+ });
57
+ }
58
+ handleUndefinedValue({ property, rawValue, name, fieldErrors, parent }) {
59
+ if (rawValue !== undefined || property.dataType === 'undefined') {
60
+ return { handled: false, value: rawValue };
61
+ }
62
+ if (property.default !== undefined || (property.dataType === 'union' && property.subSchemas?.some(p => p.dataType === 'undefined'))) {
63
+ return { handled: false, value: property.default };
64
+ }
65
+ if (property.required) {
66
+ fieldErrors[parent + name] = {
67
+ message: this.getRequiredFieldMessage(property.validators, name),
68
+ value: rawValue,
69
+ };
70
+ return { handled: true, value: rawValue };
71
+ }
72
+ return { handled: true, value: rawValue };
73
+ }
74
+ getRequiredFieldMessage(validators, name) {
75
+ let message = `'${name}' is required`;
76
+ if (!validators) {
77
+ return message;
78
+ }
79
+ Object.keys(validators).forEach((key) => {
80
+ const errorMsg = validators[key]?.errorMsg;
81
+ if (key.startsWith('is') && errorMsg) {
82
+ message = errorMsg;
83
+ }
84
+ });
85
+ return message;
86
+ }
87
+ validateResolvedProperty({ property, value, name, fieldErrors, isBodyParam, parent, metadata, }) {
53
88
  switch (property.dataType) {
54
89
  case 'string':
55
90
  return this.validateString(name, value, fieldErrors, property.validators, parent);
@@ -64,23 +99,41 @@ class ValidationService {
64
99
  case 'enum':
65
100
  return this.validateEnum(name, value, fieldErrors, property.enums, parent);
66
101
  case 'array':
67
- return this.validateArray(name, value, fieldErrors, isBodyParam, property.array, property.validators, parent);
102
+ return this.validateArray({
103
+ name,
104
+ value,
105
+ fieldErrors,
106
+ isBodyParam,
107
+ schema: property.array,
108
+ validators: property.validators,
109
+ parent,
110
+ metadata,
111
+ });
68
112
  case 'date':
69
113
  return this.validateDate(name, value, fieldErrors, isBodyParam, property.validators, parent);
70
114
  case 'datetime':
71
115
  return this.validateDateTime(name, value, fieldErrors, isBodyParam, property.validators, parent);
72
116
  case 'buffer':
73
- return this.validateBuffer(name, value);
117
+ return this.validateBuffer(name, value, fieldErrors, parent);
74
118
  case 'union':
75
- return this.validateUnion(name, value, fieldErrors, isBodyParam, property, parent);
119
+ return this.validateUnion(name, value, fieldErrors, isBodyParam, property, parent, metadata);
76
120
  case 'intersection':
77
- return this.validateIntersection(name, value, fieldErrors, isBodyParam, property.subSchemas, parent);
121
+ return this.validateIntersection(name, value, fieldErrors, isBodyParam, property.subSchemas, parent, metadata);
78
122
  case 'undefined':
79
123
  return this.validateUndefined(name, value, fieldErrors, parent);
80
124
  case 'any':
81
125
  return value;
82
126
  case 'nestedObjectLiteral':
83
- return this.validateNestedObjectLiteral(name, value, fieldErrors, isBodyParam, property.nestedProperties, property.additionalProperties, parent);
127
+ return this.validateNestedObjectLiteral({
128
+ name,
129
+ value,
130
+ fieldErrors,
131
+ isBodyParam,
132
+ nestedProperties: property.nestedProperties,
133
+ additionalProperties: property.additionalProperties,
134
+ parent,
135
+ metadata,
136
+ });
84
137
  default:
85
138
  if (property.ref) {
86
139
  // Detect circular references to prevent stack overflow
@@ -90,7 +143,7 @@ class ValidationService {
90
143
  }
91
144
  this.validationStack.add(refPath);
92
145
  try {
93
- return this.validateModel({ name, value, modelDefinition: this.models[property.ref], fieldErrors, isBodyParam, parent });
146
+ return this.validateModel({ name, value, modelDefinition: this.models[property.ref], fieldErrors, isBodyParam, parent, metadata });
94
147
  }
95
148
  finally {
96
149
  this.validationStack.delete(refPath);
@@ -99,11 +152,73 @@ class ValidationService {
99
152
  return value;
100
153
  }
101
154
  }
155
+ validateExternal(name, rawValue, fieldErrors, property, parent, metadata) {
156
+ const value = rawValue === undefined && property.default !== undefined ? property.default : rawValue;
157
+ const runtimeMetadata = this.getRuntimeExternalValidatorMetadata(metadata, property);
158
+ const fieldPath = parent + name;
159
+ if (!runtimeMetadata) {
160
+ fieldErrors[fieldPath] = {
161
+ message: `Missing runtime schema metadata for external validator '${property.externalValidator?.kind || 'unknown'}' on '${fieldPath || '(anonymous parameter)'}'. Ensure the controller module is imported so decorators run, and ensure custom templates pass controllerClass, methodName, and parameterIndex into validation.`,
162
+ value,
163
+ };
164
+ return undefined;
165
+ }
166
+ const declaredKind = property.externalValidator?.kind;
167
+ const runtimeKind = runtimeMetadata.kind;
168
+ if (declaredKind && declaredKind !== runtimeKind) {
169
+ fieldErrors[fieldPath] = {
170
+ message: `External validator kind mismatch for '${fieldPath}'. Route schema expects '${declaredKind}' but runtime metadata provided '${runtimeKind}'.`,
171
+ value,
172
+ };
173
+ return undefined;
174
+ }
175
+ const kindToUse = declaredKind || runtimeKind;
176
+ const result = (0, externalValidation_1.validateExternalSchema)(kindToUse, runtimeMetadata.schema, value, this.config.validation ?? {});
177
+ if (result.ok) {
178
+ return result.value;
179
+ }
180
+ this.projectExternalFailureToFieldErrors(result.failure, fieldErrors, name, parent, value);
181
+ return undefined;
182
+ }
183
+ getRuntimeExternalValidatorMetadata(metadata, property) {
184
+ if (!metadata?.controllerClass || metadata.parameterIndex === undefined || !metadata.methodName || !property.externalValidator) {
185
+ return undefined;
186
+ }
187
+ const controllerTarget = metadata.controllerClass;
188
+ const candidateTargets = controllerTarget.prototype ? [controllerTarget.prototype, controllerTarget] : [controllerTarget];
189
+ for (const target of candidateTargets) {
190
+ const runtimeMetadata = (0, validate_1.getParameterExternalValidatorMetadata)(target, metadata.methodName, metadata.parameterIndex);
191
+ if (runtimeMetadata) {
192
+ return runtimeMetadata;
193
+ }
194
+ }
195
+ return undefined;
196
+ }
197
+ projectExternalFailureToFieldErrors(failure, fieldErrors, name, parent, value) {
198
+ if (failure.issues.length === 0) {
199
+ fieldErrors[parent + name] = {
200
+ message: failure.summaryMessage,
201
+ value,
202
+ };
203
+ return;
204
+ }
205
+ for (const issue of failure.issues) {
206
+ const baseFieldPath = parent + name;
207
+ const fieldPath = issue.path ? this.buildIssueFieldPath(baseFieldPath, issue.path) : baseFieldPath;
208
+ if (!fieldErrors[fieldPath]) {
209
+ fieldErrors[fieldPath] = {
210
+ message: issue.message || failure.summaryMessage,
211
+ value,
212
+ };
213
+ }
214
+ }
215
+ }
102
216
  hasCorrectJsType(value, type, isBodyParam) {
103
217
  return !isBodyParam || this.config.bodyCoercion || typeof value === type;
104
218
  }
105
- validateNestedObjectLiteral(name, value, fieldErrors, isBodyParam, nestedProperties, additionalProperties, parent) {
106
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
219
+ validateNestedObjectLiteral(...args) {
220
+ const { name, value, fieldErrors, isBodyParam, nestedProperties, additionalProperties, parent, metadata } = this.normalizeValidateNestedObjectLiteralArgs(args);
221
+ if (!this.isRecord(value)) {
107
222
  fieldErrors[parent + name] = {
108
223
  message: `invalid object`,
109
224
  value,
@@ -115,7 +230,7 @@ class ValidationService {
115
230
  throw new Error('internal tsoa error: ' +
116
231
  'the metadata that was generated should have had nested property schemas since it’s for a nested object,' +
117
232
  'however it did not. ' +
118
- 'Please file an issue with tsoa at https://github.com/lukeautry/tsoa/issues');
233
+ 'Please file an issue with tsoa at https://github.com/tsoa-next/tsoa-next/issues');
119
234
  }
120
235
  const propHandling = this.config.noImplicitAdditionalProperties;
121
236
  if (propHandling !== 'ignore') {
@@ -134,17 +249,18 @@ class ValidationService {
134
249
  }
135
250
  }
136
251
  }
252
+ const childPath = this.buildChildPath(parent, name);
137
253
  Object.keys(nestedProperties).forEach(key => {
138
- const validatedProp = this.ValidateParam(nestedProperties[key], value[key], key, fieldErrors, isBodyParam, parent + name + '.');
254
+ const validatedProp = this.ValidateParam(nestedProperties[key], value[key], key, fieldErrors, isBodyParam, childPath, metadata);
139
255
  // Add value from validator if it's not undefined or if value is required and unfedined is valid type
140
256
  if (validatedProp !== undefined || (nestedProperties[key].dataType === 'undefined' && nestedProperties[key].required)) {
141
257
  value[key] = validatedProp;
142
258
  }
143
259
  });
144
- if (typeof additionalProperties === 'object' && typeof value === 'object') {
260
+ if (typeof additionalProperties === 'object') {
145
261
  const keys = Object.keys(value).filter(key => typeof nestedProperties[key] === 'undefined');
146
262
  keys.forEach(key => {
147
- const validatedProp = this.ValidateParam(additionalProperties, value[key], key, fieldErrors, isBodyParam, parent + name + '.');
263
+ const validatedProp = this.ValidateParam(additionalProperties, value[key], key, fieldErrors, isBodyParam, childPath, metadata);
148
264
  // Add value from validator if it's not undefined or if value is required and unfedined is valid type
149
265
  if (validatedProp !== undefined || (additionalProperties.dataType === 'undefined' && additionalProperties.required)) {
150
266
  value[key] = validatedProp;
@@ -156,6 +272,14 @@ class ValidationService {
156
272
  }
157
273
  return value;
158
274
  }
275
+ normalizeValidateNestedObjectLiteralArgs(args) {
276
+ if (typeof args[0] === 'string') {
277
+ const tupleArgs = args;
278
+ const [name, value, fieldErrors, isBodyParam, nestedProperties, additionalProperties, parent = '', metadata] = tupleArgs;
279
+ return { name, value, fieldErrors, isBodyParam, nestedProperties, additionalProperties, parent, metadata };
280
+ }
281
+ return args[0];
282
+ }
159
283
  validateInt(name, value, fieldErrors, isBodyParam, validators, parent = '') {
160
284
  if (!this.hasCorrectJsType(value, 'number', isBodyParam) || !validator_1.default.isInt(String(value))) {
161
285
  let message = `invalid integer number`;
@@ -186,6 +310,15 @@ class ValidationService {
186
310
  return;
187
311
  }
188
312
  }
313
+ if (validators.exclusiveMinimum && validators.exclusiveMinimum.value !== undefined) {
314
+ if (validators.exclusiveMinimum.value >= numberValue) {
315
+ fieldErrors[parent + name] = {
316
+ message: validators.exclusiveMinimum.errorMsg || `exclusiveMin ${validators.exclusiveMinimum.value}`,
317
+ value,
318
+ };
319
+ return;
320
+ }
321
+ }
189
322
  if (validators.maximum && validators.maximum.value !== undefined) {
190
323
  if (validators.maximum.value < numberValue) {
191
324
  fieldErrors[parent + name] = {
@@ -195,6 +328,15 @@ class ValidationService {
195
328
  return;
196
329
  }
197
330
  }
331
+ if (validators.exclusiveMaximum && validators.exclusiveMaximum.value !== undefined) {
332
+ if (validators.exclusiveMaximum.value <= numberValue) {
333
+ fieldErrors[parent + name] = {
334
+ message: validators.exclusiveMaximum.errorMsg || `exclusiveMax ${validators.exclusiveMaximum.value}`,
335
+ value,
336
+ };
337
+ return;
338
+ }
339
+ }
198
340
  return numberValue;
199
341
  }
200
342
  validateFloat(name, value, fieldErrors, isBodyParam, validators, parent = '') {
@@ -227,6 +369,15 @@ class ValidationService {
227
369
  return;
228
370
  }
229
371
  }
372
+ if (validators.exclusiveMinimum && validators.exclusiveMinimum.value !== undefined) {
373
+ if (validators.exclusiveMinimum.value >= numberValue) {
374
+ fieldErrors[parent + name] = {
375
+ message: validators.exclusiveMinimum.errorMsg || `exclusiveMin ${validators.exclusiveMinimum.value}`,
376
+ value,
377
+ };
378
+ return;
379
+ }
380
+ }
230
381
  if (validators.maximum && validators.maximum.value !== undefined) {
231
382
  if (validators.maximum.value < numberValue) {
232
383
  fieldErrors[parent + name] = {
@@ -236,6 +387,15 @@ class ValidationService {
236
387
  return;
237
388
  }
238
389
  }
390
+ if (validators.exclusiveMaximum && validators.exclusiveMaximum.value !== undefined) {
391
+ if (validators.exclusiveMaximum.value <= numberValue) {
392
+ fieldErrors[parent + name] = {
393
+ message: validators.exclusiveMaximum.errorMsg || `exclusiveMax ${validators.exclusiveMaximum.value}`,
394
+ value,
395
+ };
396
+ return;
397
+ }
398
+ }
239
399
  return numberValue;
240
400
  }
241
401
  validateEnum(name, value, fieldErrors, members, parent = '') {
@@ -402,72 +562,101 @@ class ValidationService {
402
562
  };
403
563
  return;
404
564
  }
405
- validateArray(name, value, fieldErrors, isBodyParam, schema, validators, parent = '') {
406
- if ((isBodyParam && this.config.bodyCoercion === false && !Array.isArray(value)) || !schema || value === undefined) {
407
- const message = validators && validators.isArray && validators.isArray.errorMsg ? validators.isArray.errorMsg : `invalid array`;
408
- fieldErrors[parent + name] = {
565
+ validateArray(...args) {
566
+ const options = this.normalizeValidateArrayArgs(args);
567
+ const { name, value: resolvedValue, fieldErrors: resolvedFieldErrors, isBodyParam: resolvedIsBodyParam, schema: resolvedSchema, validators: resolvedValidators, parent: resolvedParent = '', metadata: resolvedMetadata, } = options;
568
+ if ((resolvedIsBodyParam && this.config.bodyCoercion === false && !Array.isArray(resolvedValue)) || !resolvedSchema || resolvedValue === undefined) {
569
+ const message = resolvedValidators?.isArray?.errorMsg || `invalid array`;
570
+ resolvedFieldErrors[resolvedParent + name] = {
409
571
  message,
410
- value,
572
+ value: resolvedValue,
411
573
  };
412
574
  return;
413
575
  }
414
576
  let arrayValue = [];
415
- const previousErrors = Object.keys(fieldErrors).length;
416
- if (Array.isArray(value)) {
417
- arrayValue = value.map((elementValue, index) => {
418
- return this.ValidateParam(schema, elementValue, `$${index}`, fieldErrors, isBodyParam, name + '.');
577
+ const previousErrors = Object.keys(resolvedFieldErrors).length;
578
+ const childParent = this.buildChildPath(resolvedParent, name);
579
+ if (Array.isArray(resolvedValue)) {
580
+ arrayValue = resolvedValue.map((elementValue, index) => {
581
+ const validatedElement = this.ValidateParam(resolvedSchema, elementValue, `$${index}`, resolvedFieldErrors, resolvedIsBodyParam, childParent, resolvedMetadata);
582
+ return validatedElement;
419
583
  });
420
584
  }
421
585
  else {
422
- arrayValue = [this.ValidateParam(schema, value, '$0', fieldErrors, isBodyParam, name + '.')];
586
+ const validatedElement = this.ValidateParam(resolvedSchema, resolvedValue, '$0', resolvedFieldErrors, resolvedIsBodyParam, childParent, resolvedMetadata);
587
+ arrayValue = [validatedElement];
423
588
  }
424
- if (Object.keys(fieldErrors).length > previousErrors) {
589
+ if (Object.keys(resolvedFieldErrors).length > previousErrors) {
590
+ return;
591
+ }
592
+ const validatorError = this.getArrayValidatorError(resolvedValidators, arrayValue, resolvedValue);
593
+ if (validatorError) {
594
+ resolvedFieldErrors[resolvedParent + name] = validatorError;
425
595
  return;
426
596
  }
597
+ return arrayValue;
598
+ }
599
+ normalizeValidateArrayArgs(args) {
600
+ if (typeof args[0] === 'string') {
601
+ const [name, value, fieldErrors, isBodyParam, schema, validators, parent = '', metadata] = args;
602
+ return { name, value, fieldErrors, isBodyParam, schema, validators, parent, metadata };
603
+ }
604
+ return args[0];
605
+ }
606
+ getArrayValidatorError(validators, arrayValue, originalValue) {
427
607
  if (!validators) {
428
- return arrayValue;
608
+ return undefined;
429
609
  }
430
- if (validators.minItems && validators.minItems.value) {
431
- if (validators.minItems.value > arrayValue.length) {
432
- fieldErrors[parent + name] = {
433
- message: validators.minItems.errorMsg || `minItems ${validators.minItems.value}`,
434
- value,
435
- };
436
- return;
437
- }
610
+ if (validators.minItems?.value && validators.minItems.value > arrayValue.length) {
611
+ return {
612
+ message: validators.minItems.errorMsg || `minItems ${validators.minItems.value}`,
613
+ value: originalValue,
614
+ };
438
615
  }
439
- if (validators.maxItems && validators.maxItems.value) {
440
- if (validators.maxItems.value < arrayValue.length) {
441
- fieldErrors[parent + name] = {
442
- message: validators.maxItems.errorMsg || `maxItems ${validators.maxItems.value}`,
443
- value,
444
- };
445
- return;
446
- }
616
+ if (validators.maxItems?.value && validators.maxItems.value < arrayValue.length) {
617
+ return {
618
+ message: validators.maxItems.errorMsg || `maxItems ${validators.maxItems.value}`,
619
+ value: originalValue,
620
+ };
447
621
  }
448
- if (validators.uniqueItems) {
449
- const unique = arrayValue.some((elem, index, arr) => {
450
- const indexOf = arr.indexOf(elem);
451
- return indexOf > -1 && indexOf !== index;
452
- });
453
- if (unique) {
454
- fieldErrors[parent + name] = {
455
- message: validators.uniqueItems.errorMsg || `required unique array`,
456
- value,
457
- };
458
- return;
459
- }
622
+ if (validators.uniqueItems && this.hasDuplicateArrayItems(arrayValue)) {
623
+ return {
624
+ message: validators.uniqueItems.errorMsg || `required unique array`,
625
+ value: originalValue,
626
+ };
460
627
  }
461
- return arrayValue;
628
+ return undefined;
462
629
  }
463
- validateBuffer(_name, value) {
464
- return Buffer.from(value);
630
+ hasDuplicateArrayItems(arrayValue) {
631
+ return arrayValue.some((elem, index, arr) => {
632
+ const indexOf = arr.indexOf(elem);
633
+ return indexOf > -1 && indexOf !== index;
634
+ });
635
+ }
636
+ buildIssueFieldPath(baseFieldPath, issuePath) {
637
+ return baseFieldPath ? `${baseFieldPath}.${issuePath}` : issuePath;
638
+ }
639
+ validateBuffer(name, value, fieldErrors, parent = '') {
640
+ if (Buffer.isBuffer(value)) {
641
+ return value;
642
+ }
643
+ if (typeof value === 'string') {
644
+ return Buffer.from(value);
645
+ }
646
+ if (value instanceof Uint8Array) {
647
+ return Buffer.from(value);
648
+ }
649
+ fieldErrors[parent + name] = {
650
+ message: 'invalid buffer value',
651
+ value,
652
+ };
653
+ return;
465
654
  }
466
- validateUnion(name, value, fieldErrors, isBodyParam, property, parent = '') {
655
+ validateUnion(name, value, fieldErrors, isBodyParam, property, parent = '', metadata) {
467
656
  if (!property.subSchemas) {
468
657
  throw new Error('internal tsoa error: ' +
469
658
  'the metadata that was generated should have had sub schemas since it’s for a union, however it did not. ' +
470
- 'Please file an issue with tsoa at https://github.com/lukeautry/tsoa/issues');
659
+ 'Please file an issue with tsoa at https://github.com/tsoa-next/tsoa-next/issues');
471
660
  }
472
661
  const subFieldErrors = [];
473
662
  for (const subSchema of property.subSchemas) {
@@ -475,7 +664,7 @@ class ValidationService {
475
664
  // Clean value if it's not undefined or use undefined directly if it's undefined.
476
665
  // Value can be undefined if undefined is allowed datatype of the union
477
666
  const validateableValue = value !== undefined ? this.deepClone(value) : value;
478
- const cleanValue = this.ValidateParam({ ...subSchema, validators: { ...property.validators, ...subSchema.validators } }, validateableValue, name, subFieldError, isBodyParam, parent);
667
+ const cleanValue = this.ValidateParam({ ...subSchema, validators: { ...property.validators, ...subSchema.validators } }, validateableValue, name, subFieldError, isBodyParam, parent, metadata);
479
668
  subFieldErrors.push(subFieldError);
480
669
  if (Object.keys(subFieldError).length === 0) {
481
670
  return cleanValue;
@@ -484,11 +673,11 @@ class ValidationService {
484
673
  this.addSummarizedError(fieldErrors, parent + name, 'Could not match the union against any of the items. Issues: ', subFieldErrors, value);
485
674
  return;
486
675
  }
487
- validateIntersection(name, value, fieldErrors, isBodyParam, subSchemas, parent = '') {
676
+ validateIntersection(name, value, fieldErrors, isBodyParam, subSchemas, parent = '', metadata) {
488
677
  if (!subSchemas) {
489
678
  throw new Error('internal tsoa error: ' +
490
679
  'the metadata that was generated should have had sub schemas since it’s for a intersection, however it did not. ' +
491
- 'Please file an issue with tsoa at https://github.com/lukeautry/tsoa/issues');
680
+ 'Please file an issue with tsoa at https://github.com/tsoa-next/tsoa-next/issues');
492
681
  }
493
682
  const subFieldErrors = [];
494
683
  let cleanValues = {};
@@ -496,11 +685,13 @@ class ValidationService {
496
685
  const subFieldError = {};
497
686
  const cleanValue = this.createChildValidationService({
498
687
  noImplicitAdditionalProperties: 'silently-remove-extras',
499
- }).ValidateParam(subSchema, this.deepClone(value), name, subFieldError, isBodyParam, parent);
500
- cleanValues = {
501
- ...cleanValues,
502
- ...cleanValue,
503
- };
688
+ }).ValidateParam(subSchema, this.deepClone(value), name, subFieldError, isBodyParam, parent, metadata);
689
+ if (this.isRecord(cleanValue)) {
690
+ cleanValues = {
691
+ ...cleanValues,
692
+ ...cleanValue,
693
+ };
694
+ }
504
695
  subFieldErrors.push(subFieldError);
505
696
  });
506
697
  const filtered = subFieldErrors.filter(subFieldError => Object.keys(subFieldError).length !== 0);
@@ -519,12 +710,13 @@ class ValidationService {
519
710
  modelDefinition: schema,
520
711
  fieldErrors: requiredPropError,
521
712
  isBodyParam,
713
+ metadata,
522
714
  });
523
715
  return requiredPropError;
524
716
  };
525
717
  const schemasWithRequiredProps = schemas.filter(schema => Object.keys(getRequiredPropError(schema)).length === 0);
526
718
  if (this.config.noImplicitAdditionalProperties === 'ignore') {
527
- return { ...value, ...cleanValues };
719
+ return this.isRecord(value) ? { ...value, ...cleanValues } : cleanValues;
528
720
  }
529
721
  if (this.config.noImplicitAdditionalProperties === 'silently-remove-extras') {
530
722
  if (schemasWithRequiredProps.length > 0) {
@@ -538,7 +730,7 @@ class ValidationService {
538
730
  return;
539
731
  }
540
732
  }
541
- if (schemasWithRequiredProps.length > 0 && schemasWithRequiredProps.some(schema => this.getExcessPropertiesFor(schema, Object.keys(value)).length === 0)) {
733
+ if (this.isRecord(value) && schemasWithRequiredProps.length > 0 && schemasWithRequiredProps.some(schema => this.getExcessPropertiesFor(schema, Object.keys(value)).length === 0)) {
542
734
  return cleanValues;
543
735
  }
544
736
  else {
@@ -644,17 +836,18 @@ class ValidationService {
644
836
  }
645
837
  }
646
838
  validateModel(input) {
647
- const { name, value, modelDefinition, fieldErrors, isBodyParam, parent = '' } = input;
839
+ const { name, value, modelDefinition, fieldErrors, isBodyParam, parent = '', metadata } = input;
648
840
  const previousErrors = Object.keys(fieldErrors).length;
649
841
  if (modelDefinition) {
650
842
  if (modelDefinition.dataType === 'refEnum') {
651
843
  return this.validateEnum(name, value, fieldErrors, modelDefinition.enums, parent);
652
844
  }
653
845
  if (modelDefinition.dataType === 'refAlias') {
654
- return this.ValidateParam(modelDefinition.type, value, name, fieldErrors, isBodyParam, parent);
846
+ return this.ValidateParam(modelDefinition.type, value, name, fieldErrors, isBodyParam, parent, metadata);
655
847
  }
656
848
  const fieldPath = parent + name;
657
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
849
+ const childPath = this.buildChildPath(parent, name);
850
+ if (!this.isRecord(value)) {
658
851
  fieldErrors[fieldPath] = {
659
852
  message: `invalid object`,
660
853
  value,
@@ -665,7 +858,7 @@ class ValidationService {
665
858
  const keysOnPropertiesModelDefinition = new Set(Object.keys(properties));
666
859
  const allPropertiesOnData = new Set(Object.keys(value));
667
860
  Object.entries(properties).forEach(([key, property]) => {
668
- const validatedParam = this.ValidateParam(property, value[key], key, fieldErrors, isBodyParam, fieldPath + '.');
861
+ const validatedParam = this.ValidateParam(property, value[key], key, fieldErrors, isBodyParam, childPath, metadata);
669
862
  // Add value from validator if it's not undefined or if value is required and unfedined is valid type
670
863
  if (validatedParam !== undefined || (property.dataType === 'undefined' && property.required)) {
671
864
  value[key] = validatedParam;
@@ -682,7 +875,7 @@ class ValidationService {
682
875
  Object.keys(value).forEach((key) => {
683
876
  if (isAnExcessProperty(key)) {
684
877
  if (this.config.noImplicitAdditionalProperties === 'throw-on-extras') {
685
- fieldErrors[`${fieldPath}.${key}`] = {
878
+ fieldErrors[`${childPath}${key}`] = {
686
879
  message: `"${key}" is an excess property and therefore is not allowed`,
687
880
  value: key,
688
881
  };
@@ -702,13 +895,13 @@ class ValidationService {
702
895
  else {
703
896
  Object.keys(value).forEach((key) => {
704
897
  if (isAnExcessProperty(key)) {
705
- const validatedValue = this.ValidateParam(additionalProperties, value[key], key, fieldErrors, isBodyParam, fieldPath + '.');
898
+ const validatedValue = this.ValidateParam(additionalProperties, value[key], key, fieldErrors, isBodyParam, childPath, metadata);
706
899
  // Add value from validator if it's not undefined or if value is required and unfedined is valid type
707
900
  if (validatedValue !== undefined || (additionalProperties.dataType === 'undefined' && additionalProperties.required)) {
708
901
  value[key] = validatedValue;
709
902
  }
710
903
  else {
711
- fieldErrors[`${fieldPath}.${key}`] = {
904
+ fieldErrors[`${childPath}${key}`] = {
712
905
  message: `No matching model found in additionalProperties to validate ${key}`,
713
906
  value: key,
714
907
  };
@@ -754,14 +947,13 @@ class ValidationService {
754
947
  return new Date(obj.getTime());
755
948
  }
756
949
  if (obj instanceof RegExp) {
757
- return new RegExp(obj.source, obj.flags);
950
+ // Preserve the existing instance instead of reconstructing a pattern from untrusted data.
951
+ return obj;
758
952
  }
759
- if (obj instanceof Array) {
760
- const cloneArr = new Array(obj.length);
761
- for (let i = 0; i < obj.length; i++) {
762
- cloneArr[i] = this.deepClone(obj[i]);
763
- }
764
- return cloneArr;
953
+ if (Array.isArray(obj)) {
954
+ const arrayValues = obj;
955
+ const clonedArray = arrayValues.map(value => this.deepClone(value));
956
+ return clonedArray;
765
957
  }
766
958
  if (Buffer && obj instanceof Buffer) {
767
959
  return Buffer.from(obj);