|
|
@@ -1,10 +1,11 @@
|
|
|
-import { Column, ColumnType, Entity } from 'typeorm';
|
|
|
+import { Column, ColumnType, Connection, ConnectionOptions, Entity, getConnection } from 'typeorm';
|
|
|
|
|
|
-import { CustomFields, CustomFieldType } from '../../../shared/shared-types';
|
|
|
+import { CustomFieldConfig, CustomFields, CustomFieldType, Type } from '../../../shared/shared-types';
|
|
|
import { assertNever } from '../../../shared/shared-utils';
|
|
|
-import { getConfig } from '../config/vendure-config';
|
|
|
+import { VendureConfig } from '../config/vendure-config';
|
|
|
|
|
|
-const config = getConfig();
|
|
|
+import { VendureEntity } from './base/base.entity';
|
|
|
+import { coreEntitiesMap } from './entities';
|
|
|
|
|
|
@Entity()
|
|
|
export class CustomAddressFields {}
|
|
|
@@ -32,16 +33,19 @@ export class CustomUserFields {}
|
|
|
/**
|
|
|
* Dynamically add columns to the custom field entity based on the CustomFields config.
|
|
|
*/
|
|
|
-function registerEntityCustomFields(
|
|
|
+function registerCustomFieldsForEntity(
|
|
|
+ config: VendureConfig,
|
|
|
entityName: keyof CustomFields,
|
|
|
ctor: { new (): any },
|
|
|
translation = false,
|
|
|
) {
|
|
|
const customFields = config.customFields && config.customFields[entityName];
|
|
|
+ const dbEngine = config.dbConnectionOptions.type;
|
|
|
if (customFields) {
|
|
|
for (const customField of customFields) {
|
|
|
const { name, type } = customField;
|
|
|
- const registerColumn = () => Column({ type: getColumnType(type), name })(new ctor(), name);
|
|
|
+ const registerColumn = () =>
|
|
|
+ Column({ type: getColumnType(dbEngine, type), name })(new ctor(), name);
|
|
|
|
|
|
if (translation) {
|
|
|
if (type === 'localeString') {
|
|
|
@@ -56,8 +60,7 @@ function registerEntityCustomFields(
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function getColumnType(type: CustomFieldType): ColumnType {
|
|
|
- const dbEngine = config.dbConnectionOptions.type;
|
|
|
+function getColumnType(dbEngine: ConnectionOptions['type'], type: CustomFieldType): ColumnType {
|
|
|
switch (type) {
|
|
|
case 'string':
|
|
|
case 'localeString':
|
|
|
@@ -76,14 +79,91 @@ function getColumnType(type: CustomFieldType): ColumnType {
|
|
|
return 'varchar';
|
|
|
}
|
|
|
|
|
|
-registerEntityCustomFields('Address', CustomAddressFields);
|
|
|
-registerEntityCustomFields('Customer', CustomCustomerFields);
|
|
|
-registerEntityCustomFields('Product', CustomProductFields);
|
|
|
-registerEntityCustomFields('Product', CustomProductFieldsTranslation, true);
|
|
|
-registerEntityCustomFields('ProductOption', CustomProductOptionFields);
|
|
|
-registerEntityCustomFields('ProductOption', CustomProductOptionFieldsTranslation, true);
|
|
|
-registerEntityCustomFields('ProductOptionGroup', CustomProductOptionGroupFields);
|
|
|
-registerEntityCustomFields('ProductOptionGroup', CustomProductOptionGroupFieldsTranslation, true);
|
|
|
-registerEntityCustomFields('ProductVariant', CustomProductVariantFields);
|
|
|
-registerEntityCustomFields('ProductVariant', CustomProductVariantFieldsTranslation, true);
|
|
|
-registerEntityCustomFields('User', CustomUserFields);
|
|
|
+function validateCustomFieldsForEntity(
|
|
|
+ connection: Connection,
|
|
|
+ entity: Type<VendureEntity>,
|
|
|
+ customFields: CustomFieldConfig[],
|
|
|
+): void {
|
|
|
+ const metadata = connection.getMetadata(entity);
|
|
|
+ const { relations } = metadata;
|
|
|
+
|
|
|
+ const translationRelation = relations.find(r => r.propertyName === 'translations');
|
|
|
+ if (translationRelation) {
|
|
|
+ const translationEntity = translationRelation.type;
|
|
|
+ const translationPropMap = connection.getMetadata(translationEntity).createPropertiesMap();
|
|
|
+ const localeStringFields = customFields.filter(field => field.type === 'localeString');
|
|
|
+ assertNoNameConflicts(entity.name, translationPropMap, localeStringFields);
|
|
|
+ } else {
|
|
|
+ assertNoLocaleStringFields(entity, customFields);
|
|
|
+ }
|
|
|
+
|
|
|
+ const nonLocaleStringFields = customFields.filter(field => field.type !== 'localeString');
|
|
|
+ const propMap = metadata.createPropertiesMap();
|
|
|
+ assertNoNameConflicts(entity.name, propMap, nonLocaleStringFields);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Assert that none of the custom field names conflict with existing properties of the entity, as provided
|
|
|
+ * by the TypeORM PropertiesMap object.
|
|
|
+ */
|
|
|
+function assertNoNameConflicts(entityName: string, propMap: object, customFields: CustomFieldConfig[]): void {
|
|
|
+ for (const customField of customFields) {
|
|
|
+ if (propMap.hasOwnProperty(customField.name)) {
|
|
|
+ const message = `Custom field name conflict: the "${entityName}" entity already has a built-in property "${
|
|
|
+ customField.name
|
|
|
+ }".`;
|
|
|
+ throw new Error(message);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * For entities which are not localized (Address, Customer), we assert that none of the custom fields
|
|
|
+ * have a type "localeString".
|
|
|
+ */
|
|
|
+function assertNoLocaleStringFields(entity: Type<any>, customFields: CustomFieldConfig[]): void {
|
|
|
+ if (!!customFields.find(f => f.type === 'localeString')) {
|
|
|
+ const message = `Custom field type error: the "${
|
|
|
+ entity.name
|
|
|
+ }" entity does not support the "localeString" type.`;
|
|
|
+ throw new Error(message);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Dynamically registers any custom fields with TypeORM. This function should be run at the bootstrap
|
|
|
+ * stage of the app lifecycle, before the AppModule is initialized.
|
|
|
+ */
|
|
|
+export function registerCustomEntityFields(config: VendureConfig) {
|
|
|
+ registerCustomFieldsForEntity(config, 'Address', CustomAddressFields);
|
|
|
+ registerCustomFieldsForEntity(config, 'Customer', CustomCustomerFields);
|
|
|
+ registerCustomFieldsForEntity(config, 'Product', CustomProductFields);
|
|
|
+ registerCustomFieldsForEntity(config, 'Product', CustomProductFieldsTranslation, true);
|
|
|
+ registerCustomFieldsForEntity(config, 'ProductOption', CustomProductOptionFields);
|
|
|
+ registerCustomFieldsForEntity(config, 'ProductOption', CustomProductOptionFieldsTranslation, true);
|
|
|
+ registerCustomFieldsForEntity(config, 'ProductOptionGroup', CustomProductOptionGroupFields);
|
|
|
+ registerCustomFieldsForEntity(
|
|
|
+ config,
|
|
|
+ 'ProductOptionGroup',
|
|
|
+ CustomProductOptionGroupFieldsTranslation,
|
|
|
+ true,
|
|
|
+ );
|
|
|
+ registerCustomFieldsForEntity(config, 'ProductVariant', CustomProductVariantFields);
|
|
|
+ registerCustomFieldsForEntity(config, 'ProductVariant', CustomProductVariantFieldsTranslation, true);
|
|
|
+ registerCustomFieldsForEntity(config, 'User', CustomUserFields);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Validates the custom fields config, e.g. by ensuring that there are no naming conflicts with the built-in fields
|
|
|
+ * of each entity.
|
|
|
+ */
|
|
|
+export function validateCustomFieldsConfig(customFieldConfig: CustomFields) {
|
|
|
+ const connection = getConnection();
|
|
|
+
|
|
|
+ for (const key of Object.keys(customFieldConfig)) {
|
|
|
+ const entityName = key as keyof CustomFields;
|
|
|
+ const customEntityFields = customFieldConfig[entityName] || [];
|
|
|
+ const entity = coreEntitiesMap[entityName];
|
|
|
+ validateCustomFieldsForEntity(connection, entity, customEntityFields);
|
|
|
+ }
|
|
|
+}
|