validate-custom-fields-config.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. import { Type } from '@vendure/common/lib/shared-types';
  2. import { getMetadataArgsStorage } from 'typeorm';
  3. import { CustomFieldConfig, CustomFields } from '../config/custom-field/custom-field-types';
  4. import { VendureEntity } from './base/base.entity';
  5. function validateCustomFieldsForEntity(
  6. entity: Type<VendureEntity>,
  7. customFields: CustomFieldConfig[],
  8. ): string[] {
  9. return [
  10. ...assertValidFieldNames(entity.name, customFields),
  11. ...assertNoNameConflictsWithEntity(entity, customFields),
  12. ...assertNoDuplicatedCustomFieldNames(entity.name, customFields),
  13. ...assetNonNullablesHaveDefaults(entity.name, customFields),
  14. ...(isTranslatable(entity) ? [] : assertNoLocaleStringFields(entity.name, customFields)),
  15. ];
  16. }
  17. /**
  18. * Assert that the custom entity names are valid
  19. */
  20. function assertValidFieldNames(entityName: string, customFields: CustomFieldConfig[]): string[] {
  21. const errors: string[] = [];
  22. const validNameRe = /^[a-zA-Z_]+[a-zA-Z0-9_]*$/;
  23. for (const field of customFields) {
  24. if (!validNameRe.test(field.name)) {
  25. errors.push(`${entityName} entity has an invalid custom field name: "${field.name}"`);
  26. }
  27. }
  28. return errors;
  29. }
  30. function assertNoNameConflictsWithEntity(entity: Type<any>, customFields: CustomFieldConfig[]): string[] {
  31. const errors: string[] = [];
  32. for (const field of customFields) {
  33. const conflicts = (e: Type<any>): boolean => {
  34. return -1 < getAllColumnNames(e).findIndex(name => name === field.name);
  35. };
  36. const translation = getEntityTranslation(entity);
  37. if (conflicts(entity) || (translation && conflicts(translation))) {
  38. errors.push(`${entity.name} entity already has a field named "${field.name}"`);
  39. }
  40. }
  41. return errors;
  42. }
  43. /**
  44. * Assert that none of the custom field names conflict with one another.
  45. */
  46. function assertNoDuplicatedCustomFieldNames(entityName: string, customFields: CustomFieldConfig[]): string[] {
  47. const nameCounts = customFields
  48. .map(f => f.name)
  49. .reduce(
  50. (hash, name) => {
  51. hash[name] ? hash[name]++ : (hash[name] = 1);
  52. return hash;
  53. },
  54. {} as { [name: string]: number },
  55. );
  56. return Object.entries(nameCounts)
  57. .filter(([name, count]) => 1 < count)
  58. .map(([name, count]) => `${entityName} entity has duplicated custom field name: "${name}"`);
  59. }
  60. /**
  61. * For entities which are not localized (Address, Customer), we assert that none of the custom fields
  62. * have a type "localeString".
  63. */
  64. function assertNoLocaleStringFields(entityName: string, customFields: CustomFieldConfig[]): string[] {
  65. if (!!customFields.find(f => f.type === 'localeString')) {
  66. return [`${entityName} entity does not support custom fields of type "localeString"`];
  67. }
  68. return [];
  69. }
  70. /**
  71. * Assert that any non-nullable field must have a defaultValue specified.
  72. */
  73. function assetNonNullablesHaveDefaults(entityName: string, customFields: CustomFieldConfig[]): string[] {
  74. const errors: string[] = [];
  75. for (const field of customFields) {
  76. if (field.nullable === false && field.defaultValue === undefined) {
  77. errors.push(
  78. `${entityName} entity custom field "${field.name}" is non-nullable and must have a defaultValue`,
  79. );
  80. }
  81. }
  82. return errors;
  83. }
  84. function isTranslatable(entity: Type<any>): boolean {
  85. return !!getEntityTranslation(entity);
  86. }
  87. function getEntityTranslation(entity: Type<any>): Type<any> | undefined {
  88. const metadata = getMetadataArgsStorage();
  89. const translation = metadata.filterRelations(entity).find(r => r.propertyName === 'translations');
  90. if (translation) {
  91. const type = translation.type;
  92. if (typeof type === 'function') {
  93. return type();
  94. }
  95. }
  96. }
  97. function getAllColumnNames(entity: Type<any>): string[] {
  98. const metadata = getMetadataArgsStorage();
  99. const ownColumns = metadata.filterColumns(entity);
  100. const relationColumns = metadata.filterRelations(entity);
  101. const embeddedColumns = metadata.filterEmbeddeds(entity);
  102. const baseColumns = metadata.filterColumns(VendureEntity);
  103. return [...ownColumns, ...relationColumns, ...embeddedColumns, ...baseColumns].map(c => c.propertyName);
  104. }
  105. /**
  106. * Validates the custom fields config, e.g. by ensuring that there are no naming conflicts with the built-in fields
  107. * of each entity.
  108. */
  109. export function validateCustomFieldsConfig(
  110. customFieldConfig: CustomFields,
  111. entities: Array<Type<any>>,
  112. ): { valid: boolean; errors: string[] } {
  113. let errors: string[] = [];
  114. getMetadataArgsStorage();
  115. for (const key of Object.keys(customFieldConfig)) {
  116. const entityName = key as keyof CustomFields;
  117. const customEntityFields = customFieldConfig[entityName] || [];
  118. const entity = entities.find(e => e.name === entityName);
  119. if (entity && customEntityFields.length) {
  120. errors = errors.concat(validateCustomFieldsForEntity(entity, customEntityFields));
  121. }
  122. }
  123. return {
  124. valid: errors.length === 0,
  125. errors,
  126. };
  127. }