Skip to content

Commit

Permalink
feat(core): Implement access control for custom fields
Browse files Browse the repository at this point in the history
Relates to #85. Not full permissions-based access, but a `public` flag which can be used to hide a custom field from the Shop API.
  • Loading branch information
michaelbromley committed Jul 16, 2019
1 parent bc0813e commit 8f763b2
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 22 deletions.
70 changes: 58 additions & 12 deletions packages/core/e2e/custom-fields.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
],
},
},
Expand Down Expand Up @@ -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' },
],
});
});
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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!');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/api/config/configure-graphql-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
32 changes: 24 additions & 8 deletions packages/core/src/api/config/graphql-custom-fields.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

Expand All @@ -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();
});

Expand All @@ -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();
});

Expand All @@ -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();
});

Expand All @@ -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();
});

Expand All @@ -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();
});

Expand All @@ -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();
});

Expand Down Expand Up @@ -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();
});
});
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/api/config/graphql-custom-fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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');
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/config/custom-field/custom-field-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export type TypedCustomFieldConfig<T extends CustomFieldType, C extends CustomFi
'__typename'
> & {
type: T;
/**
* Whether or not the custom field is available via the Shop API.
* @default true
*/
public?: boolean;
defaultValue?: DefaultValueType<T>;
nullable?: boolean;
validate?: (value: DefaultValueType<T>) => string | LocalizedString[] | void;
Expand Down

0 comments on commit 8f763b2

Please sign in to comment.