Skip to content

Commit

Permalink
refactor(core): Split custom fields functions into separate files
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelbromley committed Jul 11, 2019
1 parent b444a5a commit 090758c
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 161 deletions.
2 changes: 1 addition & 1 deletion packages/core/mock-data/clear-all-tables.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<INestApplication>;
Expand Down
158 changes: 0 additions & 158 deletions packages/core/src/entity/custom-entity-fields.ts
Original file line number Diff line number Diff line change
@@ -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 {}
Expand All @@ -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<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, '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);
}
}
110 changes: 110 additions & 0 deletions packages/core/src/entity/register-custom-entity-fields.ts
Original file line number Diff line number Diff line change
@@ -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);
}

74 changes: 74 additions & 0 deletions packages/core/src/entity/validate-custom-fields-config.ts
Original file line number Diff line number Diff line change
@@ -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<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);
}
}

/**
* 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);
}
}

0 comments on commit 090758c

Please sign in to comment.