validate-custom-field-value.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. import { LanguageCode } from '@vendure/common/lib/generated-types';
  2. import { assertNever } from '@vendure/common/lib/shared-utils';
  3. import { UserInputError } from '../../common/error/errors';
  4. import { Injector } from '../../common/injector';
  5. import {
  6. CustomFieldConfig,
  7. DateTimeCustomFieldConfig,
  8. FloatCustomFieldConfig,
  9. IntCustomFieldConfig,
  10. LocaleStringCustomFieldConfig,
  11. StringCustomFieldConfig,
  12. TypedCustomFieldConfig,
  13. } from '../../config/custom-field/custom-field-types';
  14. /**
  15. * Validates the value of a custom field input against any configured constraints.
  16. * If validation fails, an error is thrown.
  17. */
  18. export async function validateCustomFieldValue(
  19. config: CustomFieldConfig,
  20. value: any | any[],
  21. injector: Injector,
  22. languageCode?: LanguageCode,
  23. ): Promise<void> {
  24. if (config.readonly) {
  25. throw new UserInputError('error.field-invalid-readonly', { name: config.name });
  26. }
  27. if (config.nullable === false) {
  28. if (value === null) {
  29. throw new UserInputError('error.field-invalid-non-nullable', {
  30. name: config.name,
  31. });
  32. }
  33. }
  34. if (config.list === true && Array.isArray(value)) {
  35. for (const singleValue of value) {
  36. validateSingleValue(config, singleValue);
  37. }
  38. } else {
  39. validateSingleValue(config, value);
  40. }
  41. await validateCustomFunction(config as TypedCustomFieldConfig<any, any>, value, injector, languageCode);
  42. }
  43. function validateSingleValue(config: CustomFieldConfig, value: any) {
  44. switch (config.type) {
  45. case 'string':
  46. case 'localeString':
  47. validateStringField(config, value);
  48. break;
  49. case 'int':
  50. case 'float':
  51. validateNumberField(config, value);
  52. break;
  53. case 'datetime':
  54. validateDateTimeField(config, value);
  55. break;
  56. case 'boolean':
  57. case 'relation':
  58. case 'text':
  59. case 'localeText':
  60. break;
  61. default:
  62. assertNever(config);
  63. }
  64. }
  65. async function validateCustomFunction<T extends TypedCustomFieldConfig<any, any>>(
  66. config: T,
  67. value: any,
  68. injector: Injector,
  69. languageCode?: LanguageCode,
  70. ) {
  71. if (typeof config.validate === 'function') {
  72. const error = await config.validate(value, injector);
  73. if (typeof error === 'string') {
  74. throw new UserInputError(error);
  75. }
  76. if (Array.isArray(error)) {
  77. const localizedError = error.find(e => e.languageCode === languageCode) || error[0];
  78. throw new UserInputError(localizedError.value);
  79. }
  80. }
  81. }
  82. function validateStringField(
  83. config: StringCustomFieldConfig | LocaleStringCustomFieldConfig,
  84. value: string,
  85. ): void {
  86. const { pattern } = config;
  87. if (pattern) {
  88. const re = new RegExp(pattern);
  89. if (!re.test(value)) {
  90. throw new UserInputError('error.field-invalid-string-pattern', {
  91. name: config.name,
  92. value,
  93. pattern,
  94. });
  95. }
  96. }
  97. const options = (config as StringCustomFieldConfig).options;
  98. if (options) {
  99. const validOptions = options.map(o => o.value);
  100. if (value === null && config.nullable === true) {
  101. return;
  102. }
  103. if (!validOptions.includes(value)) {
  104. throw new UserInputError('error.field-invalid-string-option', {
  105. name: config.name,
  106. value,
  107. validOptions: validOptions.map(o => `'${o}'`).join(', '),
  108. });
  109. }
  110. }
  111. }
  112. function validateNumberField(config: IntCustomFieldConfig | FloatCustomFieldConfig, value: number): void {
  113. const { min, max } = config;
  114. if (min != null && value < min) {
  115. throw new UserInputError('error.field-invalid-number-range-min', { name: config.name, value, min });
  116. }
  117. if (max != null && max < value) {
  118. throw new UserInputError('error.field-invalid-number-range-max', { name: config.name, value, max });
  119. }
  120. }
  121. function validateDateTimeField(config: DateTimeCustomFieldConfig, value: string): void {
  122. const { min, max } = config;
  123. const valueDate = new Date(value);
  124. if (min != null && valueDate < new Date(min)) {
  125. throw new UserInputError('error.field-invalid-datetime-range-min', {
  126. name: config.name,
  127. value: valueDate.toISOString(),
  128. min,
  129. });
  130. }
  131. if (max != null && new Date(max) < valueDate) {
  132. throw new UserInputError('error.field-invalid-datetime-range-max', {
  133. name: config.name,
  134. value: valueDate.toISOString(),
  135. max,
  136. });
  137. }
  138. }