Browse Source

feat(core): Add nullable & defaultValue options to custom fields config

Relates to #85
Michael Bromley 6 years ago
parent
commit
b1722d8986

+ 57 - 49
admin-ui/src/app/common/generated-types.ts

@@ -937,6 +937,8 @@ export type CustomFieldConfig = {
   __typename?: 'CustomFieldConfig',
   name: Scalars['String'],
   type: Scalars['String'],
+  label?: Maybe<Array<LocalizedString>>,
+  description?: Maybe<Array<LocalizedString>>,
 };
 
 export type CustomFields = {
@@ -1554,6 +1556,12 @@ export enum LanguageCode {
   zu = 'zu'
 }
 
+export type LocalizedString = {
+  __typename?: 'LocalizedString',
+  languageCode: LanguageCode,
+  value: Scalars['String'],
+};
+
 export type LoginResult = {
   __typename?: 'LoginResult',
   user: CurrentUser,
@@ -1567,14 +1575,14 @@ export type MoveCollectionInput = {
 
 export type Mutation = {
   __typename?: 'Mutation',
+  /** Create a new Asset */
+  createAssets: Array<Asset>,
   /** Create a new Administrator */
   createAdministrator: Administrator,
   /** Update an existing Administrator */
   updateAdministrator: Administrator,
   /** Assign a Role to an Administrator */
   assignRoleToAdministrator: Administrator,
-  /** Create a new Asset */
-  createAssets: Array<Asset>,
   login: LoginResult,
   logout: Scalars['Boolean'],
   /** Create a new Channel */
@@ -1603,8 +1611,6 @@ export type Mutation = {
   addCustomersToGroup: CustomerGroup,
   /** Remove Customers from a CustomerGroup */
   removeCustomersFromGroup: CustomerGroup,
-  updateGlobalSettings: GlobalSettings,
-  importProducts?: Maybe<ImportInfo>,
   /** Create a new Customer. If a password is provided, a new User will also be created an linked to the Customer. */
   createCustomer: Customer,
   /** Update an existing Customer */
@@ -1629,6 +1635,8 @@ export type Mutation = {
   updateFacetValues: Array<FacetValue>,
   /** Delete one or more FacetValues */
   deleteFacetValues: Array<DeletionResponse>,
+  updateGlobalSettings: GlobalSettings,
+  importProducts?: Maybe<ImportInfo>,
   settlePayment: Payment,
   fulfillOrder: Fulfillment,
   cancelOrder: Order,
@@ -1677,10 +1685,6 @@ export type Mutation = {
   createTaxCategory: TaxCategory,
   /** Update an existing TaxCategory */
   updateTaxCategory: TaxCategory,
-  /** Create a new TaxRate */
-  createTaxRate: TaxRate,
-  /** Update an existing TaxRate */
-  updateTaxRate: TaxRate,
   /** Create a new Zone */
   createZone: Zone,
   /** Update an existing Zone */
@@ -1691,6 +1695,10 @@ export type Mutation = {
   addMembersToZone: Zone,
   /** Remove members from a Zone */
   removeMembersFromZone: Zone,
+  /** Create a new TaxRate */
+  createTaxRate: TaxRate,
+  /** Update an existing TaxRate */
+  updateTaxRate: TaxRate,
   requestStarted: Scalars['Int'],
   requestCompleted: Scalars['Int'],
   setAsLoggedIn: UserStatus,
@@ -1699,6 +1707,11 @@ export type Mutation = {
 };
 
 
+export type MutationCreateAssetsArgs = {
+  input: Array<CreateAssetInput>
+};
+
+
 export type MutationCreateAdministratorArgs = {
   input: CreateAdministratorInput
 };
@@ -1715,11 +1728,6 @@ export type MutationAssignRoleToAdministratorArgs = {
 };
 
 
-export type MutationCreateAssetsArgs = {
-  input: Array<CreateAssetInput>
-};
-
-
 export type MutationLoginArgs = {
   username: Scalars['String'],
   password: Scalars['String'],
@@ -1794,16 +1802,6 @@ export type MutationRemoveCustomersFromGroupArgs = {
 };
 
 
-export type MutationUpdateGlobalSettingsArgs = {
-  input: UpdateGlobalSettingsInput
-};
-
-
-export type MutationImportProductsArgs = {
-  csvFile: Scalars['Upload']
-};
-
-
 export type MutationCreateCustomerArgs = {
   input: CreateCustomerInput,
   password?: Maybe<Scalars['String']>
@@ -1868,6 +1866,16 @@ export type MutationDeleteFacetValuesArgs = {
 };
 
 
+export type MutationUpdateGlobalSettingsArgs = {
+  input: UpdateGlobalSettingsInput
+};
+
+
+export type MutationImportProductsArgs = {
+  csvFile: Scalars['Upload']
+};
+
+
 export type MutationSettlePaymentArgs = {
   id: Scalars['ID']
 };
@@ -2010,16 +2018,6 @@ export type MutationUpdateTaxCategoryArgs = {
 };
 
 
-export type MutationCreateTaxRateArgs = {
-  input: CreateTaxRateInput
-};
-
-
-export type MutationUpdateTaxRateArgs = {
-  input: UpdateTaxRateInput
-};
-
-
 export type MutationCreateZoneArgs = {
   input: CreateZoneInput
 };
@@ -2047,6 +2045,16 @@ export type MutationRemoveMembersFromZoneArgs = {
 };
 
 
+export type MutationCreateTaxRateArgs = {
+  input: CreateTaxRateInput
+};
+
+
+export type MutationUpdateTaxRateArgs = {
+  input: UpdateTaxRateInput
+};
+
+
 export type MutationSetAsLoggedInArgs = {
   username: Scalars['String'],
   loginTime: Scalars['String']
@@ -2558,10 +2566,10 @@ export type PromotionSortParameter = {
 
 export type Query = {
   __typename?: 'Query',
-  administrators: AdministratorList,
-  administrator?: Maybe<Administrator>,
   assets: AssetList,
   asset?: Maybe<Asset>,
+  administrators: AdministratorList,
+  administrator?: Maybe<Administrator>,
   me?: Maybe<CurrentUser>,
   channels: Array<Channel>,
   channel?: Maybe<Channel>,
@@ -2573,11 +2581,11 @@ export type Query = {
   country?: Maybe<Country>,
   customerGroups: Array<CustomerGroup>,
   customerGroup?: Maybe<CustomerGroup>,
-  globalSettings: GlobalSettings,
   customers: CustomerList,
   customer?: Maybe<Customer>,
   facets: FacetList,
   facet?: Maybe<Facet>,
+  globalSettings: GlobalSettings,
   job?: Maybe<JobInfo>,
   jobs: Array<JobInfo>,
   order?: Maybe<Order>,
@@ -2601,32 +2609,32 @@ export type Query = {
   shippingCalculators: Array<ConfigurableOperation>,
   taxCategories: Array<TaxCategory>,
   taxCategory?: Maybe<TaxCategory>,
-  taxRates: TaxRateList,
-  taxRate?: Maybe<TaxRate>,
   zones: Array<Zone>,
   zone?: Maybe<Zone>,
+  taxRates: TaxRateList,
+  taxRate?: Maybe<TaxRate>,
   networkStatus: NetworkStatus,
   userStatus: UserStatus,
   uiState: UiState,
 };
 
 
-export type QueryAdministratorsArgs = {
-  options?: Maybe<AdministratorListOptions>
+export type QueryAssetsArgs = {
+  options?: Maybe<AssetListOptions>
 };
 
 
-export type QueryAdministratorArgs = {
+export type QueryAssetArgs = {
   id: Scalars['ID']
 };
 
 
-export type QueryAssetsArgs = {
-  options?: Maybe<AssetListOptions>
+export type QueryAdministratorsArgs = {
+  options?: Maybe<AdministratorListOptions>
 };
 
 
-export type QueryAssetArgs = {
+export type QueryAdministratorArgs = {
   id: Scalars['ID']
 };
 
@@ -2780,17 +2788,17 @@ export type QueryTaxCategoryArgs = {
 };
 
 
-export type QueryTaxRatesArgs = {
-  options?: Maybe<TaxRateListOptions>
+export type QueryZoneArgs = {
+  id: Scalars['ID']
 };
 
 
-export type QueryTaxRateArgs = {
-  id: Scalars['ID']
+export type QueryTaxRatesArgs = {
+  options?: Maybe<TaxRateListOptions>
 };
 
 
-export type QueryZoneArgs = {
+export type QueryTaxRateArgs = {
   id: Scalars['ID']
 };
 

+ 8 - 0
packages/common/src/generated-shop-types.ts

@@ -659,6 +659,8 @@ export type CustomFieldConfig = {
     __typename?: 'CustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    label?: Maybe<Array<LocalizedString>>;
+    description?: Maybe<Array<LocalizedString>>;
 };
 
 export type CustomFields = {
@@ -1205,6 +1207,12 @@ export enum LanguageCode {
     zu = 'zu',
 }
 
+export type LocalizedString = {
+    __typename?: 'LocalizedString';
+    languageCode: LanguageCode;
+    value: Scalars['String'];
+};
+
 export type LoginResult = {
     __typename?: 'LoginResult';
     user: CurrentUser;

+ 57 - 49
packages/common/src/generated-types.ts

@@ -936,6 +936,8 @@ export type CustomFieldConfig = {
   __typename?: 'CustomFieldConfig',
   name: Scalars['String'],
   type: Scalars['String'],
+  label?: Maybe<Array<LocalizedString>>,
+  description?: Maybe<Array<LocalizedString>>,
 };
 
 export type CustomFields = {
@@ -1553,6 +1555,12 @@ export enum LanguageCode {
   zu = 'zu'
 }
 
+export type LocalizedString = {
+  __typename?: 'LocalizedString',
+  languageCode: LanguageCode,
+  value: Scalars['String'],
+};
+
 export type LoginResult = {
   __typename?: 'LoginResult',
   user: CurrentUser,
@@ -1566,14 +1574,14 @@ export type MoveCollectionInput = {
 
 export type Mutation = {
   __typename?: 'Mutation',
+  /** Create a new Asset */
+  createAssets: Array<Asset>,
   /** Create a new Administrator */
   createAdministrator: Administrator,
   /** Update an existing Administrator */
   updateAdministrator: Administrator,
   /** Assign a Role to an Administrator */
   assignRoleToAdministrator: Administrator,
-  /** Create a new Asset */
-  createAssets: Array<Asset>,
   login: LoginResult,
   logout: Scalars['Boolean'],
   /** Create a new Channel */
@@ -1602,8 +1610,6 @@ export type Mutation = {
   addCustomersToGroup: CustomerGroup,
   /** Remove Customers from a CustomerGroup */
   removeCustomersFromGroup: CustomerGroup,
-  updateGlobalSettings: GlobalSettings,
-  importProducts?: Maybe<ImportInfo>,
   /** Create a new Customer. If a password is provided, a new User will also be created an linked to the Customer. */
   createCustomer: Customer,
   /** Update an existing Customer */
@@ -1628,6 +1634,8 @@ export type Mutation = {
   updateFacetValues: Array<FacetValue>,
   /** Delete one or more FacetValues */
   deleteFacetValues: Array<DeletionResponse>,
+  updateGlobalSettings: GlobalSettings,
+  importProducts?: Maybe<ImportInfo>,
   settlePayment: Payment,
   fulfillOrder: Fulfillment,
   cancelOrder: Order,
@@ -1676,10 +1684,6 @@ export type Mutation = {
   createTaxCategory: TaxCategory,
   /** Update an existing TaxCategory */
   updateTaxCategory: TaxCategory,
-  /** Create a new TaxRate */
-  createTaxRate: TaxRate,
-  /** Update an existing TaxRate */
-  updateTaxRate: TaxRate,
   /** Create a new Zone */
   createZone: Zone,
   /** Update an existing Zone */
@@ -1690,6 +1694,15 @@ export type Mutation = {
   addMembersToZone: Zone,
   /** Remove members from a Zone */
   removeMembersFromZone: Zone,
+  /** Create a new TaxRate */
+  createTaxRate: TaxRate,
+  /** Update an existing TaxRate */
+  updateTaxRate: TaxRate,
+};
+
+
+export type MutationCreateAssetsArgs = {
+  input: Array<CreateAssetInput>
 };
 
 
@@ -1709,11 +1722,6 @@ export type MutationAssignRoleToAdministratorArgs = {
 };
 
 
-export type MutationCreateAssetsArgs = {
-  input: Array<CreateAssetInput>
-};
-
-
 export type MutationLoginArgs = {
   username: Scalars['String'],
   password: Scalars['String'],
@@ -1788,16 +1796,6 @@ export type MutationRemoveCustomersFromGroupArgs = {
 };
 
 
-export type MutationUpdateGlobalSettingsArgs = {
-  input: UpdateGlobalSettingsInput
-};
-
-
-export type MutationImportProductsArgs = {
-  csvFile: Scalars['Upload']
-};
-
-
 export type MutationCreateCustomerArgs = {
   input: CreateCustomerInput,
   password?: Maybe<Scalars['String']>
@@ -1862,6 +1860,16 @@ export type MutationDeleteFacetValuesArgs = {
 };
 
 
+export type MutationUpdateGlobalSettingsArgs = {
+  input: UpdateGlobalSettingsInput
+};
+
+
+export type MutationImportProductsArgs = {
+  csvFile: Scalars['Upload']
+};
+
+
 export type MutationSettlePaymentArgs = {
   id: Scalars['ID']
 };
@@ -2004,16 +2012,6 @@ export type MutationUpdateTaxCategoryArgs = {
 };
 
 
-export type MutationCreateTaxRateArgs = {
-  input: CreateTaxRateInput
-};
-
-
-export type MutationUpdateTaxRateArgs = {
-  input: UpdateTaxRateInput
-};
-
-
 export type MutationCreateZoneArgs = {
   input: CreateZoneInput
 };
@@ -2040,6 +2038,16 @@ export type MutationRemoveMembersFromZoneArgs = {
   memberIds: Array<Scalars['ID']>
 };
 
+
+export type MutationCreateTaxRateArgs = {
+  input: CreateTaxRateInput
+};
+
+
+export type MutationUpdateTaxRateArgs = {
+  input: UpdateTaxRateInput
+};
+
 export type Node = {
   __typename?: 'Node',
   id: Scalars['ID'],
@@ -2536,10 +2544,10 @@ export type PromotionSortParameter = {
 
 export type Query = {
   __typename?: 'Query',
-  administrators: AdministratorList,
-  administrator?: Maybe<Administrator>,
   assets: AssetList,
   asset?: Maybe<Asset>,
+  administrators: AdministratorList,
+  administrator?: Maybe<Administrator>,
   me?: Maybe<CurrentUser>,
   channels: Array<Channel>,
   channel?: Maybe<Channel>,
@@ -2551,11 +2559,11 @@ export type Query = {
   country?: Maybe<Country>,
   customerGroups: Array<CustomerGroup>,
   customerGroup?: Maybe<CustomerGroup>,
-  globalSettings: GlobalSettings,
   customers: CustomerList,
   customer?: Maybe<Customer>,
   facets: FacetList,
   facet?: Maybe<Facet>,
+  globalSettings: GlobalSettings,
   job?: Maybe<JobInfo>,
   jobs: Array<JobInfo>,
   order?: Maybe<Order>,
@@ -2579,29 +2587,29 @@ export type Query = {
   shippingCalculators: Array<ConfigurableOperation>,
   taxCategories: Array<TaxCategory>,
   taxCategory?: Maybe<TaxCategory>,
-  taxRates: TaxRateList,
-  taxRate?: Maybe<TaxRate>,
   zones: Array<Zone>,
   zone?: Maybe<Zone>,
+  taxRates: TaxRateList,
+  taxRate?: Maybe<TaxRate>,
 };
 
 
-export type QueryAdministratorsArgs = {
-  options?: Maybe<AdministratorListOptions>
+export type QueryAssetsArgs = {
+  options?: Maybe<AssetListOptions>
 };
 
 
-export type QueryAdministratorArgs = {
+export type QueryAssetArgs = {
   id: Scalars['ID']
 };
 
 
-export type QueryAssetsArgs = {
-  options?: Maybe<AssetListOptions>
+export type QueryAdministratorsArgs = {
+  options?: Maybe<AdministratorListOptions>
 };
 
 
-export type QueryAssetArgs = {
+export type QueryAdministratorArgs = {
   id: Scalars['ID']
 };
 
@@ -2755,17 +2763,17 @@ export type QueryTaxCategoryArgs = {
 };
 
 
-export type QueryTaxRatesArgs = {
-  options?: Maybe<TaxRateListOptions>
+export type QueryZoneArgs = {
+  id: Scalars['ID']
 };
 
 
-export type QueryTaxRateArgs = {
-  id: Scalars['ID']
+export type QueryTaxRatesArgs = {
+  options?: Maybe<TaxRateListOptions>
 };
 
 
-export type QueryZoneArgs = {
+export type QueryTaxRateArgs = {
   id: Scalars['ID']
 };
 

+ 1 - 0
packages/common/src/shared-types.ts

@@ -17,6 +17,7 @@ export type DeepPartial<T> = {
  * A type representing the type rather than instance of a class.
  */
 export interface Type<T> extends Function {
+    // tslint:disable-next-line:callable-types
     new (...args: any[]): T;
 }
 

+ 80 - 24
packages/core/e2e/custom-fields.e2e-spec.ts

@@ -2,10 +2,9 @@ import gql from 'graphql-tag';
 import path from 'path';
 
 import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
-import { GetProductList } from './graphql/generated-e2e-admin-types';
-import { GET_PRODUCT_LIST } from './graphql/shared-definitions';
 import { TestAdminClient, TestShopClient } from './test-client';
 import { TestServer } from './test-server';
+import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 // tslint:disable:no-non-null-assertion
 
@@ -21,7 +20,14 @@ describe('Custom fields', () => {
         }, {
             customFields: {
                 Product: [
-                    { name: 'foo', type: 'string' },
+                    { name: 'nullable', type: 'string' },
+                    { name: 'notNullable', type: 'string', nullable: false, defaultValue: '' },
+                    { name: 'stringWithDefault', type: 'string', defaultValue: 'hello' },
+                    { name: 'localeStringWithDefault', type: 'localeString', defaultValue: 'hola' },
+                    { name: 'intWithDefault', type: 'int', defaultValue: 5 },
+                    { name: 'floatWithDefault', type: 'float', defaultValue: 5.5 },
+                    { name: 'booleanWithDefault', type: 'boolean', defaultValue: true },
+                    { name: 'dateTimeWithDefault', type: 'datetime', defaultValue: new Date('2019-04-30T12:59:16.4158386+02:00') },
                 ],
             },
         });
@@ -51,35 +57,85 @@ describe('Custom fields', () => {
 
         expect(globalSettings.serverConfig.customFieldConfig).toEqual({
             Product: [
-                { name: 'foo', type: 'string' },
+                { name: 'nullable', type: 'string' },
+                { name: 'notNullable', type: 'string' },
+                { name: 'stringWithDefault', type: 'string' },
+                { name: 'localeStringWithDefault', type: 'localeString' },
+                { name: 'intWithDefault', type: 'int' },
+                { name: 'floatWithDefault', type: 'float' },
+                { name: 'booleanWithDefault', type: 'boolean' },
+                { name: 'dateTimeWithDefault', type: 'datetime' },
             ],
         });
     });
 
-    it('works', async () => {
-        const { products } = await adminClient.query(gql`
-            query GetProductsCustomFields {
-                products {
-                    items {
-                        id
-                        name
-                        customFields {
-                            foo
-                        }
+    it('get nullable with no default', async () => {
+        const { product } = await adminClient.query(gql`
+            query {
+                product(id: "T_1") {
+                    id
+                    name
+                    customFields {
+                        nullable
                     }
                 }
-            }`, {
-            options: {},
+            }`,
+        );
+
+        expect(product).toEqual({
+            id: 'T_1',
+            name: 'Laptop',
+            customFields: {
+                nullable: null,
+            },
         });
+    });
 
-        expect(products.items).toEqual([
-            {
-                id: 'T_1',
-                name: 'Laptop',
-                customFields: {
-                    foo: null,
-                },
+    it('get fields with default values', async () => {
+        const { product } = await adminClient.query(gql`
+            query {
+                product(id: "T_1") {
+                    id
+                    name
+                    customFields {
+                        stringWithDefault
+                        localeStringWithDefault
+                        intWithDefault
+                        floatWithDefault
+                        booleanWithDefault
+                        dateTimeWithDefault
+                    }
+                }
+            }`,
+        );
+
+        expect(product).toEqual({
+            id: 'T_1',
+            name: 'Laptop',
+            customFields: {
+                stringWithDefault: 'hello',
+                localeStringWithDefault: 'hola',
+                intWithDefault: 5,
+                floatWithDefault: 5.5,
+                booleanWithDefault: true,
+                dateTimeWithDefault: '2019-04-30T12:59:16.415Z',
             },
-        ]);
+        });
     });
+
+    it('update non-nullable field', assertThrowsWithMessage(async () => {
+            await adminClient.query(gql`
+                mutation {
+                    updateProduct(input: {
+                        id: "T_1"
+                        customFields: {
+                            notNullable: null
+                        }
+                    }) { id }
+                }
+            `);
+        },
+        'NOT NULL constraint failed: product.customFieldsNotnullable',
+        ),
+    );
 });

+ 51 - 43
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -935,6 +935,8 @@ export type CustomFieldConfig = {
     __typename?: 'CustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    label?: Maybe<Array<LocalizedString>>;
+    description?: Maybe<Array<LocalizedString>>;
 };
 
 export type CustomFields = {
@@ -1550,6 +1552,12 @@ export enum LanguageCode {
     zu = 'zu',
 }
 
+export type LocalizedString = {
+    __typename?: 'LocalizedString';
+    languageCode: LanguageCode;
+    value: Scalars['String'];
+};
+
 export type LoginResult = {
     __typename?: 'LoginResult';
     user: CurrentUser;
@@ -1563,14 +1571,14 @@ export type MoveCollectionInput = {
 
 export type Mutation = {
     __typename?: 'Mutation';
+    /** Create a new Asset */
+    createAssets: Array<Asset>;
     /** Create a new Administrator */
     createAdministrator: Administrator;
     /** Update an existing Administrator */
     updateAdministrator: Administrator;
     /** Assign a Role to an Administrator */
     assignRoleToAdministrator: Administrator;
-    /** Create a new Asset */
-    createAssets: Array<Asset>;
     login: LoginResult;
     logout: Scalars['Boolean'];
     /** Create a new Channel */
@@ -1599,8 +1607,6 @@ export type Mutation = {
     addCustomersToGroup: CustomerGroup;
     /** Remove Customers from a CustomerGroup */
     removeCustomersFromGroup: CustomerGroup;
-    updateGlobalSettings: GlobalSettings;
-    importProducts?: Maybe<ImportInfo>;
     /** Create a new Customer. If a password is provided, a new User will also be created an linked to the Customer. */
     createCustomer: Customer;
     /** Update an existing Customer */
@@ -1625,6 +1631,8 @@ export type Mutation = {
     updateFacetValues: Array<FacetValue>;
     /** Delete one or more FacetValues */
     deleteFacetValues: Array<DeletionResponse>;
+    updateGlobalSettings: GlobalSettings;
+    importProducts?: Maybe<ImportInfo>;
     settlePayment: Payment;
     fulfillOrder: Fulfillment;
     cancelOrder: Order;
@@ -1673,10 +1681,6 @@ export type Mutation = {
     createTaxCategory: TaxCategory;
     /** Update an existing TaxCategory */
     updateTaxCategory: TaxCategory;
-    /** Create a new TaxRate */
-    createTaxRate: TaxRate;
-    /** Update an existing TaxRate */
-    updateTaxRate: TaxRate;
     /** Create a new Zone */
     createZone: Zone;
     /** Update an existing Zone */
@@ -1687,6 +1691,14 @@ export type Mutation = {
     addMembersToZone: Zone;
     /** Remove members from a Zone */
     removeMembersFromZone: Zone;
+    /** Create a new TaxRate */
+    createTaxRate: TaxRate;
+    /** Update an existing TaxRate */
+    updateTaxRate: TaxRate;
+};
+
+export type MutationCreateAssetsArgs = {
+    input: Array<CreateAssetInput>;
 };
 
 export type MutationCreateAdministratorArgs = {
@@ -1702,10 +1714,6 @@ export type MutationAssignRoleToAdministratorArgs = {
     roleId: Scalars['ID'];
 };
 
-export type MutationCreateAssetsArgs = {
-    input: Array<CreateAssetInput>;
-};
-
 export type MutationLoginArgs = {
     username: Scalars['String'];
     password: Scalars['String'];
@@ -1766,14 +1774,6 @@ export type MutationRemoveCustomersFromGroupArgs = {
     customerIds: Array<Scalars['ID']>;
 };
 
-export type MutationUpdateGlobalSettingsArgs = {
-    input: UpdateGlobalSettingsInput;
-};
-
-export type MutationImportProductsArgs = {
-    csvFile: Scalars['Upload'];
-};
-
 export type MutationCreateCustomerArgs = {
     input: CreateCustomerInput;
     password?: Maybe<Scalars['String']>;
@@ -1826,6 +1826,14 @@ export type MutationDeleteFacetValuesArgs = {
     force?: Maybe<Scalars['Boolean']>;
 };
 
+export type MutationUpdateGlobalSettingsArgs = {
+    input: UpdateGlobalSettingsInput;
+};
+
+export type MutationImportProductsArgs = {
+    csvFile: Scalars['Upload'];
+};
+
 export type MutationSettlePaymentArgs = {
     id: Scalars['ID'];
 };
@@ -1940,14 +1948,6 @@ export type MutationUpdateTaxCategoryArgs = {
     input: UpdateTaxCategoryInput;
 };
 
-export type MutationCreateTaxRateArgs = {
-    input: CreateTaxRateInput;
-};
-
-export type MutationUpdateTaxRateArgs = {
-    input: UpdateTaxRateInput;
-};
-
 export type MutationCreateZoneArgs = {
     input: CreateZoneInput;
 };
@@ -1970,6 +1970,14 @@ export type MutationRemoveMembersFromZoneArgs = {
     memberIds: Array<Scalars['ID']>;
 };
 
+export type MutationCreateTaxRateArgs = {
+    input: CreateTaxRateInput;
+};
+
+export type MutationUpdateTaxRateArgs = {
+    input: UpdateTaxRateInput;
+};
+
 export type Node = {
     __typename?: 'Node';
     id: Scalars['ID'];
@@ -2464,10 +2472,10 @@ export type PromotionSortParameter = {
 
 export type Query = {
     __typename?: 'Query';
-    administrators: AdministratorList;
-    administrator?: Maybe<Administrator>;
     assets: AssetList;
     asset?: Maybe<Asset>;
+    administrators: AdministratorList;
+    administrator?: Maybe<Administrator>;
     me?: Maybe<CurrentUser>;
     channels: Array<Channel>;
     channel?: Maybe<Channel>;
@@ -2479,11 +2487,11 @@ export type Query = {
     country?: Maybe<Country>;
     customerGroups: Array<CustomerGroup>;
     customerGroup?: Maybe<CustomerGroup>;
-    globalSettings: GlobalSettings;
     customers: CustomerList;
     customer?: Maybe<Customer>;
     facets: FacetList;
     facet?: Maybe<Facet>;
+    globalSettings: GlobalSettings;
     job?: Maybe<JobInfo>;
     jobs: Array<JobInfo>;
     order?: Maybe<Order>;
@@ -2507,25 +2515,25 @@ export type Query = {
     shippingCalculators: Array<ConfigurableOperation>;
     taxCategories: Array<TaxCategory>;
     taxCategory?: Maybe<TaxCategory>;
-    taxRates: TaxRateList;
-    taxRate?: Maybe<TaxRate>;
     zones: Array<Zone>;
     zone?: Maybe<Zone>;
+    taxRates: TaxRateList;
+    taxRate?: Maybe<TaxRate>;
 };
 
-export type QueryAdministratorsArgs = {
-    options?: Maybe<AdministratorListOptions>;
+export type QueryAssetsArgs = {
+    options?: Maybe<AssetListOptions>;
 };
 
-export type QueryAdministratorArgs = {
+export type QueryAssetArgs = {
     id: Scalars['ID'];
 };
 
-export type QueryAssetsArgs = {
-    options?: Maybe<AssetListOptions>;
+export type QueryAdministratorsArgs = {
+    options?: Maybe<AdministratorListOptions>;
 };
 
-export type QueryAssetArgs = {
+export type QueryAdministratorArgs = {
     id: Scalars['ID'];
 };
 
@@ -2650,6 +2658,10 @@ export type QueryTaxCategoryArgs = {
     id: Scalars['ID'];
 };
 
+export type QueryZoneArgs = {
+    id: Scalars['ID'];
+};
+
 export type QueryTaxRatesArgs = {
     options?: Maybe<TaxRateListOptions>;
 };
@@ -2658,10 +2670,6 @@ export type QueryTaxRateArgs = {
     id: Scalars['ID'];
 };
 
-export type QueryZoneArgs = {
-    id: Scalars['ID'];
-};
-
 export type Refund = Node & {
     __typename?: 'Refund';
     id: Scalars['ID'];

+ 8 - 0
packages/core/e2e/graphql/generated-e2e-shop-types.ts

@@ -659,6 +659,8 @@ export type CustomFieldConfig = {
     __typename?: 'CustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    label?: Maybe<Array<LocalizedString>>;
+    description?: Maybe<Array<LocalizedString>>;
 };
 
 export type CustomFields = {
@@ -1205,6 +1207,12 @@ export enum LanguageCode {
     zu = 'zu',
 }
 
+export type LocalizedString = {
+    __typename?: 'LocalizedString';
+    languageCode: LanguageCode;
+    value: Scalars['String'];
+};
+
 export type LoginResult = {
     __typename?: 'LoginResult';
     user: CurrentUser;

+ 18 - 6
packages/core/src/api/config/graphql-custom-fields.ts

@@ -147,12 +147,17 @@ export function addGraphQLCustomFields(
     return extendSchema(schema, parse(customFieldTypeDefs));
 }
 
-export function addServerConfigCustomFields(typeDefsOrSchema: string | GraphQLSchema,
-                                            customFieldConfig: CustomFields) {
+export function addServerConfigCustomFields(
+    typeDefsOrSchema: string | GraphQLSchema,
+    customFieldConfig: CustomFields,
+) {
     const schema = typeof typeDefsOrSchema === 'string' ? buildSchema(typeDefsOrSchema) : typeDefsOrSchema;
     const customFieldTypeDefs = `
             type CustomFields {
-                ${Object.keys(customFieldConfig).reduce((output, name) => output + name + `: [CustomFieldConfig!]!\n`, '')}
+                ${Object.keys(customFieldConfig).reduce(
+                    (output, name) => output + name + `: [CustomFieldConfig!]!\n`,
+                    '',
+                )}
             }
 
             extend type ServerConfig {
@@ -167,7 +172,10 @@ export function addServerConfigCustomFields(typeDefsOrSchema: string | GraphQLSc
  * If CustomFields are defined on the OrderLine entity, then an extra `customFields` argument
  * must be added to the `addItemToOrder` and `adjustOrderLine` mutations.
  */
-export function addOrderLineCustomFieldsInput(typeDefsOrSchema: string | GraphQLSchema, orderLineCustomFields: CustomFieldConfig[]): GraphQLSchema {
+export function addOrderLineCustomFieldsInput(
+    typeDefsOrSchema: string | GraphQLSchema,
+    orderLineCustomFields: CustomFieldConfig[],
+): GraphQLSchema {
     const schema = typeof typeDefsOrSchema === 'string' ? buildSchema(typeDefsOrSchema) : typeDefsOrSchema;
     if (!orderLineCustomFields || orderLineCustomFields.length === 0) {
         return schema;
@@ -207,8 +215,12 @@ type GraphQLFieldType = 'DateTime' | 'String' | 'Int' | 'Float' | 'Boolean' | 'I
 /**
  * Maps an array of CustomFieldConfig objects into a string of SDL fields.
  */
-function mapToFields(fieldDefs: CustomFieldConfig[], typeFn: (fieldType: CustomFieldType) => string): string {
-    return fieldDefs.map(field => `${field.name}: ${typeFn(field.type)}`).join('\n');
+function mapToFields(
+    fieldDefs: CustomFieldConfig[],
+    typeFn: (fieldType: CustomFieldType) => string,
+    prefix: string = '',
+): string {
+    return fieldDefs.map(field => `${prefix}${field.name}: ${typeFn(field.type)}`).join('\n');
 }
 
 function getFilterOperator(type: CustomFieldType): string {

+ 12 - 0
packages/core/src/api/schema/common/common-types.graphql

@@ -20,6 +20,18 @@ type Adjustment {
     amount: Int!
 }
 
+type CustomFieldConfig {
+    name: String!
+    type: String!
+    label: [LocalizedString!]
+    description: [LocalizedString!]
+}
+
+type LocalizedString {
+    languageCode: LanguageCode!
+    value: String!
+}
+
 """
 Certain entities allow arbitrary configuration arguments to be specified which can then
 be set in the admin-ui and used in the business logic of the app. These are the valid

+ 0 - 5
packages/core/src/api/schema/type/global-settings.type.graphql

@@ -9,8 +9,3 @@ type GlobalSettings {
 
 # Programatically extended by the addGraphQLCustomFields function
 type ServerConfig
-
-type CustomFieldConfig {
-    name: String!
-    type: String!
-}

+ 0 - 5
packages/core/src/app.module.ts

@@ -6,7 +6,6 @@ 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/validate-custom-fields-config';
 import { I18nModule } from './i18n/i18n.module';
 import { I18nService } from './i18n/i18n.service';
 
@@ -19,10 +18,6 @@ export class AppModule implements NestModule, OnModuleDestroy, OnApplicationShut
 
     configure(consumer: MiddlewareConsumer) {
         const { adminApiPath, shopApiPath } = this.configService;
-
-        // tslint:disable-next-line:no-floating-promises
-        validateCustomFieldsConfig(this.configService.customFields);
-
         const i18nextHandler = this.i18nService.handle();
         const defaultMiddleware: Array<{ handler: RequestHandler; route?: string }> = [
             { handler: i18nextHandler, route: adminApiPath },

+ 5 - 0
packages/core/src/bootstrap.ts

@@ -11,6 +11,7 @@ import { DefaultLogger } from './config/logger/default-logger';
 import { Logger } from './config/logger/vendure-logger';
 import { VendureConfig } from './config/vendure-config';
 import { registerCustomEntityFields } from './entity/register-custom-entity-fields';
+import { validateCustomFieldsConfig } from './entity/validate-custom-fields-config';
 import { logProxyMiddlewares } from './plugin/plugin-utils';
 
 export type VendureBootstrapFunction = (config: VendureConfig) => Promise<INestApplication>;
@@ -136,6 +137,10 @@ export async function preBootstrapConfig(
     });
 
     let config = getConfig();
+    const customFieldValidationResult = validateCustomFieldsConfig(config.customFields, entities);
+    if (!customFieldValidationResult.valid) {
+        throw new Error(`CustomFields config error:\n- ` + customFieldValidationResult.errors.join('\n- '));
+    }
     config = await runPluginConfigurations(config);
     registerCustomEntityFields(config);
     return config;

+ 23 - 3
packages/core/src/config/custom-field/custom-field-types.ts

@@ -1,15 +1,35 @@
 import { CustomFieldConfig as GraphQLCustomFieldConfig } from '@vendure/common/lib/generated-types';
 import { CustomFieldsObject, CustomFieldType } from '@vendure/common/src/shared-types';
 
+// prettier-ignore
+export type DefaultValueType<T extends CustomFieldType> =
+    T extends 'string' | 'localeString' ? string :
+        T extends 'int' | 'float' ? number :
+            T extends 'boolean' ? boolean :
+                T extends 'datetime' ? Date : never;
+
 /**
  * @description
  * Configures a custom field on an entity in the {@link CustomFields} config object.
  *
  * @docsCategory custom-fields
  */
-export interface CustomFieldConfig extends Omit<GraphQLCustomFieldConfig, '__typename'> {
-    type: CustomFieldType;
-}
+export type TypedCustomFieldConfig<T extends CustomFieldType = CustomFieldType> = Omit<
+    GraphQLCustomFieldConfig,
+    '__typename'
+> & {
+    type: T;
+    defaultValue?: DefaultValueType<T>;
+    nullable?: boolean;
+};
+
+export type CustomFieldConfig =
+    | TypedCustomFieldConfig<'string'>
+    | TypedCustomFieldConfig<'localeString'>
+    | TypedCustomFieldConfig<'int'>
+    | TypedCustomFieldConfig<'float'>
+    | TypedCustomFieldConfig<'boolean'>
+    | TypedCustomFieldConfig<'datetime'>;
 
 /**
  * @description

+ 39 - 5
packages/core/src/entity/register-custom-entity-fields.ts

@@ -1,6 +1,7 @@
 import { CustomFieldType } from '@vendure/common/lib/shared-types';
 import { assertNever } from '@vendure/common/lib/shared-utils';
 import { Column, ColumnType, ConnectionOptions } from 'typeorm';
+import { DateUtils } from 'typeorm/util/DateUtils';
 
 import { CustomFields } from '../config/custom-field/custom-field-types';
 import { VendureConfig } from '../config/vendure-config';
@@ -41,9 +42,14 @@ function registerCustomFieldsForEntity(
     const dbEngine = config.dbConnectionOptions.type;
     if (customFields) {
         for (const customField of customFields) {
-            const { name, type } = customField;
+            const { name, type, defaultValue, nullable } = customField;
             const registerColumn = () =>
-                Column({ type: getColumnType(dbEngine, type), name, nullable: true })(new ctor(), name);
+                Column({
+                    type: getColumnType(dbEngine, type),
+                    default: type === 'datetime' ? formatDefaultDatetime(dbEngine, defaultValue) : defaultValue,
+                    name,
+                    nullable: nullable === false ? false : true,
+                })(new ctor(), name);
 
             if (translation) {
                 if (type === 'localeString') {
@@ -58,19 +64,48 @@ function registerCustomFieldsForEntity(
     }
 }
 
+function formatDefaultDatetime(dbEngine: ConnectionOptions['type'], datetime: any): Date | string {
+    switch (dbEngine) {
+        case 'sqlite':
+        case 'sqljs':
+            return DateUtils.mixedDateToDatetimeString(datetime);
+        case 'mysql':
+        case 'postgres':
+        default:
+            return DateUtils.mixedDateToDate(datetime);
+    }
+}
+
 function getColumnType(dbEngine: ConnectionOptions['type'], type: CustomFieldType): ColumnType {
     switch (type) {
         case 'string':
         case 'localeString':
             return 'varchar';
         case 'boolean':
-            return dbEngine === 'mysql' ? 'tinyint' : 'bool';
+            switch (dbEngine) {
+                case 'mysql':
+                    return 'tinyint';
+                case 'postgres':
+                    return 'bool';
+                case 'sqlite':
+                case 'sqljs':
+                default:
+                    return 'boolean';
+            }
         case 'int':
             return 'int';
         case 'float':
             return 'double';
         case 'datetime':
-            return dbEngine === 'mysql' ? 'datetime' : 'timestamp';
+            switch (dbEngine) {
+                case 'postgres':
+                    return 'timestamp';
+                case 'mysql':
+                case 'sqlite':
+                case 'sqljs':
+                default:
+                    return 'datetime';
+            }
         default:
             assertNever(type);
     }
@@ -107,4 +142,3 @@ export function registerCustomEntityFields(config: VendureConfig) {
     registerCustomFieldsForEntity(config, 'User', CustomUserFields);
     registerCustomFieldsForEntity(config, 'GlobalSettings', CustomGlobalSettingsFields);
 }
-

+ 130 - 0
packages/core/src/entity/validate-custom-fields-config.spec.ts

@@ -0,0 +1,130 @@
+import { Type } from '@vendure/common/lib/shared-types';
+
+import { User } from '../../dist/entity/user/user.entity';
+import { CustomFields } from '../config/custom-field/custom-field-types';
+
+import { coreEntitiesMap } from './entities';
+import { ProductTranslation } from './product/product-translation.entity';
+import { Product } from './product/product.entity';
+import { validateCustomFieldsConfig } from './validate-custom-fields-config';
+
+describe('validateCustomFieldsConfig()', () => {
+
+    const allEntities = Object.values(coreEntitiesMap) as Array<Type<any>>;
+
+    it('valid config', () => {
+        const config: CustomFields = {
+            Product: [
+                { name: 'foo', type: 'string' },
+                { name: 'bar', type: 'localeString' },
+            ],
+        };
+        const result = validateCustomFieldsConfig(config, allEntities);
+
+        expect(result.valid).toBe(true);
+        expect(result.errors.length).toBe(0);
+    });
+
+    it('invalid localeString', () => {
+        const config: CustomFields = {
+            User: [
+                { name: 'foo', type: 'string' },
+                { name: 'bar', type: 'localeString' },
+            ],
+        };
+        const result = validateCustomFieldsConfig(config, allEntities);
+
+        expect(result.valid).toBe(false);
+        expect(result.errors).toEqual([
+            'User entity does not support custom fields of type "localeString"',
+        ]);
+    });
+
+    it('valid names', () => {
+        const config: CustomFields = {
+            User: [
+                { name: 'love2code', type: 'string' },
+                { name: 'snake_case', type: 'string' },
+                { name: 'camelCase', type: 'string' },
+                { name: 'SHOUTY', type: 'string' },
+            ],
+        };
+        const result = validateCustomFieldsConfig(config, allEntities);
+
+        expect(result.valid).toBe(true);
+        expect(result.errors).toEqual([]);
+    });
+
+    it('invalid names', () => {
+        const config: CustomFields = {
+            User: [
+                { name: '2cool', type: 'string' },
+                { name: 'has space', type: 'string' },
+                { name: 'speci@alChar', type: 'string' },
+                { name: 'has-dash', type: 'string' },
+            ],
+        };
+        const result = validateCustomFieldsConfig(config, allEntities);
+
+        expect(result.valid).toBe(false);
+        expect(result.errors).toEqual([
+            'User entity has an invalid custom field name: "2cool"',
+            'User entity has an invalid custom field name: "has space"',
+            'User entity has an invalid custom field name: "speci@alChar"',
+            'User entity has an invalid custom field name: "has-dash"',
+        ]);
+    });
+
+    it('duplicate names', () => {
+        const config: CustomFields = {
+            User: [
+                { name: 'foo', type: 'string' },
+                { name: 'bar', type: 'string' },
+                { name: 'baz', type: 'string' },
+                { name: 'foo', type: 'boolean' },
+                { name: 'bar', type: 'boolean' },
+            ],
+        };
+        const result = validateCustomFieldsConfig(config, allEntities);
+
+        expect(result.valid).toBe(false);
+        expect(result.errors).toEqual([
+            'User entity has duplicated custom field name: "foo"',
+            'User entity has duplicated custom field name: "bar"',
+        ]);
+    });
+
+    it('duplicate names in translation', () => {
+        const config: CustomFields = {
+            Product: [
+                { name: 'foo', type: 'string' },
+                { name: 'bar', type: 'string' },
+                { name: 'baz', type: 'string' },
+                { name: 'foo', type: 'localeString' },
+                { name: 'bar', type: 'boolean' },
+            ],
+        };
+        const result = validateCustomFieldsConfig(config, allEntities);
+
+        expect(result.valid).toBe(false);
+        expect(result.errors).toEqual([
+            'Product entity has duplicated custom field name: "foo"',
+            'Product entity has duplicated custom field name: "bar"',
+        ]);
+    });
+
+    it('non-nullable must have defaultValue', () => {
+        const config: CustomFields = {
+            Product: [
+                { name: 'foo', type: 'string', nullable: false },
+            ],
+        };
+        const result = validateCustomFieldsConfig(config, allEntities);
+
+        expect(result.valid).toBe(false);
+        expect(result.errors).toEqual([
+            'Product entity custom field "foo" is non-nullable and must have a defaultValue',
+        ]);
+    });
+
+});

+ 70 - 40
packages/core/src/entity/validate-custom-fields-config.ts

@@ -1,4 +1,4 @@
-import { Connection, getConnection } from 'typeorm';
+import { Connection, getConnection, getMetadataArgsStorage } from 'typeorm';
 
 import { Type } from '../../../common/lib/shared-types';
 import { CustomFieldConfig, CustomFields } from '../config/custom-field/custom-field-types';
@@ -6,69 +6,99 @@ import { CustomFieldConfig, CustomFields } from '../config/custom-field/custom-f
 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);
+): string[] {
+    const metadata = getMetadataArgsStorage();
+    const isTranslatable =
+        -1 < metadata.relations.findIndex(r => r.target === entity && r.propertyName === 'translations');
+    return [
+        ...assertValidFieldNames(entity.name, customFields),
+        ...assertNoNameConflicts(entity.name, customFields),
+        ...assetNonNullablesHaveDefaults(entity.name, customFields),
+        ...(isTranslatable ? [] : assertNoLocaleStringFields(entity.name, customFields)),
+    ];
 }
 
 /**
- * Assert that none of the custom field names conflict with existing properties of the entity, as provided
- * by the TypeORM PropertiesMap object.
+ * Assert that the custom entity names are valid
  */
-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);
+function assertValidFieldNames(entityName: string, customFields: CustomFieldConfig[]): string[] {
+    const errors: string[] = [];
+    const validNameRe = /^[a-zA-Z_]+[a-zA-Z0-9_]*$/;
+    for (const field of customFields) {
+        if (!validNameRe.test(field.name)) {
+            errors.push(`${entityName} entity has an invalid custom field name: "${field.name}"`);
         }
     }
+    return errors;
+}
+
+/**
+ * Assert that none of the custom field names conflict with one another.
+ */
+function assertNoNameConflicts(entityName: string, customFields: CustomFieldConfig[]): string[] {
+    const nameCounts = customFields
+        .map(f => f.name)
+        .reduce(
+            (hash, name) => {
+                hash[name] ? hash[name]++ : (hash[name] = 1);
+                return hash;
+            },
+            {} as { [name: string]: number },
+        );
+    return Object.entries(nameCounts)
+        .filter(([name, count]) => 1 < count)
+        .map(([name, count]) => `${entityName} entity has duplicated custom field name: "${name}"`);
 }
 
 /**
  * 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 {
+function assertNoLocaleStringFields(entityName: string, customFields: CustomFieldConfig[]): string[] {
     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);
+        return [`${entityName} entity does not support custom fields of type "localeString"`];
     }
+    return [];
+}
+
+/**
+ * Assert that any non-nullable field must have a defaultValue specified.
+ */
+function assetNonNullablesHaveDefaults(entityName: string, customFields: CustomFieldConfig[]): string[] {
+    const errors: string[] = [];
+    for (const field of customFields) {
+        if (field.nullable === false && field.defaultValue === undefined) {
+            errors.push(
+                `${entityName} entity custom field "${
+                    field.name
+                }" is non-nullable and must have a defaultValue`,
+            );
+        }
+    }
+    return errors;
 }
 
 /**
  * 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');
-
+export function validateCustomFieldsConfig(
+    customFieldConfig: CustomFields,
+    entities: Array<Type<any>>,
+): { valid: boolean; errors: string[] } {
+    let errors: string[] = [];
     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);
+        const entity = entities.find(e => e.name === entityName);
+        if (entity) {
+            errors = errors.concat(validateCustomFieldsForEntity(entity, customEntityFields));
+        }
     }
+    return {
+        valid: errors.length === 0,
+        errors,
+    };
 }

+ 3 - 3
packages/dev-server/dev-config.ts

@@ -29,10 +29,10 @@ export const devConfig: VendureConfig = {
         paymentMethodHandlers: [examplePaymentHandler],
     },
     customFields: {
-        Product: [
-            { type: 'string', name: 'foo' },
+        /*Product: [
+            { type: 'string', name: 'name' },
             { type: 'datetime', name: 'expires' },
-        ],
+        ],*/
     },
     logger: new DefaultLogger({ level: LogLevel.Verbose }),
     importExportOptions: {

File diff suppressed because it is too large
+ 0 - 0
schema-admin.json


File diff suppressed because it is too large
+ 0 - 0
schema-shop.json


Some files were not shown because too many files changed in this diff