diff --git a/packages/core/e2e/custom-fields.e2e-spec.ts b/packages/core/e2e/custom-fields.e2e-spec.ts index a53674e9cc..5ba8067783 100644 --- a/packages/core/e2e/custom-fields.e2e-spec.ts +++ b/packages/core/e2e/custom-fields.e2e-spec.ts @@ -77,6 +77,18 @@ describe('Custom fields', () => { type: 'string', options: [{ value: 'small' }, { value: 'medium' }, { value: 'large' }], }, + { + name: 'nonPublic', + type: 'string', + defaultValue: 'hi!', + public: false, + }, + { + name: 'public', + type: 'string', + defaultValue: 'ho!', + public: true, + }, ], }, }, @@ -125,6 +137,8 @@ describe('Custom fields', () => { { name: 'validateFn1', type: 'string' }, { name: 'validateFn2', type: 'string' }, { name: 'stringWithOptions', type: 'string' }, + { name: 'nonPublic', type: 'string' }, + { name: 'public', type: 'string' }, ], }); }); @@ -223,20 +237,19 @@ describe('Custom fields', () => { }, `The custom field value ['tiny'] is invalid. Valid options are ['small', 'medium', 'large']`), ); - it( - 'valid string option', async () => { - const { updateProduct } = await adminClient.query(gql` - mutation { - updateProduct(input: { id: "T_1", customFields: { stringWithOptions: "medium" } }) { - id - customFields { - stringWithOptions - } + it('valid string option', async () => { + const { updateProduct } = await adminClient.query(gql` + mutation { + updateProduct(input: { id: "T_1", customFields: { stringWithOptions: "medium" } }) { + id + customFields { + stringWithOptions } } - `); - expect(updateProduct.customFields.stringWithOptions).toBe('medium'); - }); + } + `); + expect(updateProduct.customFields.stringWithOptions).toBe('medium'); + }); it( 'invalid localeString', @@ -332,4 +345,37 @@ describe('Custom fields', () => { }, `The value ['invalid'] is not valid`), ); }); + + describe('public access', () => { + it( + 'non-public throws for Shop API', + assertThrowsWithMessage(async () => { + await shopClient.query(gql` + query { + product(id: "T_1") { + id + customFields { + nonPublic + } + } + } + `); + }, `Cannot query field "nonPublic" on type "ProductCustomFields"`), + ); + + it('publicly accessible via Shop API', async () => { + const { product } = await shopClient.query(gql` + query { + product(id: "T_1") { + id + customFields { + public + } + } + } + `); + + expect(product.customFields.public).toBe('ho!'); + }); + }); }); diff --git a/packages/core/src/api/config/__snapshots__/graphql-custom-fields.spec.ts.snap b/packages/core/src/api/config/__snapshots__/graphql-custom-fields.spec.ts.snap index d986e279c3..e360edfb75 100644 --- a/packages/core/src/api/config/__snapshots__/graphql-custom-fields.spec.ts.snap +++ b/packages/core/src/api/config/__snapshots__/graphql-custom-fields.spec.ts.snap @@ -210,6 +210,22 @@ input UpdateProductInput { " `; +exports[`addGraphQLCustomFields() publicOnly = true 1`] = ` +"scalar DateTime + +scalar JSON + +type Product { + id: ID + customFields: ProductCustomFields +} + +type ProductCustomFields { + available: Boolean +} +" +`; + exports[`addGraphQLCustomFields() uses JSON scalar if no custom fields defined 1`] = ` "scalar DateTime diff --git a/packages/core/src/api/config/configure-graphql-module.ts b/packages/core/src/api/config/configure-graphql-module.ts index e6c68aabdf..8a0275f0df 100644 --- a/packages/core/src/api/config/configure-graphql-module.ts +++ b/packages/core/src/api/config/configure-graphql-module.ts @@ -150,7 +150,7 @@ async function createGraphQLOptions( const customFields = configService.customFields; const typeDefs = await typesLoader.mergeTypesByPaths(options.typePaths); let schema = generateListOptions(typeDefs); - schema = addGraphQLCustomFields(schema, customFields); + schema = addGraphQLCustomFields(schema, customFields, apiType === 'shop'); schema = addServerConfigCustomFields(schema, customFields); schema = addOrderLineCustomFieldsInput(schema, customFields.OrderLine || []); const pluginSchemaExtensions = getPluginAPIExtensions(configService.plugins, apiType).map( diff --git a/packages/core/src/api/config/graphql-custom-fields.spec.ts b/packages/core/src/api/config/graphql-custom-fields.spec.ts index b941c04c7b..daf6978fc3 100644 --- a/packages/core/src/api/config/graphql-custom-fields.spec.ts +++ b/packages/core/src/api/config/graphql-custom-fields.spec.ts @@ -14,7 +14,7 @@ describe('addGraphQLCustomFields()', () => { const customFieldConfig: CustomFields = { Product: [], }; - const result = addGraphQLCustomFields(input, customFieldConfig); + const result = addGraphQLCustomFields(input, customFieldConfig, false); expect(printSchema(result)).toMatchSnapshot(); }); @@ -27,7 +27,7 @@ describe('addGraphQLCustomFields()', () => { const customFieldConfig: CustomFields = { Product: [{ name: 'available', type: 'boolean' }], }; - const result = addGraphQLCustomFields(input, customFieldConfig); + const result = addGraphQLCustomFields(input, customFieldConfig, false); expect(printSchema(result)).toMatchSnapshot(); }); @@ -45,7 +45,7 @@ describe('addGraphQLCustomFields()', () => { const customFieldConfig: CustomFields = { Product: [{ name: 'available', type: 'boolean' }, { name: 'shortName', type: 'localeString' }], }; - const result = addGraphQLCustomFields(input, customFieldConfig); + const result = addGraphQLCustomFields(input, customFieldConfig, false); expect(printSchema(result)).toMatchSnapshot(); }); @@ -62,7 +62,7 @@ describe('addGraphQLCustomFields()', () => { const customFieldConfig: CustomFields = { Product: [{ name: 'available', type: 'boolean' }, { name: 'shortName', type: 'localeString' }], }; - const result = addGraphQLCustomFields(input, customFieldConfig); + const result = addGraphQLCustomFields(input, customFieldConfig, false); expect(printSchema(result)).toMatchSnapshot(); }); @@ -79,7 +79,7 @@ describe('addGraphQLCustomFields()', () => { const customFieldConfig: CustomFields = { Product: [{ name: 'available', type: 'boolean' }, { name: 'shortName', type: 'localeString' }], }; - const result = addGraphQLCustomFields(input, customFieldConfig); + const result = addGraphQLCustomFields(input, customFieldConfig, false); expect(printSchema(result)).toMatchSnapshot(); }); @@ -104,7 +104,7 @@ describe('addGraphQLCustomFields()', () => { const customFieldConfig: CustomFields = { Product: [{ name: 'available', type: 'boolean' }, { name: 'shortName', type: 'localeString' }], }; - const result = addGraphQLCustomFields(input, customFieldConfig); + const result = addGraphQLCustomFields(input, customFieldConfig, false); expect(printSchema(result)).toMatchSnapshot(); }); @@ -126,7 +126,7 @@ describe('addGraphQLCustomFields()', () => { const customFieldConfig: CustomFields = { Product: [{ name: 'available', type: 'boolean' }, { name: 'shortName', type: 'localeString' }], }; - const result = addGraphQLCustomFields(input, customFieldConfig); + const result = addGraphQLCustomFields(input, customFieldConfig, false); expect(printSchema(result)).toMatchSnapshot(); }); @@ -165,7 +165,23 @@ describe('addGraphQLCustomFields()', () => { { name: 'published', type: 'datetime' }, ], }; - const result = addGraphQLCustomFields(input, customFieldConfig); + const result = addGraphQLCustomFields(input, customFieldConfig, false); + expect(printSchema(result)).toMatchSnapshot(); + }); + + it('publicOnly = true', () => { + const input = ` + type Product { + id: ID + } + `; + const customFieldConfig: CustomFields = { + Product: [ + { name: 'available', type: 'boolean', public: true }, + { name: 'profitMargin', type: 'float', public: false }, + ], + }; + const result = addGraphQLCustomFields(input, customFieldConfig, true); expect(printSchema(result)).toMatchSnapshot(); }); }); diff --git a/packages/core/src/api/config/graphql-custom-fields.ts b/packages/core/src/api/config/graphql-custom-fields.ts index 61dba8077e..9ea73c497f 100644 --- a/packages/core/src/api/config/graphql-custom-fields.ts +++ b/packages/core/src/api/config/graphql-custom-fields.ts @@ -12,6 +12,7 @@ import { CustomFieldConfig, CustomFields } from '../../config/custom-field/custo export function addGraphQLCustomFields( typeDefsOrSchema: string | GraphQLSchema, customFieldConfig: CustomFields, + publicOnly: boolean, ): GraphQLSchema { const schema = typeof typeDefsOrSchema === 'string' ? buildSchema(typeDefsOrSchema) : typeDefsOrSchema; @@ -30,7 +31,9 @@ export function addGraphQLCustomFields( } for (const entityName of Object.keys(customFieldConfig)) { - const customEntityFields = customFieldConfig[entityName as keyof CustomFields] || []; + const customEntityFields = (customFieldConfig[entityName as keyof CustomFields] || []).filter(config => { + return (publicOnly === true) ? config.public !== false : true; + }); const localeStringFields = customEntityFields.filter(field => field.type === 'localeString'); const nonLocaleStringFields = customEntityFields.filter(field => field.type !== 'localeString'); diff --git a/packages/core/src/config/custom-field/custom-field-types.ts b/packages/core/src/config/custom-field/custom-field-types.ts index abb342a996..f072595c05 100644 --- a/packages/core/src/config/custom-field/custom-field-types.ts +++ b/packages/core/src/config/custom-field/custom-field-types.ts @@ -28,6 +28,11 @@ export type TypedCustomFieldConfig & { type: T; + /** + * Whether or not the custom field is available via the Shop API. + * @default true + */ + public?: boolean; defaultValue?: DefaultValueType; nullable?: boolean; validate?: (value: DefaultValueType) => string | LocalizedString[] | void;