diff --git a/packages/core/mock-data/clear-all-tables.ts b/packages/core/mock-data/clear-all-tables.ts index 288033a708..6ef807b844 100644 --- a/packages/core/mock-data/clear-all-tables.ts +++ b/packages/core/mock-data/clear-all-tables.ts @@ -1,7 +1,7 @@ import { createConnection } from 'typeorm'; import { isTestEnvironment } from '../e2e/utils/test-environment'; -import { registerCustomEntityFields } from '../src/entity/custom-entity-fields'; +import { registerCustomEntityFields } from '../src/entity/register-custom-entity-fields'; // tslint:disable:no-console // tslint:disable:no-floating-promises diff --git a/packages/core/src/app.module.ts b/packages/core/src/app.module.ts index 8bf701942f..cb428a6f4e 100644 --- a/packages/core/src/app.module.ts +++ b/packages/core/src/app.module.ts @@ -6,7 +6,7 @@ import { ApiModule } from './api/api.module'; import { ConfigModule } from './config/config.module'; import { ConfigService } from './config/config.service'; import { Logger } from './config/logger/vendure-logger'; -import { validateCustomFieldsConfig } from './entity/custom-entity-fields'; +import { validateCustomFieldsConfig } from './entity/validate-custom-fields-config'; import { I18nModule } from './i18n/i18n.module'; import { I18nService } from './i18n/i18n.service'; diff --git a/packages/core/src/bootstrap.ts b/packages/core/src/bootstrap.ts index 8a0c71784c..839c269857 100644 --- a/packages/core/src/bootstrap.ts +++ b/packages/core/src/bootstrap.ts @@ -10,7 +10,7 @@ import { getConfig, setConfig } from './config/config-helpers'; import { DefaultLogger } from './config/logger/default-logger'; import { Logger } from './config/logger/vendure-logger'; import { VendureConfig } from './config/vendure-config'; -import { registerCustomEntityFields } from './entity/custom-entity-fields'; +import { registerCustomEntityFields } from './entity/register-custom-entity-fields'; import { logProxyMiddlewares } from './plugin/plugin-utils'; export type VendureBootstrapFunction = (config: VendureConfig) => Promise; diff --git a/packages/core/src/entity/custom-entity-fields.ts b/packages/core/src/entity/custom-entity-fields.ts index dcbe334444..b7c97b35e3 100644 --- a/packages/core/src/entity/custom-entity-fields.ts +++ b/packages/core/src/entity/custom-entity-fields.ts @@ -1,12 +1,3 @@ -import { CustomFieldType, Type } from '@vendure/common/lib/shared-types'; -import { assertNever } from '@vendure/common/lib/shared-utils'; -import { Column, ColumnType, Connection, ConnectionOptions, getConnection } from 'typeorm'; - -import { CustomFieldConfig, CustomFields } from '../config/custom-field/custom-field-types'; -import { VendureConfig } from '../config/vendure-config'; - -import { VendureEntity } from './base/base.entity'; - export class CustomAddressFields {} export class CustomFacetFields {} export class CustomFacetFieldsTranslation {} @@ -26,152 +17,3 @@ export class CustomProductVariantFields {} export class CustomProductVariantFieldsTranslation {} export class CustomUserFields {} export class CustomGlobalSettingsFields {} - -/** - * Dynamically add columns to the custom field entity based on the CustomFields config. - */ -function registerCustomFieldsForEntity( - config: VendureConfig, - entityName: keyof CustomFields, - // tslint:disable-next-line:callable-types - 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(dbEngine, type), name, nullable: true })(new ctor(), name); - - if (translation) { - if (type === 'localeString') { - registerColumn(); - } - } else { - if (type !== 'localeString') { - registerColumn(); - } - } - } - } -} - -function getColumnType(dbEngine: ConnectionOptions['type'], type: CustomFieldType): ColumnType { - switch (type) { - case 'string': - case 'localeString': - return 'varchar'; - case 'boolean': - return dbEngine === 'mysql' ? 'tinyint' : 'bool'; - case 'int': - return 'int'; - case 'float': - return 'double'; - case 'datetime': - return dbEngine === 'mysql' ? 'datetime' : 'timestamp'; - default: - assertNever(type); - } - return 'varchar'; -} - -function validateCustomFieldsForEntity( - connection: Connection, - entity: Type, - 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, 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, 'Collection', CustomCollectionFields); - registerCustomFieldsForEntity(config, 'Collection', CustomCollectionFieldsTranslation, true); - registerCustomFieldsForEntity(config, 'Customer', CustomCustomerFields); - registerCustomFieldsForEntity(config, 'Facet', CustomFacetFields); - registerCustomFieldsForEntity(config, 'Facet', CustomFacetFieldsTranslation, true); - registerCustomFieldsForEntity(config, 'FacetValue', CustomFacetValueFields); - registerCustomFieldsForEntity(config, 'FacetValue', CustomFacetValueFieldsTranslation, true); - registerCustomFieldsForEntity(config, 'OrderLine', CustomOrderLineFields); - 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); - registerCustomFieldsForEntity(config, 'GlobalSettings', CustomGlobalSettingsFields); -} - -/** - * Validates the custom fields config, e.g. by ensuring that there are no naming conflicts with the built-in fields - * of each entity. - */ -export async function validateCustomFieldsConfig(customFieldConfig: CustomFields) { - const connection = getConnection(); - // dynamic import to avoid bootstrap-time order of loading issues - const { coreEntitiesMap } = await import('./entities'); - - 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); - } -} diff --git a/packages/core/src/entity/register-custom-entity-fields.ts b/packages/core/src/entity/register-custom-entity-fields.ts new file mode 100644 index 0000000000..a5915feb71 --- /dev/null +++ b/packages/core/src/entity/register-custom-entity-fields.ts @@ -0,0 +1,110 @@ +import { CustomFieldType } from '@vendure/common/lib/shared-types'; +import { assertNever } from '@vendure/common/lib/shared-utils'; +import { Column, ColumnType, ConnectionOptions } from 'typeorm'; + +import { CustomFields } from '../config/custom-field/custom-field-types'; +import { VendureConfig } from '../config/vendure-config'; + +import { + CustomAddressFields, + CustomCollectionFields, + CustomCollectionFieldsTranslation, + CustomCustomerFields, + CustomFacetFields, + CustomFacetFieldsTranslation, + CustomFacetValueFields, + CustomFacetValueFieldsTranslation, + CustomGlobalSettingsFields, + CustomOrderLineFields, + CustomProductFields, + CustomProductFieldsTranslation, + CustomProductOptionFields, + CustomProductOptionFieldsTranslation, + CustomProductOptionGroupFields, + CustomProductOptionGroupFieldsTranslation, + CustomProductVariantFields, + CustomProductVariantFieldsTranslation, + CustomUserFields, +} from './custom-entity-fields'; + +/** + * Dynamically add columns to the custom field entity based on the CustomFields config. + */ +function registerCustomFieldsForEntity( + config: VendureConfig, + entityName: keyof CustomFields, + // tslint:disable-next-line:callable-types + 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(dbEngine, type), name, nullable: true })(new ctor(), name); + + if (translation) { + if (type === 'localeString') { + registerColumn(); + } + } else { + if (type !== 'localeString') { + registerColumn(); + } + } + } + } +} + +function getColumnType(dbEngine: ConnectionOptions['type'], type: CustomFieldType): ColumnType { + switch (type) { + case 'string': + case 'localeString': + return 'varchar'; + case 'boolean': + return dbEngine === 'mysql' ? 'tinyint' : 'bool'; + case 'int': + return 'int'; + case 'float': + return 'double'; + case 'datetime': + return dbEngine === 'mysql' ? 'datetime' : 'timestamp'; + default: + assertNever(type); + } + return 'varchar'; +} + +/** + * 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, 'Collection', CustomCollectionFields); + registerCustomFieldsForEntity(config, 'Collection', CustomCollectionFieldsTranslation, true); + registerCustomFieldsForEntity(config, 'Customer', CustomCustomerFields); + registerCustomFieldsForEntity(config, 'Facet', CustomFacetFields); + registerCustomFieldsForEntity(config, 'Facet', CustomFacetFieldsTranslation, true); + registerCustomFieldsForEntity(config, 'FacetValue', CustomFacetValueFields); + registerCustomFieldsForEntity(config, 'FacetValue', CustomFacetValueFieldsTranslation, true); + registerCustomFieldsForEntity(config, 'OrderLine', CustomOrderLineFields); + 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); + registerCustomFieldsForEntity(config, 'GlobalSettings', CustomGlobalSettingsFields); +} + \ No newline at end of file diff --git a/packages/core/src/entity/validate-custom-fields-config.ts b/packages/core/src/entity/validate-custom-fields-config.ts new file mode 100644 index 0000000000..bb76602c1f --- /dev/null +++ b/packages/core/src/entity/validate-custom-fields-config.ts @@ -0,0 +1,74 @@ +import { Connection, getConnection } from 'typeorm'; + +import { Type } from '../../../common/lib/shared-types'; +import { CustomFieldConfig, CustomFields } from '../config/custom-field/custom-field-types'; + +import { VendureEntity } from './base/base.entity'; + +function validateCustomFieldsForEntity( + connection: Connection, + entity: Type, + 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, 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); + } +} + +/** + * Validates the custom fields config, e.g. by ensuring that there are no naming conflicts with the built-in fields + * of each entity. + */ +export async function validateCustomFieldsConfig(customFieldConfig: CustomFields) { + const connection = getConnection(); + // dynamic import to avoid bootstrap-time order of loading issues + const {coreEntitiesMap} = await import('./entities'); + + 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); + } +}