Browse Source

feat(core): Support list types for custom fields

Relates to #416
Michael Bromley 5 years ago
parent
commit
1fa3cf14dc

+ 8 - 1
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts

@@ -186,6 +186,7 @@ export type BooleanCustomFieldConfig = CustomField & {
     __typename?: 'BooleanCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -321,7 +322,7 @@ export type ConfigArgDefinition = {
     list: Scalars['Boolean'];
     label?: Maybe<Scalars['String']>;
     description?: Maybe<Scalars['String']>;
-    config?: Maybe<Scalars['JSON']>;
+    ui?: Maybe<Scalars['JSON']>;
 };
 
 export type ConfigArgInput = {
@@ -1042,6 +1043,7 @@ export type CustomerSortParameter = {
 export type CustomField = {
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -1094,6 +1096,7 @@ export type DateTimeCustomFieldConfig = CustomField & {
     __typename?: 'DateTimeCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -1219,6 +1222,7 @@ export type FloatCustomFieldConfig = CustomField & {
     __typename?: 'FloatCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -1328,6 +1332,7 @@ export type IntCustomFieldConfig = CustomField & {
     __typename?: 'IntCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -1737,6 +1742,7 @@ export type LocaleStringCustomFieldConfig = CustomField & {
     __typename?: 'LocaleStringCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -3335,6 +3341,7 @@ export type StringCustomFieldConfig = CustomField & {
     __typename?: 'StringCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     length?: Maybe<Scalars['Int']>;
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;

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

@@ -111,6 +111,7 @@ export type BooleanCustomFieldConfig = CustomField & {
     __typename?: 'BooleanCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -236,7 +237,7 @@ export type ConfigArgDefinition = {
     list: Scalars['Boolean'];
     label?: Maybe<Scalars['String']>;
     description?: Maybe<Scalars['String']>;
-    config?: Maybe<Scalars['JSON']>;
+    ui?: Maybe<Scalars['JSON']>;
 };
 
 export type ConfigArgInput = {
@@ -727,6 +728,7 @@ export type CustomerSortParameter = {
 export type CustomField = {
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -779,6 +781,7 @@ export type DateTimeCustomFieldConfig = CustomField & {
     __typename?: 'DateTimeCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -865,6 +868,7 @@ export type FloatCustomFieldConfig = CustomField & {
     __typename?: 'FloatCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -968,6 +972,7 @@ export type IntCustomFieldConfig = CustomField & {
     __typename?: 'IntCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -1307,6 +1312,7 @@ export type LocaleStringCustomFieldConfig = CustomField & {
     __typename?: 'LocaleStringCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -2279,6 +2285,7 @@ export type StringCustomFieldConfig = CustomField & {
     __typename?: 'StringCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     length?: Maybe<Scalars['Int']>;
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;

+ 8 - 1
packages/common/src/generated-types.ts

@@ -186,6 +186,7 @@ export type BooleanCustomFieldConfig = CustomField & {
    __typename?: 'BooleanCustomFieldConfig';
   name: Scalars['String'];
   type: Scalars['String'];
+  list: Scalars['Boolean'];
   label?: Maybe<Array<LocalizedString>>;
   description?: Maybe<Array<LocalizedString>>;
   readonly?: Maybe<Scalars['Boolean']>;
@@ -321,7 +322,7 @@ export type ConfigArgDefinition = {
   list: Scalars['Boolean'];
   label?: Maybe<Scalars['String']>;
   description?: Maybe<Scalars['String']>;
-  config?: Maybe<Scalars['JSON']>;
+  ui?: Maybe<Scalars['JSON']>;
 };
 
 export type ConfigArgInput = {
@@ -1045,6 +1046,7 @@ export type CustomerSortParameter = {
 export type CustomField = {
   name: Scalars['String'];
   type: Scalars['String'];
+  list: Scalars['Boolean'];
   label?: Maybe<Array<LocalizedString>>;
   description?: Maybe<Array<LocalizedString>>;
   readonly?: Maybe<Scalars['Boolean']>;
@@ -1092,6 +1094,7 @@ export type DateTimeCustomFieldConfig = CustomField & {
    __typename?: 'DateTimeCustomFieldConfig';
   name: Scalars['String'];
   type: Scalars['String'];
+  list: Scalars['Boolean'];
   label?: Maybe<Array<LocalizedString>>;
   description?: Maybe<Array<LocalizedString>>;
   readonly?: Maybe<Scalars['Boolean']>;
@@ -1217,6 +1220,7 @@ export type FloatCustomFieldConfig = CustomField & {
    __typename?: 'FloatCustomFieldConfig';
   name: Scalars['String'];
   type: Scalars['String'];
+  list: Scalars['Boolean'];
   label?: Maybe<Array<LocalizedString>>;
   description?: Maybe<Array<LocalizedString>>;
   readonly?: Maybe<Scalars['Boolean']>;
@@ -1326,6 +1330,7 @@ export type IntCustomFieldConfig = CustomField & {
    __typename?: 'IntCustomFieldConfig';
   name: Scalars['String'];
   type: Scalars['String'];
+  list: Scalars['Boolean'];
   label?: Maybe<Array<LocalizedString>>;
   description?: Maybe<Array<LocalizedString>>;
   readonly?: Maybe<Scalars['Boolean']>;
@@ -1736,6 +1741,7 @@ export type LocaleStringCustomFieldConfig = CustomField & {
    __typename?: 'LocaleStringCustomFieldConfig';
   name: Scalars['String'];
   type: Scalars['String'];
+  list: Scalars['Boolean'];
   label?: Maybe<Array<LocalizedString>>;
   description?: Maybe<Array<LocalizedString>>;
   readonly?: Maybe<Scalars['Boolean']>;
@@ -3460,6 +3466,7 @@ export type StringCustomFieldConfig = CustomField & {
    __typename?: 'StringCustomFieldConfig';
   name: Scalars['String'];
   type: Scalars['String'];
+  list: Scalars['Boolean'];
   length?: Maybe<Scalars['Int']>;
   label?: Maybe<Array<LocalizedString>>;
   description?: Maybe<Array<LocalizedString>>;

+ 1 - 1
packages/common/src/shared-utils.ts

@@ -10,7 +10,7 @@ export function notNullOrUndefined<T>(val: T | undefined | null): val is T {
  * Used in exhaustiveness checks to assert a codepath should never be reached.
  */
 export function assertNever(value: never): never {
-    throw new Error(`Expected never, got ${typeof value}`);
+    throw new Error(`Expected never, got ${typeof value} (${JSON.stringify(value)})`);
 }
 
 /**

+ 11 - 4
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -186,6 +186,7 @@ export type BooleanCustomFieldConfig = CustomField & {
     __typename?: 'BooleanCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -321,7 +322,7 @@ export type ConfigArgDefinition = {
     list: Scalars['Boolean'];
     label?: Maybe<Scalars['String']>;
     description?: Maybe<Scalars['String']>;
-    config?: Maybe<Scalars['JSON']>;
+    ui?: Maybe<Scalars['JSON']>;
 };
 
 export type ConfigArgInput = {
@@ -1042,6 +1043,7 @@ export type CustomerSortParameter = {
 export type CustomField = {
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -1094,6 +1096,7 @@ export type DateTimeCustomFieldConfig = CustomField & {
     __typename?: 'DateTimeCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -1219,6 +1222,7 @@ export type FloatCustomFieldConfig = CustomField & {
     __typename?: 'FloatCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -1328,6 +1332,7 @@ export type IntCustomFieldConfig = CustomField & {
     __typename?: 'IntCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -1737,6 +1742,7 @@ export type LocaleStringCustomFieldConfig = CustomField & {
     __typename?: 'LocaleStringCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -3335,6 +3341,7 @@ export type StringCustomFieldConfig = CustomField & {
     __typename?: 'StringCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     length?: Maybe<Scalars['Int']>;
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
@@ -5401,7 +5408,7 @@ export type ConfigurableOperationDefFragment = { __typename?: 'ConfigurableOpera
     'code' | 'description'
 > & {
         args: Array<
-            { __typename?: 'ConfigArgDefinition' } & Pick<ConfigArgDefinition, 'name' | 'type' | 'config'>
+            { __typename?: 'ConfigArgDefinition' } & Pick<ConfigArgDefinition, 'name' | 'type' | 'ui'>
         >;
     };
 
@@ -5513,7 +5520,7 @@ export type GetEligibilityCheckersQuery = { __typename?: 'Query' } & {
                 args: Array<
                     { __typename?: 'ConfigArgDefinition' } & Pick<
                         ConfigArgDefinition,
-                        'name' | 'type' | 'description' | 'label' | 'config'
+                        'name' | 'type' | 'description' | 'label' | 'ui'
                     >
                 >;
             }
@@ -5531,7 +5538,7 @@ export type GetCalculatorsQuery = { __typename?: 'Query' } & {
                 args: Array<
                     { __typename?: 'ConfigArgDefinition' } & Pick<
                         ConfigArgDefinition,
-                        'name' | 'type' | 'description' | 'label' | 'config'
+                        'name' | 'type' | 'description' | 'label' | 'ui'
                     >
                 >;
             }

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

@@ -111,6 +111,7 @@ export type BooleanCustomFieldConfig = CustomField & {
     __typename?: 'BooleanCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -236,7 +237,7 @@ export type ConfigArgDefinition = {
     list: Scalars['Boolean'];
     label?: Maybe<Scalars['String']>;
     description?: Maybe<Scalars['String']>;
-    config?: Maybe<Scalars['JSON']>;
+    ui?: Maybe<Scalars['JSON']>;
 };
 
 export type ConfigArgInput = {
@@ -727,6 +728,7 @@ export type CustomerSortParameter = {
 export type CustomField = {
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -779,6 +781,7 @@ export type DateTimeCustomFieldConfig = CustomField & {
     __typename?: 'DateTimeCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -865,6 +868,7 @@ export type FloatCustomFieldConfig = CustomField & {
     __typename?: 'FloatCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -968,6 +972,7 @@ export type IntCustomFieldConfig = CustomField & {
     __typename?: 'IntCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -1307,6 +1312,7 @@ export type LocaleStringCustomFieldConfig = CustomField & {
     __typename?: 'LocaleStringCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -2279,6 +2285,7 @@ export type StringCustomFieldConfig = CustomField & {
     __typename?: 'StringCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     length?: Maybe<Scalars['Int']>;
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;

+ 4 - 4
packages/core/src/api/common/validate-custom-field-value.ts

@@ -48,7 +48,7 @@ export function validateCustomFieldValue(
         default:
             assertNever(config);
     }
-    validateCustomFunction(config, value, languageCode);
+    validateCustomFunction(config as TypedCustomFieldConfig<any, any>, value, languageCode);
 }
 
 function validateCustomFunction<T extends TypedCustomFieldConfig<any, any>>(
@@ -62,7 +62,7 @@ function validateCustomFunction<T extends TypedCustomFieldConfig<any, any>>(
             throw new UserInputError(error);
         }
         if (Array.isArray(error)) {
-            const localizedError = error.find((e) => e.languageCode === languageCode) || error[0];
+            const localizedError = error.find(e => e.languageCode === languageCode) || error[0];
             throw new UserInputError(localizedError.value);
         }
     }
@@ -85,12 +85,12 @@ function validateStringField(
     }
     const options = (config as StringCustomFieldConfig).options;
     if (options) {
-        const validOptions = options.map((o) => o.value);
+        const validOptions = options.map(o => o.value);
         if (!validOptions.includes(value)) {
             throw new UserInputError('error.field-invalid-string-option', {
                 name: config.name,
                 value,
-                validOptions: validOptions.map((o) => `'${o}'`).join(', '),
+                validOptions: validOptions.map(o => `'${o}'`).join(', '),
             });
         }
     }

+ 13 - 7
packages/core/src/api/config/graphql-custom-fields.ts

@@ -32,15 +32,15 @@ export function addGraphQLCustomFields(
 
     for (const entityName of Object.keys(customFieldConfig)) {
         const customEntityFields = (customFieldConfig[entityName as keyof CustomFields] || []).filter(
-            (config) => {
+            config => {
                 return !config.internal && (publicOnly === true ? config.public !== false : true);
             },
         );
 
-        const localeStringFields = customEntityFields.filter((field) => field.type === 'localeString');
-        const nonLocaleStringFields = customEntityFields.filter((field) => field.type !== 'localeString');
-        const writeableLocaleStringFields = localeStringFields.filter((field) => !field.readonly);
-        const writeableNonLocaleStringFields = nonLocaleStringFields.filter((field) => !field.readonly);
+        const localeStringFields = customEntityFields.filter(field => field.type === 'localeString');
+        const nonLocaleStringFields = customEntityFields.filter(field => field.type !== 'localeString');
+        const writeableLocaleStringFields = localeStringFields.filter(field => !field.readonly);
+        const writeableNonLocaleStringFields = nonLocaleStringFields.filter(field => !field.readonly);
 
         if (schema.getType(entityName)) {
             if (customEntityFields.length) {
@@ -197,7 +197,7 @@ export function addRegisterCustomerCustomFieldsInput(
     if (!customerCustomFields || customerCustomFields.length === 0) {
         return schema;
     }
-    const publicWritableCustomFields = customerCustomFields.filter((fieldDef) => {
+    const publicWritableCustomFields = customerCustomFields.filter(fieldDef => {
         return fieldDef.public !== false && !fieldDef.readonly && !fieldDef.internal;
     });
     if (publicWritableCustomFields.length < 1) {
@@ -271,7 +271,13 @@ 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');
+    return fieldDefs
+        .map(field => {
+            const primitiveType = typeFn(field.type);
+            const finalType = field.list ? `[${primitiveType}!]` : primitiveType;
+            return `${field.name}: ${finalType}`;
+        })
+        .join('\n');
 }
 
 function getFilterOperator(type: CustomFieldType): string {

+ 6 - 6
packages/core/src/api/resolvers/admin/global-settings.resolver.ts

@@ -39,9 +39,9 @@ export class GlobalSettingsResolver {
         // Do not expose custom fields marked as "internal".
         const exposedCustomFieldConfig: CustomFields = {};
         for (const [entityType, customFields] of Object.entries(this.configService.customFields)) {
-            exposedCustomFieldConfig[entityType as keyof CustomFields] = customFields.filter(
-                (c) => !c.internal,
-            );
+            exposedCustomFieldConfig[entityType as keyof CustomFields] = customFields
+                .filter(c => !c.internal)
+                .map(c => ({ ...c, list: !!c.list as any }));
         }
         return {
             customFieldConfig: exposedCustomFieldConfig,
@@ -58,12 +58,12 @@ export class GlobalSettingsResolver {
         if (availableLanguages) {
             const channels = await this.channelService.findAll();
             const unavailableDefaults = channels.filter(
-                (c) => !availableLanguages.includes(c.defaultLanguageCode),
+                c => !availableLanguages.includes(c.defaultLanguageCode),
             );
             if (unavailableDefaults.length) {
                 throw new UserInputError('error.cannot-set-default-language-as-unavailable', {
-                    language: unavailableDefaults.map((c) => c.defaultLanguageCode).join(', '),
-                    channelCode: unavailableDefaults.map((c) => c.code).join(', '),
+                    language: unavailableDefaults.map(c => c.defaultLanguageCode).join(', '),
+                    channelCode: unavailableDefaults.map(c => c.code).join(', '),
                 });
             }
         }

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

@@ -31,7 +31,7 @@ type ConfigArgDefinition {
     list: Boolean!
     label: String
     description: String
-    config: JSON
+    ui: JSON
 }
 
 type ConfigurableOperation {

+ 7 - 0
packages/core/src/api/schema/common/custom-field-types.graphql

@@ -1,6 +1,7 @@
 interface CustomField {
     name: String!
     type: String!
+    list: Boolean!
     label: [LocalizedString!]
     description: [LocalizedString!]
     readonly: Boolean
@@ -10,6 +11,7 @@ interface CustomField {
 type StringCustomFieldConfig implements CustomField {
     name: String!
     type: String!
+    list: Boolean!
     length: Int
     label: [LocalizedString!]
     description: [LocalizedString!]
@@ -27,6 +29,7 @@ type StringFieldOption {
 type LocaleStringCustomFieldConfig implements CustomField {
     name: String!
     type: String!
+    list: Boolean!
     label: [LocalizedString!]
     description: [LocalizedString!]
     readonly: Boolean
@@ -36,6 +39,7 @@ type LocaleStringCustomFieldConfig implements CustomField {
 type IntCustomFieldConfig implements CustomField {
     name: String!
     type: String!
+    list: Boolean!
     label: [LocalizedString!]
     description: [LocalizedString!]
     readonly: Boolean
@@ -47,6 +51,7 @@ type IntCustomFieldConfig implements CustomField {
 type FloatCustomFieldConfig implements CustomField {
     name: String!
     type: String!
+    list: Boolean!
     label: [LocalizedString!]
     description: [LocalizedString!]
     readonly: Boolean
@@ -58,6 +63,7 @@ type FloatCustomFieldConfig implements CustomField {
 type BooleanCustomFieldConfig implements CustomField {
     name: String!
     type: String!
+    list: Boolean!
     label: [LocalizedString!]
     description: [LocalizedString!]
     readonly: Boolean
@@ -70,6 +76,7 @@ See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/datetime-loc
 type DateTimeCustomFieldConfig implements CustomField {
     name: String!
     type: String!
+    list: Boolean!
     label: [LocalizedString!]
     description: [LocalizedString!]
     readonly: Boolean

+ 36 - 10
packages/core/src/config/custom-field/custom-field-types.ts

@@ -17,26 +17,50 @@ export type DefaultValueType<T extends CustomFieldType> =
             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 type TypedCustomFieldConfig<T extends CustomFieldType, C extends CustomField> = Omit<
+export type BaseTypedCustomFieldConfig<T extends CustomFieldType, C extends CustomField> = Omit<
     C,
-    '__typename'
+    '__typename' | 'list'
 > & {
     type: T;
     /**
+     * @description
      * Whether or not the custom field is available via the Shop API.
      * @default true
      */
     public?: boolean;
-    defaultValue?: DefaultValueType<T>;
     nullable?: boolean;
+};
+
+/**
+ * @description
+ * Configures a custom field on an entity in the {@link CustomFields} config object.
+ *
+ * @docsCategory custom-fields
+ */
+export type TypedCustomSingleFieldConfig<
+    T extends CustomFieldType,
+    C extends CustomField
+> = BaseTypedCustomFieldConfig<T, C> & {
+    list?: false;
+    defaultValue?: DefaultValueType<T>;
     validate?: (value: DefaultValueType<T>) => string | LocalizedString[] | void;
 };
+
+export type TypedCustomListFieldConfig<
+    T extends CustomFieldType,
+    C extends CustomField
+> = BaseTypedCustomFieldConfig<T, C> & {
+    list?: true;
+    defaultValue?: Array<DefaultValueType<T>>;
+    validate?: (value: Array<DefaultValueType<T>>) => string | LocalizedString[] | void;
+};
+
+export type TypedCustomFieldConfig<
+    T extends CustomFieldType,
+    C extends CustomField
+> = BaseTypedCustomFieldConfig<T, C> &
+    (TypedCustomSingleFieldConfig<T, C> | TypedCustomListFieldConfig<T, C>);
+
 export type StringCustomFieldConfig = TypedCustomFieldConfig<'string', GraphQLStringCustomFieldConfig>;
 export type LocaleStringCustomFieldConfig = TypedCustomFieldConfig<
     'localeString',
@@ -72,6 +96,7 @@ export type CustomFieldConfig =
  *
  * * `name: string`: The name of the field
  * * `type: string`: A string of type {@link CustomFieldType}
+ * * `list: boolean`: If set to `true`, then the field will be an array of the specified type
  * * `label?: LocalizedString[]`: An array of localized labels for the field.
  * * `description?: LocalizedString[]`: An array of localized descriptions for the field.
  * * `public?: boolean`: Whether or not the custom field is available via the Shop API. Defaults to `true`
@@ -79,7 +104,8 @@ export type CustomFieldConfig =
  * * `internal?: boolean`: Whether or not the custom field is exposed at all via the GraphQL APIs. Defaults to `false`
  * * `defaultValue?: any`: The default value when an Entity is created with this field.
  * * `nullable?: boolean`: Whether the field is nullable in the database. If set to `false`, then a `defaultValue` should be provided.
- * * `validate?: (value: any) => string | LocalizedString[] | void`: A custom validation function.
+ * * `validate?: (value: any) => string | LocalizedString[] | void`: A custom validation function. If the value is valid, then
+ *     the function should not return a value. If a string or LocalizedString array is returned, this is interpreted as an error message.
  *
  * The `LocalizedString` type looks like this:
  *

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

@@ -50,16 +50,20 @@ function registerCustomFieldsForEntity(
     const dbEngine = config.dbConnectionOptions.type;
     if (customFields) {
         for (const customField of customFields) {
-            const { name, type, defaultValue, nullable } = customField;
+            const { name, type, list, defaultValue, nullable } = customField;
             const registerColumn = () => {
                 const options: ColumnOptions = {
-                    type: getColumnType(dbEngine, type),
+                    type: list ? 'simple-json' : getColumnType(dbEngine, type),
                     default:
-                        type === 'datetime' ? formatDefaultDatetime(dbEngine, defaultValue) : defaultValue,
+                        list && defaultValue
+                            ? JSON.stringify(defaultValue)
+                            : type === 'datetime'
+                            ? formatDefaultDatetime(dbEngine, defaultValue)
+                            : defaultValue,
                     name,
                     nullable: nullable === false ? false : true,
                 };
-                if (customField.type === 'string') {
+                if (customField.type === 'string' && !list) {
                     const length = customField.length || 255;
                     if (MAX_STRING_LENGTH < length) {
                         throw new Error(
@@ -74,7 +78,8 @@ function registerCustomFieldsForEntity(
                     // Setting precision on an sqlite datetime will cause
                     // spurious migration commands. See https://github.com/typeorm/typeorm/issues/2333
                     dbEngine !== 'sqljs' &&
-                    dbEngine !== 'sqlite'
+                    dbEngine !== 'sqlite' &&
+                    !list
                 ) {
                     options.precision = 6;
                 }

+ 10 - 2
packages/core/src/service/services/payment-method.service.ts

@@ -1,6 +1,11 @@
 import { Injectable } from '@nestjs/common';
 import { InjectConnection } from '@nestjs/typeorm';
-import { ConfigArg, RefundOrderInput, UpdatePaymentMethodInput } from '@vendure/common/lib/generated-types';
+import {
+    ConfigArg,
+    ConfigArgInput,
+    RefundOrderInput,
+    UpdatePaymentMethodInput,
+} from '@vendure/common/lib/generated-types';
 import { omit } from '@vendure/common/lib/omit';
 import { ConfigArgType, ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { assertNever } from '@vendure/common/lib/shared-utils';
@@ -62,7 +67,10 @@ export class PaymentMethodService {
                 h => h.code === paymentMethod.code,
             );
             if (handler) {
-                updatedPaymentMethod.configArgs = input.configArgs;
+                function handlerHasArgDefinition(arg: ConfigArgInput): boolean {
+                    return !!handler?.args.hasOwnProperty(arg.name);
+                }
+                updatedPaymentMethod.configArgs = input.configArgs.filter(handlerHasArgDefinition);
             }
         }
         return this.connection.getRepository(PaymentMethod).save(updatedPaymentMethod);

+ 8 - 1
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts

@@ -186,6 +186,7 @@ export type BooleanCustomFieldConfig = CustomField & {
     __typename?: 'BooleanCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -321,7 +322,7 @@ export type ConfigArgDefinition = {
     list: Scalars['Boolean'];
     label?: Maybe<Scalars['String']>;
     description?: Maybe<Scalars['String']>;
-    config?: Maybe<Scalars['JSON']>;
+    ui?: Maybe<Scalars['JSON']>;
 };
 
 export type ConfigArgInput = {
@@ -1042,6 +1043,7 @@ export type CustomerSortParameter = {
 export type CustomField = {
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -1094,6 +1096,7 @@ export type DateTimeCustomFieldConfig = CustomField & {
     __typename?: 'DateTimeCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -1219,6 +1222,7 @@ export type FloatCustomFieldConfig = CustomField & {
     __typename?: 'FloatCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -1328,6 +1332,7 @@ export type IntCustomFieldConfig = CustomField & {
     __typename?: 'IntCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -1737,6 +1742,7 @@ export type LocaleStringCustomFieldConfig = CustomField & {
     __typename?: 'LocaleStringCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
     readonly?: Maybe<Scalars['Boolean']>;
@@ -3335,6 +3341,7 @@ export type StringCustomFieldConfig = CustomField & {
     __typename?: 'StringCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
+    list: Scalars['Boolean'];
     length?: Maybe<Scalars['Int']>;
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;

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