فهرست منبع

feat(core): Implement internal and readonly CustomField properties

Relates to #216
Michael Bromley 6 سال پیش
والد
کامیت
c2ae44f845

+ 32 - 3
packages/admin-ui/src/app/common/generated-types.ts

@@ -175,6 +175,8 @@ export type BooleanCustomFieldConfig = CustomField & {
   type: Scalars['String'],
   label?: Maybe<Array<LocalizedString>>,
   description?: Maybe<Array<LocalizedString>>,
+  readonly?: Maybe<Scalars['Boolean']>,
+  internal?: Maybe<Scalars['Boolean']>,
 };
 
 export type BooleanOperators = {
@@ -485,12 +487,16 @@ export type CreateGroupOptionInput = {
   translations: Array<ProductOptionGroupTranslationInput>,
 };
 
+export type CreateProductCustomFieldsInput = {
+  rating?: Maybe<Scalars['Float']>,
+};
+
 export type CreateProductInput = {
   featuredAssetId?: Maybe<Scalars['ID']>,
   assetIds?: Maybe<Array<Scalars['ID']>>,
   facetValueIds?: Maybe<Array<Scalars['ID']>>,
   translations: Array<ProductTranslationInput>,
-  customFields?: Maybe<Scalars['JSON']>,
+  customFields?: Maybe<CreateProductCustomFieldsInput>,
 };
 
 export type CreateProductOptionGroupInput = {
@@ -984,6 +990,8 @@ export type CustomField = {
   type: Scalars['String'],
   label?: Maybe<Array<LocalizedString>>,
   description?: Maybe<Array<LocalizedString>>,
+  readonly?: Maybe<Scalars['Boolean']>,
+  internal?: Maybe<Scalars['Boolean']>,
 };
 
 export type CustomFieldConfig = StringCustomFieldConfig | LocaleStringCustomFieldConfig | IntCustomFieldConfig | FloatCustomFieldConfig | BooleanCustomFieldConfig | DateTimeCustomFieldConfig;
@@ -1027,6 +1035,8 @@ export type DateTimeCustomFieldConfig = CustomField & {
   type: Scalars['String'],
   label?: Maybe<Array<LocalizedString>>,
   description?: Maybe<Array<LocalizedString>>,
+  readonly?: Maybe<Scalars['Boolean']>,
+  internal?: Maybe<Scalars['Boolean']>,
   min?: Maybe<Scalars['String']>,
   max?: Maybe<Scalars['String']>,
   step?: Maybe<Scalars['Int']>,
@@ -1149,6 +1159,8 @@ export type FloatCustomFieldConfig = CustomField & {
   type: Scalars['String'],
   label?: Maybe<Array<LocalizedString>>,
   description?: Maybe<Array<LocalizedString>>,
+  readonly?: Maybe<Scalars['Boolean']>,
+  internal?: Maybe<Scalars['Boolean']>,
   min?: Maybe<Scalars['Float']>,
   max?: Maybe<Scalars['Float']>,
   step?: Maybe<Scalars['Float']>,
@@ -1242,6 +1254,8 @@ export type IntCustomFieldConfig = CustomField & {
   type: Scalars['String'],
   label?: Maybe<Array<LocalizedString>>,
   description?: Maybe<Array<LocalizedString>>,
+  readonly?: Maybe<Scalars['Boolean']>,
+  internal?: Maybe<Scalars['Boolean']>,
   min?: Maybe<Scalars['Int']>,
   max?: Maybe<Scalars['Int']>,
   step?: Maybe<Scalars['Int']>,
@@ -1655,6 +1669,8 @@ export type LocaleStringCustomFieldConfig = CustomField & {
   type: Scalars['String'],
   label?: Maybe<Array<LocalizedString>>,
   description?: Maybe<Array<LocalizedString>>,
+  readonly?: Maybe<Scalars['Boolean']>,
+  internal?: Maybe<Scalars['Boolean']>,
   pattern?: Maybe<Scalars['String']>,
 };
 
@@ -2492,7 +2508,12 @@ export type Product = Node & {
   facetValues: Array<FacetValue>,
   translations: Array<ProductTranslation>,
   collections: Array<Collection>,
-  customFields?: Maybe<Scalars['JSON']>,
+  customFields?: Maybe<ProductCustomFields>,
+};
+
+export type ProductCustomFields = {
+  __typename?: 'ProductCustomFields',
+  rating?: Maybe<Scalars['Float']>,
 };
 
 export type ProductFilterParameter = {
@@ -2503,6 +2524,7 @@ export type ProductFilterParameter = {
   name?: Maybe<StringOperators>,
   slug?: Maybe<StringOperators>,
   description?: Maybe<StringOperators>,
+  rating?: Maybe<NumberOperators>,
 };
 
 export type ProductList = PaginatedList & {
@@ -2583,6 +2605,7 @@ export type ProductSortParameter = {
   name?: Maybe<SortOrder>,
   slug?: Maybe<SortOrder>,
   description?: Maybe<SortOrder>,
+  rating?: Maybe<SortOrder>,
 };
 
 export type ProductTranslation = {
@@ -3250,6 +3273,8 @@ export type StringCustomFieldConfig = CustomField & {
   length?: Maybe<Scalars['Int']>,
   label?: Maybe<Array<LocalizedString>>,
   description?: Maybe<Array<LocalizedString>>,
+  readonly?: Maybe<Scalars['Boolean']>,
+  internal?: Maybe<Scalars['Boolean']>,
   pattern?: Maybe<Scalars['String']>,
   options?: Maybe<Array<StringFieldOption>>,
 };
@@ -3448,6 +3473,10 @@ export type UpdatePaymentMethodInput = {
   configArgs?: Maybe<Array<ConfigArgInput>>,
 };
 
+export type UpdateProductCustomFieldsInput = {
+  rating?: Maybe<Scalars['Float']>,
+};
+
 export type UpdateProductInput = {
   id: Scalars['ID'],
   enabled?: Maybe<Scalars['Boolean']>,
@@ -3455,7 +3484,7 @@ export type UpdateProductInput = {
   assetIds?: Maybe<Array<Scalars['ID']>>,
   facetValueIds?: Maybe<Array<Scalars['ID']>>,
   translations?: Maybe<Array<ProductTranslationInput>>,
-  customFields?: Maybe<Scalars['JSON']>,
+  customFields?: Maybe<UpdateProductCustomFieldsInput>,
 };
 
 export type UpdateProductOptionGroupInput = {

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

@@ -105,6 +105,8 @@ export type BooleanCustomFieldConfig = CustomField & {
     type: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
 };
 
 export type BooleanOperators = {
@@ -676,6 +678,8 @@ export type CustomField = {
     type: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
 };
 
 export type CustomFieldConfig =
@@ -724,6 +728,8 @@ export type DateTimeCustomFieldConfig = CustomField & {
     type: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
     min?: Maybe<Scalars['String']>;
     max?: Maybe<Scalars['String']>;
     step?: Maybe<Scalars['Int']>;
@@ -807,6 +813,8 @@ export type FloatCustomFieldConfig = CustomField & {
     type: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
     min?: Maybe<Scalars['Float']>;
     max?: Maybe<Scalars['Float']>;
     step?: Maybe<Scalars['Float']>;
@@ -894,6 +902,8 @@ export type IntCustomFieldConfig = CustomField & {
     type: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
     min?: Maybe<Scalars['Int']>;
     max?: Maybe<Scalars['Int']>;
     step?: Maybe<Scalars['Int']>;
@@ -1281,6 +1291,8 @@ export type LocaleStringCustomFieldConfig = CustomField & {
     type: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
     pattern?: Maybe<Scalars['String']>;
 };
 
@@ -2133,6 +2145,8 @@ export type StringCustomFieldConfig = CustomField & {
     length?: Maybe<Scalars['Int']>;
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
     pattern?: Maybe<Scalars['String']>;
     options?: Maybe<Array<StringFieldOption>>;
 };

+ 14 - 0
packages/common/src/generated-types.ts

@@ -174,6 +174,8 @@ export type BooleanCustomFieldConfig = CustomField & {
   type: Scalars['String'],
   label?: Maybe<Array<LocalizedString>>,
   description?: Maybe<Array<LocalizedString>>,
+  readonly?: Maybe<Scalars['Boolean']>,
+  internal?: Maybe<Scalars['Boolean']>,
 };
 
 export type BooleanOperators = {
@@ -976,6 +978,8 @@ export type CustomField = {
   type: Scalars['String'],
   label?: Maybe<Array<LocalizedString>>,
   description?: Maybe<Array<LocalizedString>>,
+  readonly?: Maybe<Scalars['Boolean']>,
+  internal?: Maybe<Scalars['Boolean']>,
 };
 
 export type CustomFieldConfig = StringCustomFieldConfig | LocaleStringCustomFieldConfig | IntCustomFieldConfig | FloatCustomFieldConfig | BooleanCustomFieldConfig | DateTimeCustomFieldConfig;
@@ -1019,6 +1023,8 @@ export type DateTimeCustomFieldConfig = CustomField & {
   type: Scalars['String'],
   label?: Maybe<Array<LocalizedString>>,
   description?: Maybe<Array<LocalizedString>>,
+  readonly?: Maybe<Scalars['Boolean']>,
+  internal?: Maybe<Scalars['Boolean']>,
   min?: Maybe<Scalars['String']>,
   max?: Maybe<Scalars['String']>,
   step?: Maybe<Scalars['Int']>,
@@ -1141,6 +1147,8 @@ export type FloatCustomFieldConfig = CustomField & {
   type: Scalars['String'],
   label?: Maybe<Array<LocalizedString>>,
   description?: Maybe<Array<LocalizedString>>,
+  readonly?: Maybe<Scalars['Boolean']>,
+  internal?: Maybe<Scalars['Boolean']>,
   min?: Maybe<Scalars['Float']>,
   max?: Maybe<Scalars['Float']>,
   step?: Maybe<Scalars['Float']>,
@@ -1234,6 +1242,8 @@ export type IntCustomFieldConfig = CustomField & {
   type: Scalars['String'],
   label?: Maybe<Array<LocalizedString>>,
   description?: Maybe<Array<LocalizedString>>,
+  readonly?: Maybe<Scalars['Boolean']>,
+  internal?: Maybe<Scalars['Boolean']>,
   min?: Maybe<Scalars['Int']>,
   max?: Maybe<Scalars['Int']>,
   step?: Maybe<Scalars['Int']>,
@@ -1647,6 +1657,8 @@ export type LocaleStringCustomFieldConfig = CustomField & {
   type: Scalars['String'],
   label?: Maybe<Array<LocalizedString>>,
   description?: Maybe<Array<LocalizedString>>,
+  readonly?: Maybe<Scalars['Boolean']>,
+  internal?: Maybe<Scalars['Boolean']>,
   pattern?: Maybe<Scalars['String']>,
 };
 
@@ -3207,6 +3219,8 @@ export type StringCustomFieldConfig = CustomField & {
   length?: Maybe<Scalars['Int']>,
   label?: Maybe<Array<LocalizedString>>,
   description?: Maybe<Array<LocalizedString>>,
+  readonly?: Maybe<Scalars['Boolean']>,
+  internal?: Maybe<Scalars['Boolean']>,
   pattern?: Maybe<Scalars['String']>,
   options?: Maybe<Array<StringFieldOption>>,
 };

+ 122 - 0
packages/core/e2e/custom-fields.e2e-spec.ts

@@ -85,6 +85,16 @@ const customConfig = {
                     name: 'longString',
                     type: 'string',
                 },
+                {
+                    name: 'readonlyString',
+                    type: 'string',
+                    readonly: true,
+                },
+                {
+                    name: 'internalString',
+                    type: 'string',
+                    internal: true,
+                },
             ],
             Facet: [
                 {
@@ -92,6 +102,13 @@ const customConfig = {
                     type: 'localeString',
                 },
             ],
+            Customer: [
+                {
+                    name: 'score',
+                    type: 'int',
+                    readonly: true,
+                },
+            ],
         } as CustomFields,
     },
 };
@@ -152,6 +169,9 @@ describe('Custom fields', () => {
                 { name: 'nonPublic', type: 'string' },
                 { name: 'public', type: 'string' },
                 { name: 'longString', type: 'string' },
+                { name: 'readonlyString', type: 'string' },
+                // The internal type should not be exposed at all
+                // { name: 'internalString', type: 'string' },
             ],
         });
     });
@@ -245,6 +265,50 @@ describe('Custom fields', () => {
         }, 'NOT NULL constraint failed: product.customFieldsNotnullable'),
     );
 
+    it(
+        'thows on attempt to update readonly field',
+        assertThrowsWithMessage(async () => {
+            await adminClient.query(gql`
+                mutation {
+                    updateProduct(input: { id: "T_1", customFields: { readonlyString: "hello" } }) {
+                        id
+                    }
+                }
+            `);
+        }, `Field "readonlyString" is not defined by type UpdateProductCustomFieldsInput`),
+    );
+
+    it(
+        'thows on attempt to update readonly field when no other custom fields defined',
+        assertThrowsWithMessage(async () => {
+            await adminClient.query(gql`
+                mutation {
+                    updateCustomer(input: { id: "T_1", customFields: { score: 5 } }) {
+                        id
+                    }
+                }
+            `);
+        }, `The custom field 'score' is readonly`),
+    );
+
+    it(
+        'thows on attempt to create readonly field',
+        assertThrowsWithMessage(async () => {
+            await adminClient.query(gql`
+                mutation {
+                    createProduct(
+                        input: {
+                            translations: [{ languageCode: en, name: "test" }]
+                            customFields: { readonlyString: "hello" }
+                        }
+                    ) {
+                        id
+                    }
+                }
+            `);
+        }, `Field "readonlyString" is not defined by type CreateProductCustomFieldsInput`),
+    );
+
     it('string length allows long strings', async () => {
         const longString = Array.from({ length: 5000 }, v => 'hello there!').join(' ');
         const result = await adminClient.query(
@@ -431,6 +495,38 @@ describe('Custom fields', () => {
 
             expect(product.customFields.public).toBe('ho!');
         });
+
+        it(
+            'internal throws for Shop API',
+            assertThrowsWithMessage(async () => {
+                await shopClient.query(gql`
+                    query {
+                        product(id: "T_1") {
+                            id
+                            customFields {
+                                internalString
+                            }
+                        }
+                    }
+                `);
+            }, `Cannot query field "internalString" on type "ProductCustomFields"`),
+        );
+
+        it(
+            'internal throws for Admin API',
+            assertThrowsWithMessage(async () => {
+                await adminClient.query(gql`
+                    query {
+                        product(id: "T_1") {
+                            id
+                            customFields {
+                                internalString
+                            }
+                        }
+                    }
+                `);
+            }, `Cannot query field "internalString" on type "ProductCustomFields"`),
+        );
     });
 
     describe('sort & filter', () => {
@@ -457,5 +553,31 @@ describe('Custom fields', () => {
 
             expect(products.totalItems).toBe(1);
         });
+
+        it(
+            'cannot filter by internal field in Admin API',
+            assertThrowsWithMessage(async () => {
+                await adminClient.query(gql`
+                    query {
+                        products(options: { filter: { internalString: { contains: "hello" } } }) {
+                            totalItems
+                        }
+                    }
+                `);
+            }, `Field "internalString" is not defined by type ProductFilterParameter`),
+        );
+
+        it(
+            'cannot filter by internal field in Shop API',
+            assertThrowsWithMessage(async () => {
+                await shopClient.query(gql`
+                    query {
+                        products(options: { filter: { internalString: { contains: "hello" } } }) {
+                            totalItems
+                        }
+                    }
+                `);
+            }, `Field "internalString" is not defined by type ProductFilterParameter`),
+        );
     });
 });

+ 14 - 0
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -174,6 +174,8 @@ export type BooleanCustomFieldConfig = CustomField & {
     type: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
 };
 
 export type BooleanOperators = {
@@ -975,6 +977,8 @@ export type CustomField = {
     type: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
 };
 
 export type CustomFieldConfig =
@@ -1023,6 +1027,8 @@ export type DateTimeCustomFieldConfig = CustomField & {
     type: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
     min?: Maybe<Scalars['String']>;
     max?: Maybe<Scalars['String']>;
     step?: Maybe<Scalars['Int']>;
@@ -1145,6 +1151,8 @@ export type FloatCustomFieldConfig = CustomField & {
     type: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
     min?: Maybe<Scalars['Float']>;
     max?: Maybe<Scalars['Float']>;
     step?: Maybe<Scalars['Float']>;
@@ -1238,6 +1246,8 @@ export type IntCustomFieldConfig = CustomField & {
     type: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
     min?: Maybe<Scalars['Int']>;
     max?: Maybe<Scalars['Int']>;
     step?: Maybe<Scalars['Int']>;
@@ -1650,6 +1660,8 @@ export type LocaleStringCustomFieldConfig = CustomField & {
     type: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
     pattern?: Maybe<Scalars['String']>;
 };
 
@@ -3102,6 +3114,8 @@ export type StringCustomFieldConfig = CustomField & {
     length?: Maybe<Scalars['Int']>;
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
     pattern?: Maybe<Scalars['String']>;
     options?: Maybe<Array<StringFieldOption>>;
 };

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

@@ -105,6 +105,8 @@ export type BooleanCustomFieldConfig = CustomField & {
     type: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
 };
 
 export type BooleanOperators = {
@@ -676,6 +678,8 @@ export type CustomField = {
     type: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
 };
 
 export type CustomFieldConfig =
@@ -724,6 +728,8 @@ export type DateTimeCustomFieldConfig = CustomField & {
     type: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
     min?: Maybe<Scalars['String']>;
     max?: Maybe<Scalars['String']>;
     step?: Maybe<Scalars['Int']>;
@@ -807,6 +813,8 @@ export type FloatCustomFieldConfig = CustomField & {
     type: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
     min?: Maybe<Scalars['Float']>;
     max?: Maybe<Scalars['Float']>;
     step?: Maybe<Scalars['Float']>;
@@ -894,6 +902,8 @@ export type IntCustomFieldConfig = CustomField & {
     type: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
     min?: Maybe<Scalars['Int']>;
     max?: Maybe<Scalars['Int']>;
     step?: Maybe<Scalars['Int']>;
@@ -1281,6 +1291,8 @@ export type LocaleStringCustomFieldConfig = CustomField & {
     type: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
     pattern?: Maybe<Scalars['String']>;
 };
 
@@ -2133,6 +2145,8 @@ export type StringCustomFieldConfig = CustomField & {
     length?: Maybe<Scalars['Int']>;
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
     pattern?: Maybe<Scalars['String']>;
     options?: Maybe<Array<StringFieldOption>>;
 };

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

@@ -22,6 +22,9 @@ export function validateCustomFieldValue(
     value: any,
     languageCode?: LanguageCode,
 ): void {
+    if (config.readonly) {
+        throw new UserInputError('error.field-invalid-readonly', { name: config.name });
+    }
     switch (config.type) {
         case 'string':
         case 'localeString':

+ 10 - 8
packages/core/src/api/config/graphql-custom-fields.ts

@@ -33,12 +33,14 @@ export function addGraphQLCustomFields(
     for (const entityName of Object.keys(customFieldConfig)) {
         const customEntityFields = (customFieldConfig[entityName as keyof CustomFields] || []).filter(
             config => {
-                return publicOnly === true ? config.public !== false : true;
+                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);
 
         if (schema.getType(entityName)) {
             if (customEntityFields.length) {
@@ -73,10 +75,10 @@ export function addGraphQLCustomFields(
         }
 
         if (schema.getType(`Create${entityName}Input`)) {
-            if (nonLocaleStringFields.length) {
+            if (writeableNonLocaleStringFields.length) {
                 customFieldTypeDefs += `
                     input Create${entityName}CustomFieldsInput {
-                       ${mapToFields(nonLocaleStringFields, getGraphQlType)}
+                       ${mapToFields(writeableNonLocaleStringFields, getGraphQlType)}
                     }
 
                     extend input Create${entityName}Input {
@@ -93,10 +95,10 @@ export function addGraphQLCustomFields(
         }
 
         if (schema.getType(`Update${entityName}Input`)) {
-            if (nonLocaleStringFields.length) {
+            if (writeableNonLocaleStringFields.length) {
                 customFieldTypeDefs += `
                     input Update${entityName}CustomFieldsInput {
-                       ${mapToFields(nonLocaleStringFields, getGraphQlType)}
+                       ${mapToFields(writeableNonLocaleStringFields, getGraphQlType)}
                     }
 
                     extend input Update${entityName}Input {
@@ -128,11 +130,11 @@ export function addGraphQLCustomFields(
                 `;
         }
 
-        if (localeStringFields && schema.getType(`${entityName}TranslationInput`)) {
-            if (localeStringFields.length) {
+        if (writeableLocaleStringFields && schema.getType(`${entityName}TranslationInput`)) {
+            if (writeableLocaleStringFields.length) {
                 customFieldTypeDefs += `
                     input ${entityName}TranslationCustomFieldsInput {
-                        ${mapToFields(localeStringFields, getGraphQlType)}
+                        ${mapToFields(writeableLocaleStringFields, getGraphQlType)}
                     }
 
                     extend input ${entityName}TranslationInput {

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

@@ -1,8 +1,9 @@
 import { Args, Mutation, Query, ResolveProperty, Resolver } from '@nestjs/graphql';
-import { Permission, MutationUpdateGlobalSettingsArgs } from '@vendure/common/lib/generated-types';
+import { MutationUpdateGlobalSettingsArgs, Permission } from '@vendure/common/lib/generated-types';
 
 import { VendureConfig } from '../../../config';
 import { ConfigService } from '../../../config/config.service';
+import { CustomFields } from '../../../config/custom-field/custom-field-types';
 import { GlobalSettingsService } from '../../../service/services/global-settings.service';
 import { Allow } from '../../decorators/allow.decorator';
 
@@ -21,8 +22,15 @@ export class GlobalSettingsResolver {
      */
     @ResolveProperty()
     serverConfig() {
+        // 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,
+            );
+        }
         return {
-            customFieldConfig: this.configService.customFields,
+            customFieldConfig: exposedCustomFieldConfig,
         };
     }
 

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

@@ -3,6 +3,8 @@ interface CustomField {
     type: String!
     label: [LocalizedString!]
     description: [LocalizedString!]
+    readonly: Boolean
+    internal: Boolean
 }
 
 type StringCustomFieldConfig implements CustomField {
@@ -11,6 +13,8 @@ type StringCustomFieldConfig implements CustomField {
     length: Int
     label: [LocalizedString!]
     description: [LocalizedString!]
+    readonly: Boolean
+    internal: Boolean
     pattern: String
     options: [StringFieldOption!]
 }
@@ -25,6 +29,8 @@ type LocaleStringCustomFieldConfig implements CustomField {
     type: String!
     label: [LocalizedString!]
     description: [LocalizedString!]
+    readonly: Boolean
+    internal: Boolean
     pattern: String
 }
 type IntCustomFieldConfig implements CustomField {
@@ -32,6 +38,8 @@ type IntCustomFieldConfig implements CustomField {
     type: String!
     label: [LocalizedString!]
     description: [LocalizedString!]
+    readonly: Boolean
+    internal: Boolean
     min: Int
     max: Int
     step: Int
@@ -41,6 +49,8 @@ type FloatCustomFieldConfig implements CustomField {
     type: String!
     label: [LocalizedString!]
     description: [LocalizedString!]
+    readonly: Boolean
+    internal: Boolean
     min: Float
     max: Float
     step: Float
@@ -50,6 +60,8 @@ type BooleanCustomFieldConfig implements CustomField {
     type: String!
     label: [LocalizedString!]
     description: [LocalizedString!]
+    readonly: Boolean
+    internal: Boolean
 }
 """
 Expects the same validation formats as the <input type="datetime-local"> HTML element.
@@ -60,6 +72,8 @@ type DateTimeCustomFieldConfig implements CustomField {
     type: String!
     label: [LocalizedString!]
     description: [LocalizedString!]
+    readonly: Boolean
+    internal: Boolean
     min: String
     max: String
     step: Int

+ 2 - 0
packages/core/src/config/custom-field/custom-field-types.ts

@@ -75,6 +75,8 @@ export type CustomFieldConfig =
  * * `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`
+ * * `readonly?: boolean`: Whether or not the custom field can be updated via the GraphQL APIs. Defaults to `false`
+ * * `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.

+ 1 - 0
packages/core/src/i18n/messages/en.json

@@ -29,6 +29,7 @@
     "field-invalid-datetime-range-min": "The custom field '{ name }' value [{ value }] is less than the minimum [{ min }]",
     "field-invalid-number-range-max": "The custom field '{ name }' value [{ value }] is greater than the maximum [{ max }]",
     "field-invalid-number-range-min": "The custom field '{ name }' value [{ value }] is less than the minimum [{ min }]",
+    "field-invalid-readonly": "The custom field '{ name }' is readonly",
     "field-invalid-string-option": "The custom field '{ name }' value ['{ value }'] is invalid. Valid options are [{ validOptions }]",
     "field-invalid-string-pattern": "The custom field '{ name }' value ['{ value }'] does not match the pattern [{ pattern }]",
     "forbidden": "You are not currently authorized to perform this action",

+ 14 - 0
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts

@@ -174,6 +174,8 @@ export type BooleanCustomFieldConfig = CustomField & {
     type: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
 };
 
 export type BooleanOperators = {
@@ -975,6 +977,8 @@ export type CustomField = {
     type: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
 };
 
 export type CustomFieldConfig =
@@ -1023,6 +1027,8 @@ export type DateTimeCustomFieldConfig = CustomField & {
     type: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
     min?: Maybe<Scalars['String']>;
     max?: Maybe<Scalars['String']>;
     step?: Maybe<Scalars['Int']>;
@@ -1145,6 +1151,8 @@ export type FloatCustomFieldConfig = CustomField & {
     type: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
     min?: Maybe<Scalars['Float']>;
     max?: Maybe<Scalars['Float']>;
     step?: Maybe<Scalars['Float']>;
@@ -1238,6 +1246,8 @@ export type IntCustomFieldConfig = CustomField & {
     type: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
     min?: Maybe<Scalars['Int']>;
     max?: Maybe<Scalars['Int']>;
     step?: Maybe<Scalars['Int']>;
@@ -1650,6 +1660,8 @@ export type LocaleStringCustomFieldConfig = CustomField & {
     type: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
     pattern?: Maybe<Scalars['String']>;
 };
 
@@ -3102,6 +3114,8 @@ export type StringCustomFieldConfig = CustomField & {
     length?: Maybe<Scalars['Int']>;
     label?: Maybe<Array<LocalizedString>>;
     description?: Maybe<Array<LocalizedString>>;
+    readonly?: Maybe<Scalars['Boolean']>;
+    internal?: Maybe<Scalars['Boolean']>;
     pattern?: Maybe<Scalars['String']>;
     options?: Maybe<Array<StringFieldOption>>;
 };

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
schema-admin.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
schema-shop.json


برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است