custom-entity-fields.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import { CustomFieldConfig, CustomFields, CustomFieldType, Type } from 'shared/shared-types';
  2. import { assertNever } from 'shared/shared-utils';
  3. import { Column, ColumnType, Connection, ConnectionOptions, Entity, getConnection } from 'typeorm';
  4. import { VendureConfig } from '../config/vendure-config';
  5. import { VendureEntity } from './base/base.entity';
  6. import { coreEntitiesMap } from './entities';
  7. @Entity()
  8. export class CustomAddressFields {}
  9. @Entity()
  10. export class CustomFacetFields {}
  11. @Entity()
  12. export class CustomFacetFieldsTranslation {}
  13. @Entity()
  14. export class CustomFacetValueFields {}
  15. @Entity()
  16. export class CustomFacetValueFieldsTranslation {}
  17. @Entity()
  18. export class CustomCustomerFields {}
  19. @Entity()
  20. export class CustomProductFields {}
  21. @Entity()
  22. export class CustomProductFieldsTranslation {}
  23. @Entity()
  24. export class CustomProductOptionFields {}
  25. @Entity()
  26. export class CustomProductOptionFieldsTranslation {}
  27. @Entity()
  28. export class CustomProductOptionGroupFields {}
  29. @Entity()
  30. export class CustomProductOptionGroupFieldsTranslation {}
  31. @Entity()
  32. export class CustomProductVariantFields {}
  33. @Entity()
  34. export class CustomProductVariantFieldsTranslation {}
  35. @Entity()
  36. export class CustomUserFields {}
  37. /**
  38. * Dynamically add columns to the custom field entity based on the CustomFields config.
  39. */
  40. function registerCustomFieldsForEntity(
  41. config: VendureConfig,
  42. entityName: keyof CustomFields,
  43. ctor: { new (): any },
  44. translation = false,
  45. ) {
  46. const customFields = config.customFields && config.customFields[entityName];
  47. const dbEngine = config.dbConnectionOptions.type;
  48. if (customFields) {
  49. for (const customField of customFields) {
  50. const { name, type } = customField;
  51. const registerColumn = () =>
  52. Column({ type: getColumnType(dbEngine, type), name })(new ctor(), name);
  53. if (translation) {
  54. if (type === 'localeString') {
  55. registerColumn();
  56. }
  57. } else {
  58. if (type !== 'localeString') {
  59. registerColumn();
  60. }
  61. }
  62. }
  63. }
  64. }
  65. function getColumnType(dbEngine: ConnectionOptions['type'], type: CustomFieldType): ColumnType {
  66. switch (type) {
  67. case 'string':
  68. case 'localeString':
  69. return 'varchar';
  70. case 'boolean':
  71. return dbEngine === 'mysql' ? 'tinyint' : 'bool';
  72. case 'int':
  73. return 'int';
  74. case 'float':
  75. return 'double';
  76. case 'datetime':
  77. return dbEngine === 'mysql' ? 'datetime' : 'timestamp';
  78. default:
  79. assertNever(type);
  80. }
  81. return 'varchar';
  82. }
  83. function validateCustomFieldsForEntity(
  84. connection: Connection,
  85. entity: Type<VendureEntity>,
  86. customFields: CustomFieldConfig[],
  87. ): void {
  88. const metadata = connection.getMetadata(entity);
  89. const { relations } = metadata;
  90. const translationRelation = relations.find(r => r.propertyName === 'translations');
  91. if (translationRelation) {
  92. const translationEntity = translationRelation.type;
  93. const translationPropMap = connection.getMetadata(translationEntity).createPropertiesMap();
  94. const localeStringFields = customFields.filter(field => field.type === 'localeString');
  95. assertNoNameConflicts(entity.name, translationPropMap, localeStringFields);
  96. } else {
  97. assertNoLocaleStringFields(entity, customFields);
  98. }
  99. const nonLocaleStringFields = customFields.filter(field => field.type !== 'localeString');
  100. const propMap = metadata.createPropertiesMap();
  101. assertNoNameConflicts(entity.name, propMap, nonLocaleStringFields);
  102. }
  103. /**
  104. * Assert that none of the custom field names conflict with existing properties of the entity, as provided
  105. * by the TypeORM PropertiesMap object.
  106. */
  107. function assertNoNameConflicts(entityName: string, propMap: object, customFields: CustomFieldConfig[]): void {
  108. for (const customField of customFields) {
  109. if (propMap.hasOwnProperty(customField.name)) {
  110. const message = `Custom field name conflict: the "${entityName}" entity already has a built-in property "${
  111. customField.name
  112. }".`;
  113. throw new Error(message);
  114. }
  115. }
  116. }
  117. /**
  118. * For entities which are not localized (Address, Customer), we assert that none of the custom fields
  119. * have a type "localeString".
  120. */
  121. function assertNoLocaleStringFields(entity: Type<any>, customFields: CustomFieldConfig[]): void {
  122. if (!!customFields.find(f => f.type === 'localeString')) {
  123. const message = `Custom field type error: the "${
  124. entity.name
  125. }" entity does not support the "localeString" type.`;
  126. throw new Error(message);
  127. }
  128. }
  129. /**
  130. * Dynamically registers any custom fields with TypeORM. This function should be run at the bootstrap
  131. * stage of the app lifecycle, before the AppModule is initialized.
  132. */
  133. export function registerCustomEntityFields(config: VendureConfig) {
  134. registerCustomFieldsForEntity(config, 'Address', CustomAddressFields);
  135. registerCustomFieldsForEntity(config, 'Customer', CustomCustomerFields);
  136. registerCustomFieldsForEntity(config, 'Facet', CustomFacetFields);
  137. registerCustomFieldsForEntity(config, 'Facet', CustomFacetFieldsTranslation, true);
  138. registerCustomFieldsForEntity(config, 'FacetValue', CustomFacetValueFields);
  139. registerCustomFieldsForEntity(config, 'FacetValue', CustomFacetValueFieldsTranslation, true);
  140. registerCustomFieldsForEntity(config, 'Product', CustomProductFields);
  141. registerCustomFieldsForEntity(config, 'Product', CustomProductFieldsTranslation, true);
  142. registerCustomFieldsForEntity(config, 'ProductOption', CustomProductOptionFields);
  143. registerCustomFieldsForEntity(config, 'ProductOption', CustomProductOptionFieldsTranslation, true);
  144. registerCustomFieldsForEntity(config, 'ProductOptionGroup', CustomProductOptionGroupFields);
  145. registerCustomFieldsForEntity(
  146. config,
  147. 'ProductOptionGroup',
  148. CustomProductOptionGroupFieldsTranslation,
  149. true,
  150. );
  151. registerCustomFieldsForEntity(config, 'ProductVariant', CustomProductVariantFields);
  152. registerCustomFieldsForEntity(config, 'ProductVariant', CustomProductVariantFieldsTranslation, true);
  153. registerCustomFieldsForEntity(config, 'User', CustomUserFields);
  154. }
  155. /**
  156. * Validates the custom fields config, e.g. by ensuring that there are no naming conflicts with the built-in fields
  157. * of each entity.
  158. */
  159. export function validateCustomFieldsConfig(customFieldConfig: CustomFields) {
  160. const connection = getConnection();
  161. for (const key of Object.keys(customFieldConfig)) {
  162. const entityName = key as keyof CustomFields;
  163. const customEntityFields = customFieldConfig[entityName] || [];
  164. const entity = coreEntitiesMap[entityName];
  165. validateCustomFieldsForEntity(connection, entity, customEntityFields);
  166. }
  167. }