validate-custom-fields-config.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  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. if (hash[name]) {
  52. hash[name]++;
  53. } else {
  54. hash[name] = 1;
  55. }
  56. return hash;
  57. },
  58. {} as { [name: string]: number },
  59. );
  60. return Object.entries(nameCounts)
  61. .filter(([name, count]) => 1 < count)
  62. .map(([name, count]) => `${entityName} entity has duplicated custom field name: "${name}"`);
  63. }
  64. /**
  65. * For entities which are not localized (Address, Customer), we assert that none of the custom fields
  66. * have a type "localeString".
  67. */
  68. function assertNoLocaleStringFields(entityName: string, customFields: CustomFieldConfig[]): string[] {
  69. if (!!customFields.find(f => f.type === 'localeString')) {
  70. return [`${entityName} entity does not support custom fields of type "localeString"`];
  71. }
  72. return [];
  73. }
  74. /**
  75. * Assert that any non-nullable field must have a defaultValue specified.
  76. */
  77. function assetNonNullablesHaveDefaults(entityName: string, customFields: CustomFieldConfig[]): string[] {
  78. const errors: string[] = [];
  79. for (const field of customFields) {
  80. if (field.nullable === false && field.defaultValue === undefined) {
  81. errors.push(
  82. `${entityName} entity custom field "${field.name}" is non-nullable and must have a defaultValue`,
  83. );
  84. }
  85. }
  86. return errors;
  87. }
  88. function isTranslatable(entity: Type<any>): boolean {
  89. return !!getEntityTranslation(entity);
  90. }
  91. function getEntityTranslation(entity: Type<any>): Type<any> | undefined {
  92. const metadata = getMetadataArgsStorage();
  93. const translation = metadata.filterRelations(entity).find(r => r.propertyName === 'translations');
  94. if (translation) {
  95. const type = translation.type;
  96. if (typeof type === 'function') {
  97. // See https://github.com/microsoft/TypeScript/issues/37663
  98. return (type as any)();
  99. }
  100. }
  101. }
  102. function getAllColumnNames(entity: Type<any>): string[] {
  103. const metadata = getMetadataArgsStorage();
  104. const ownColumns = metadata.filterColumns(entity);
  105. const relationColumns = metadata.filterRelations(entity);
  106. const embeddedColumns = metadata.filterEmbeddeds(entity);
  107. const baseColumns = metadata.filterColumns(VendureEntity);
  108. return [...ownColumns, ...relationColumns, ...embeddedColumns, ...baseColumns].map(c => c.propertyName);
  109. }
  110. /**
  111. * Validates the custom fields config, e.g. by ensuring that there are no naming conflicts with the built-in fields
  112. * of each entity.
  113. */
  114. export function validateCustomFieldsConfig(
  115. customFieldConfig: CustomFields,
  116. entities: Array<Type<any>>,
  117. ): { valid: boolean; errors: string[] } {
  118. let errors: string[] = [];
  119. getMetadataArgsStorage();
  120. for (const key of Object.keys(customFieldConfig)) {
  121. const entityName = key as keyof CustomFields;
  122. const customEntityFields = customFieldConfig[entityName] || [];
  123. const entity = entities.find(e => e.name === entityName);
  124. if (entity && customEntityFields.length) {
  125. errors = errors.concat(validateCustomFieldsForEntity(entity, customEntityFields));
  126. }
  127. }
  128. return {
  129. valid: errors.length === 0,
  130. errors,
  131. };
  132. }