ValidationExecutor.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. import { ValidationError } from './ValidationError';
  2. import { ValidationTypes } from './ValidationTypes';
  3. import { ValidationUtils } from './ValidationUtils';
  4. import { isPromise, convertToArray } from '../utils';
  5. import { getMetadataStorage } from '../metadata/MetadataStorage';
  6. /**
  7. * Executes validation over given object.
  8. */
  9. export class ValidationExecutor {
  10. // -------------------------------------------------------------------------
  11. // Constructor
  12. // -------------------------------------------------------------------------
  13. constructor(validator, validatorOptions) {
  14. this.validator = validator;
  15. this.validatorOptions = validatorOptions;
  16. // -------------------------------------------------------------------------
  17. // Properties
  18. // -------------------------------------------------------------------------
  19. this.awaitingPromises = [];
  20. this.ignoreAsyncValidations = false;
  21. // -------------------------------------------------------------------------
  22. // Private Properties
  23. // -------------------------------------------------------------------------
  24. this.metadataStorage = getMetadataStorage();
  25. }
  26. // -------------------------------------------------------------------------
  27. // Public Methods
  28. // -------------------------------------------------------------------------
  29. execute(object, targetSchema, validationErrors) {
  30. var _a, _b;
  31. /**
  32. * If there is no metadata registered it means possibly the dependencies are not flatterned and
  33. * more than one instance is used.
  34. *
  35. * TODO: This needs proper handling, forcing to use the same container or some other proper solution.
  36. */
  37. if (!this.metadataStorage.hasValidationMetaData && ((_a = this.validatorOptions) === null || _a === void 0 ? void 0 : _a.enableDebugMessages) === true) {
  38. console.warn(`No validation metadata found. No validation will be performed. There are multiple possible reasons:\n` +
  39. ` - There may be multiple class-validator versions installed. You will need to flatten your dependencies to fix the issue.\n` +
  40. ` - This validation runs before any file with validation decorator was parsed by NodeJS.`);
  41. }
  42. const groups = this.validatorOptions ? this.validatorOptions.groups : undefined;
  43. const strictGroups = (this.validatorOptions && this.validatorOptions.strictGroups) || false;
  44. const always = (this.validatorOptions && this.validatorOptions.always) || false;
  45. /** Forbid unknown values are turned on by default and any other value than false will enable it. */
  46. const forbidUnknownValues = ((_b = this.validatorOptions) === null || _b === void 0 ? void 0 : _b.forbidUnknownValues) === undefined || this.validatorOptions.forbidUnknownValues !== false;
  47. const targetMetadatas = this.metadataStorage.getTargetValidationMetadatas(object.constructor, targetSchema, always, strictGroups, groups);
  48. const groupedMetadatas = this.metadataStorage.groupByPropertyName(targetMetadatas);
  49. if (this.validatorOptions && forbidUnknownValues && !targetMetadatas.length) {
  50. const validationError = new ValidationError();
  51. if (!this.validatorOptions ||
  52. !this.validatorOptions.validationError ||
  53. this.validatorOptions.validationError.target === undefined ||
  54. this.validatorOptions.validationError.target === true)
  55. validationError.target = object;
  56. validationError.value = undefined;
  57. validationError.property = undefined;
  58. validationError.children = [];
  59. validationError.constraints = { unknownValue: 'an unknown value was passed to the validate function' };
  60. validationErrors.push(validationError);
  61. return;
  62. }
  63. if (this.validatorOptions && this.validatorOptions.whitelist)
  64. this.whitelist(object, groupedMetadatas, validationErrors);
  65. // General validation
  66. Object.keys(groupedMetadatas).forEach(propertyName => {
  67. const value = object[propertyName];
  68. const definedMetadatas = groupedMetadatas[propertyName].filter(metadata => metadata.type === ValidationTypes.IS_DEFINED);
  69. const metadatas = groupedMetadatas[propertyName].filter(metadata => metadata.type !== ValidationTypes.IS_DEFINED && metadata.type !== ValidationTypes.WHITELIST);
  70. if (value instanceof Promise &&
  71. metadatas.find(metadata => metadata.type === ValidationTypes.PROMISE_VALIDATION)) {
  72. this.awaitingPromises.push(value.then(resolvedValue => {
  73. this.performValidations(object, resolvedValue, propertyName, definedMetadatas, metadatas, validationErrors);
  74. }));
  75. }
  76. else {
  77. this.performValidations(object, value, propertyName, definedMetadatas, metadatas, validationErrors);
  78. }
  79. });
  80. }
  81. whitelist(object, groupedMetadatas, validationErrors) {
  82. const notAllowedProperties = [];
  83. Object.keys(object).forEach(propertyName => {
  84. // does this property have no metadata?
  85. if (!groupedMetadatas[propertyName] || groupedMetadatas[propertyName].length === 0)
  86. notAllowedProperties.push(propertyName);
  87. });
  88. if (notAllowedProperties.length > 0) {
  89. if (this.validatorOptions && this.validatorOptions.forbidNonWhitelisted) {
  90. // throw errors
  91. notAllowedProperties.forEach(property => {
  92. const validationError = this.generateValidationError(object, object[property], property);
  93. validationError.constraints = { [ValidationTypes.WHITELIST]: `property ${property} should not exist` };
  94. validationError.children = undefined;
  95. validationErrors.push(validationError);
  96. });
  97. }
  98. else {
  99. // strip non allowed properties
  100. notAllowedProperties.forEach(property => delete object[property]);
  101. }
  102. }
  103. }
  104. stripEmptyErrors(errors) {
  105. return errors.filter(error => {
  106. if (error.children) {
  107. error.children = this.stripEmptyErrors(error.children);
  108. }
  109. if (Object.keys(error.constraints).length === 0) {
  110. if (error.children.length === 0) {
  111. return false;
  112. }
  113. else {
  114. delete error.constraints;
  115. }
  116. }
  117. return true;
  118. });
  119. }
  120. // -------------------------------------------------------------------------
  121. // Private Methods
  122. // -------------------------------------------------------------------------
  123. performValidations(object, value, propertyName, definedMetadatas, metadatas, validationErrors) {
  124. const customValidationMetadatas = metadatas.filter(metadata => metadata.type === ValidationTypes.CUSTOM_VALIDATION);
  125. const nestedValidationMetadatas = metadatas.filter(metadata => metadata.type === ValidationTypes.NESTED_VALIDATION);
  126. const conditionalValidationMetadatas = metadatas.filter(metadata => metadata.type === ValidationTypes.CONDITIONAL_VALIDATION);
  127. const validationError = this.generateValidationError(object, value, propertyName);
  128. validationErrors.push(validationError);
  129. const canValidate = this.conditionalValidations(object, value, conditionalValidationMetadatas);
  130. if (!canValidate) {
  131. return;
  132. }
  133. // handle IS_DEFINED validation type the special way - it should work no matter skipUndefinedProperties/skipMissingProperties is set or not
  134. this.customValidations(object, value, definedMetadatas, validationError);
  135. this.mapContexts(object, value, definedMetadatas, validationError);
  136. if (value === undefined && this.validatorOptions && this.validatorOptions.skipUndefinedProperties === true) {
  137. return;
  138. }
  139. if (value === null && this.validatorOptions && this.validatorOptions.skipNullProperties === true) {
  140. return;
  141. }
  142. if ((value === null || value === undefined) &&
  143. this.validatorOptions &&
  144. this.validatorOptions.skipMissingProperties === true) {
  145. return;
  146. }
  147. this.customValidations(object, value, customValidationMetadatas, validationError);
  148. this.nestedValidations(value, nestedValidationMetadatas, validationError);
  149. this.mapContexts(object, value, metadatas, validationError);
  150. this.mapContexts(object, value, customValidationMetadatas, validationError);
  151. }
  152. generateValidationError(object, value, propertyName) {
  153. const validationError = new ValidationError();
  154. if (!this.validatorOptions ||
  155. !this.validatorOptions.validationError ||
  156. this.validatorOptions.validationError.target === undefined ||
  157. this.validatorOptions.validationError.target === true)
  158. validationError.target = object;
  159. if (!this.validatorOptions ||
  160. !this.validatorOptions.validationError ||
  161. this.validatorOptions.validationError.value === undefined ||
  162. this.validatorOptions.validationError.value === true)
  163. validationError.value = value;
  164. validationError.property = propertyName;
  165. validationError.children = [];
  166. validationError.constraints = {};
  167. return validationError;
  168. }
  169. conditionalValidations(object, value, metadatas) {
  170. return metadatas
  171. .map(metadata => metadata.constraints[0](object, value))
  172. .reduce((resultA, resultB) => resultA && resultB, true);
  173. }
  174. customValidations(object, value, metadatas, error) {
  175. metadatas.forEach(metadata => {
  176. this.metadataStorage.getTargetValidatorConstraints(metadata.constraintCls).forEach(customConstraintMetadata => {
  177. if (customConstraintMetadata.async && this.ignoreAsyncValidations)
  178. return;
  179. if (this.validatorOptions &&
  180. this.validatorOptions.stopAtFirstError &&
  181. Object.keys(error.constraints || {}).length > 0)
  182. return;
  183. const validationArguments = {
  184. targetName: object.constructor ? object.constructor.name : undefined,
  185. property: metadata.propertyName,
  186. object: object,
  187. value: value,
  188. constraints: metadata.constraints,
  189. };
  190. if (!metadata.each || !(Array.isArray(value) || value instanceof Set || value instanceof Map)) {
  191. const validatedValue = customConstraintMetadata.instance.validate(value, validationArguments);
  192. if (isPromise(validatedValue)) {
  193. const promise = validatedValue.then(isValid => {
  194. if (!isValid) {
  195. const [type, message] = this.createValidationError(object, value, metadata, customConstraintMetadata);
  196. error.constraints[type] = message;
  197. if (metadata.context) {
  198. if (!error.contexts) {
  199. error.contexts = {};
  200. }
  201. error.contexts[type] = Object.assign(error.contexts[type] || {}, metadata.context);
  202. }
  203. }
  204. });
  205. this.awaitingPromises.push(promise);
  206. }
  207. else {
  208. if (!validatedValue) {
  209. const [type, message] = this.createValidationError(object, value, metadata, customConstraintMetadata);
  210. error.constraints[type] = message;
  211. }
  212. }
  213. return;
  214. }
  215. // convert set and map into array
  216. const arrayValue = convertToArray(value);
  217. // Validation needs to be applied to each array item
  218. const validatedSubValues = arrayValue.map((subValue) => customConstraintMetadata.instance.validate(subValue, validationArguments));
  219. const validationIsAsync = validatedSubValues.some((validatedSubValue) => isPromise(validatedSubValue));
  220. if (validationIsAsync) {
  221. // Wrap plain values (if any) in promises, so that all are async
  222. const asyncValidatedSubValues = validatedSubValues.map((validatedSubValue) => isPromise(validatedSubValue) ? validatedSubValue : Promise.resolve(validatedSubValue));
  223. const asyncValidationIsFinishedPromise = Promise.all(asyncValidatedSubValues).then((flatValidatedValues) => {
  224. const validationResult = flatValidatedValues.every((isValid) => isValid);
  225. if (!validationResult) {
  226. const [type, message] = this.createValidationError(object, value, metadata, customConstraintMetadata);
  227. error.constraints[type] = message;
  228. if (metadata.context) {
  229. if (!error.contexts) {
  230. error.contexts = {};
  231. }
  232. error.contexts[type] = Object.assign(error.contexts[type] || {}, metadata.context);
  233. }
  234. }
  235. });
  236. this.awaitingPromises.push(asyncValidationIsFinishedPromise);
  237. return;
  238. }
  239. const validationResult = validatedSubValues.every((isValid) => isValid);
  240. if (!validationResult) {
  241. const [type, message] = this.createValidationError(object, value, metadata, customConstraintMetadata);
  242. error.constraints[type] = message;
  243. }
  244. });
  245. });
  246. }
  247. nestedValidations(value, metadatas, error) {
  248. if (value === void 0) {
  249. return;
  250. }
  251. metadatas.forEach(metadata => {
  252. if (metadata.type !== ValidationTypes.NESTED_VALIDATION && metadata.type !== ValidationTypes.PROMISE_VALIDATION) {
  253. return;
  254. }
  255. else if (this.validatorOptions &&
  256. this.validatorOptions.stopAtFirstError &&
  257. Object.keys(error.constraints || {}).length > 0) {
  258. return;
  259. }
  260. if (Array.isArray(value) || value instanceof Set || value instanceof Map) {
  261. // Treats Set as an array - as index of Set value is value itself and it is common case to have Object as value
  262. const arrayLikeValue = value instanceof Set ? Array.from(value) : value;
  263. arrayLikeValue.forEach((subValue, index) => {
  264. this.performValidations(value, subValue, index.toString(), [], metadatas, error.children);
  265. });
  266. }
  267. else if (value instanceof Object) {
  268. const targetSchema = typeof metadata.target === 'string' ? metadata.target : metadata.target.name;
  269. this.execute(value, targetSchema, error.children);
  270. }
  271. else {
  272. const [type, message] = this.createValidationError(metadata.target, value, metadata);
  273. error.constraints[type] = message;
  274. }
  275. });
  276. }
  277. mapContexts(object, value, metadatas, error) {
  278. return metadatas.forEach(metadata => {
  279. if (metadata.context) {
  280. let customConstraint;
  281. if (metadata.type === ValidationTypes.CUSTOM_VALIDATION) {
  282. const customConstraints = this.metadataStorage.getTargetValidatorConstraints(metadata.constraintCls);
  283. customConstraint = customConstraints[0];
  284. }
  285. const type = this.getConstraintType(metadata, customConstraint);
  286. if (error.constraints[type]) {
  287. if (!error.contexts) {
  288. error.contexts = {};
  289. }
  290. error.contexts[type] = Object.assign(error.contexts[type] || {}, metadata.context);
  291. }
  292. }
  293. });
  294. }
  295. createValidationError(object, value, metadata, customValidatorMetadata) {
  296. const targetName = object.constructor ? object.constructor.name : undefined;
  297. const type = this.getConstraintType(metadata, customValidatorMetadata);
  298. const validationArguments = {
  299. targetName: targetName,
  300. property: metadata.propertyName,
  301. object: object,
  302. value: value,
  303. constraints: metadata.constraints,
  304. };
  305. let message = metadata.message || '';
  306. if (!metadata.message &&
  307. (!this.validatorOptions || (this.validatorOptions && !this.validatorOptions.dismissDefaultMessages))) {
  308. if (customValidatorMetadata && customValidatorMetadata.instance.defaultMessage instanceof Function) {
  309. message = customValidatorMetadata.instance.defaultMessage(validationArguments);
  310. }
  311. }
  312. const messageString = ValidationUtils.replaceMessageSpecialTokens(message, validationArguments);
  313. return [type, messageString];
  314. }
  315. getConstraintType(metadata, customValidatorMetadata) {
  316. const type = customValidatorMetadata && customValidatorMetadata.name ? customValidatorMetadata.name : metadata.type;
  317. return type;
  318. }
  319. }
  320. //# sourceMappingURL=ValidationExecutor.js.map