From ae6e87781c9cfdb884a296a1b811bd26d7539637 Mon Sep 17 00:00:00 2001 From: Chris Kalmar Date: Wed, 25 Mar 2020 19:26:18 +0100 Subject: [PATCH 01/12] fix error message --- src/engine/action/Action.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/action/Action.js b/src/engine/action/Action.js index 6242146e..cedb0264 100644 --- a/src/engine/action/Action.js +++ b/src/engine/action/Action.js @@ -52,7 +52,7 @@ export class Action { passOrThrow( isFunction(postProcessor), () => - `postProcessor of mutation '${name}' needs to be a valid function`, + `postProcessor of action '${name}' needs to be a valid function`, ); this.postProcessor = postProcessor; From 6fce7974707a095127b293ec767b9ab17bbe2124 Mon Sep 17 00:00:00 2001 From: Chris Kalmar Date: Wed, 25 Mar 2020 19:26:37 +0100 Subject: [PATCH 02/12] add preProcessor setup for actions --- src/engine/action/Action.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/engine/action/Action.js b/src/engine/action/Action.js index cedb0264..2484a7dd 100644 --- a/src/engine/action/Action.js +++ b/src/engine/action/Action.js @@ -19,6 +19,7 @@ export class Action { resolve, type, permissions, + preProcessor, postProcessor, } = setup; @@ -48,6 +49,15 @@ export class Action { )}'`, ); + if (preProcessor) { + passOrThrow( + isFunction(preProcessor), + () => `preProcessor of of action '${name}' needs to be a valid function`, + ); + + this.preProcessor = preProcessor; + } + if (postProcessor) { passOrThrow( isFunction(postProcessor), From 2fae3b0c9f6e83d78b49569074f561bef63e75a4 Mon Sep 17 00:00:00 2001 From: Chris Kalmar Date: Wed, 25 Mar 2020 19:27:37 +0100 Subject: [PATCH 03/12] implement preProcessor call in action processing --- src/graphqlProtocol/action.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/graphqlProtocol/action.ts b/src/graphqlProtocol/action.ts index 6781901b..25d837dd 100644 --- a/src/graphqlProtocol/action.ts +++ b/src/graphqlProtocol/action.ts @@ -97,7 +97,7 @@ export const handlePermission = async (context, action, input) => { userRoles, action, input, - context + context, ); if (!permissionWhere) { @@ -210,6 +210,10 @@ export const generateActions = (graphRegistry, actionTypeFilter) => { await handlePermission(context, action, payload); try { + if (action.preProcessor) { + await action.preProcessor(action, source, payload, context, info); + } + const result = await action.resolve(source, payload, context, info); if (action.postProcessor) { @@ -228,8 +232,7 @@ export const generateActions = (graphRegistry, actionTypeFilter) => { result, clientMutationId, }; - } - catch (error) { + } catch (error) { if (action.postProcessor) { await action.postProcessor( error, From f029f80ea6322062c86593d7a2f5a597bdd433d1 Mon Sep 17 00:00:00 2001 From: Chris Kalmar Date: Wed, 25 Mar 2020 19:28:13 +0100 Subject: [PATCH 04/12] update test schema generator to accept actions as well --- src/engine/entity/Entity.spec.ts | 8 ++++---- src/graphqlProtocol/test-helper.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/engine/entity/Entity.spec.ts b/src/engine/entity/Entity.spec.ts index 8d762813..1b42cc85 100644 --- a/src/engine/entity/Entity.spec.ts +++ b/src/engine/entity/Entity.spec.ts @@ -884,10 +884,10 @@ describe('Entity', () => { }); // create mutations to test processors ? - const setup = await generateTestSchema([ - SomeEntityWithPreprocess, - SomeEntityWithPostprocess, - ]); + const setup = await generateTestSchema({ + entities: [SomeEntityWithPreprocess, SomeEntityWithPostprocess], + actions: [], + }); const configuration = setup.configuration; graphqlSchema = generateGraphQLSchema(configuration); }); diff --git a/src/graphqlProtocol/test-helper.ts b/src/graphqlProtocol/test-helper.ts index a91ecbfe..1cc2659a 100644 --- a/src/graphqlProtocol/test-helper.ts +++ b/src/graphqlProtocol/test-helper.ts @@ -24,7 +24,7 @@ import { StorageDataType } from '../engine/storage/StorageDataType'; // context // ); -export const generateTestSchema = async entities => { +export const generateTestSchema = async ({ entities, actions } = {}) => { const testEntity = new Entity({ name: 'TestEntityName', description: 'Just some description', @@ -143,7 +143,7 @@ export const generateTestSchema = async entities => { // defaultStorageType: StorageTypeMemory, defaultActionPermissions: null, permissionsMap: null, - actions: [], + actions: actions || [], entities: entities ? [...entities, testEntity] : [testEntity], }); From d9f45e2318302c9ff7617e5b2a73f1cf015e1f65 Mon Sep 17 00:00:00 2001 From: Chris Kalmar Date: Wed, 25 Mar 2020 19:29:29 +0100 Subject: [PATCH 05/12] implement preProcessor tests for actions --- src/engine/action/Action.spec.js | 86 ++++++++++++++++++- .../action/__snapshots__/Action.spec.js.snap | 26 ++++++ 2 files changed, 109 insertions(+), 3 deletions(-) diff --git a/src/engine/action/Action.spec.js b/src/engine/action/Action.spec.js index fcaf0648..7504e06f 100644 --- a/src/engine/action/Action.spec.js +++ b/src/engine/action/Action.spec.js @@ -1,12 +1,15 @@ /* eslint-disable @typescript-eslint/no-empty-function */ -import { Action, isAction } from './Action'; +import { Action, isAction, ACTION_TYPE_QUERY } from './Action'; import { Permission, isPermission } from '../permission/Permission'; -import { DataTypeString } from '../datatype/dataTypes'; +import { DataTypeString, DataTypeInteger } from '../datatype/dataTypes'; import { buildObjectDataType } from '../datatype/ObjectDataType'; import { passOrThrow } from '../util'; +import { generateTestSchema } from '../../graphqlProtocol/test-helper'; +import { generateGraphQLSchema } from '../../graphqlProtocol/generator'; +import { graphql } from 'graphql'; describe('Action', () => { it('should have a name', () => { @@ -327,7 +330,7 @@ describe('Action', () => { input: {}, output: {}, resolve() {}, - permissions: [ new Permission().authenticated(), new Permission() ], + permissions: [new Permission().authenticated(), new Permission()], }); function fn() { @@ -337,4 +340,81 @@ describe('Action', () => { expect(fn).toThrowErrorMatchingSnapshot(); }); }); + + describe('preProcessor', () => { + it('should have a valid preProcessor function if defined', () => { + function fn() { + // eslint-disable-next-line no-new + new Action({ + name: 'example', + description: 'do something', + resolve() {}, + preProcessor: 'not-a-func', + }); + } + + expect(fn).toThrowErrorMatchingSnapshot(); + }); + + it('should pass through preProcessor if it is declared', async () => { + const setup = await generateTestSchema({ + actions: [ + new Action({ + name: 'SomeActionWithPreProcessor', + type: ACTION_TYPE_QUERY, + description: 'do something', + input: { + type: DataTypeInteger, + }, + output: { + type: buildObjectDataType({ + attributes: { + value: { + type: DataTypeInteger, + description: 'result value', + }, + }, + }), + }, + resolve(source, args) { + return { + value: args, + }; + }, + preProcessor: (action, source, payload) => { + if (payload === 13) { + throw new Error('13 brings bad luck'); + } + }, + }), + ], + }); + + const graphqlSchema = generateGraphQLSchema(setup.configuration); + + const query = ` + query SomeActionWithPreProcessor($number: Int!) { + someActionWithPreProcessor (input: { + data: $number + }) { + result { + value + } + } + } + + + `; + + const result1 = await graphql(graphqlSchema, query, null, null, { + number: 123, + }); + expect(result1).toMatchSnapshot(); + + const result2 = await graphql(graphqlSchema, query, null, null, { + number: 13, + }); + expect(result2).toMatchSnapshot(); + }); + }); }); diff --git a/src/engine/action/__snapshots__/Action.spec.js.snap b/src/engine/action/__snapshots__/Action.spec.js.snap index 8d97bd00..6f0667b3 100644 --- a/src/engine/action/__snapshots__/Action.spec.js.snap +++ b/src/engine/action/__snapshots__/Action.spec.js.snap @@ -4,6 +4,32 @@ exports[`Action isAction should recognize non-Action objects 1`] = `"Not a Actio exports[`Action permissions should throw if empty permissions are provided 1`] = `"Action 'example' has one or more empty permission definitions"`; + +exports[`Action preProcessor should have a valid preProcessor function if defined 1`] = `"preProcessor of of action 'example' needs to be a valid function"`; + +exports[`Action preProcessor should pass through preProcessor if it is declared 1`] = ` +Object { + "data": Object { + "someActionWithPreProcessor": Object { + "result": Object { + "value": 123, + }, + }, + }, +} +`; + +exports[`Action preProcessor should pass through preProcessor if it is declared 2`] = ` +Object { + "data": Object { + "someActionWithPreProcessor": null, + }, + "errors": Array [ + [GraphQLError: 13 brings bad luck], + ], +} +`; + exports[`Action should have a description 1`] = `"Missing description for action 'example'"`; exports[`Action should have a name 1`] = `"Missing action name"`; From 31773ea5cfff6a80712c62836293a25fdeb803b2 Mon Sep 17 00:00:00 2001 From: Chris Kalmar Date: Wed, 25 Mar 2020 19:34:32 +0100 Subject: [PATCH 06/12] implement postProcessor tests for actions --- src/engine/action/Action.spec.js | 77 +++++++++++++++++++ .../action/__snapshots__/Action.spec.js.snap | 25 ++++++ 2 files changed, 102 insertions(+) diff --git a/src/engine/action/Action.spec.js b/src/engine/action/Action.spec.js index 7504e06f..f2be202b 100644 --- a/src/engine/action/Action.spec.js +++ b/src/engine/action/Action.spec.js @@ -417,4 +417,81 @@ describe('Action', () => { expect(result2).toMatchSnapshot(); }); }); + + describe('postProcessor', () => { + it('should have a valid postProcessor function if defined', () => { + function fn() { + // eslint-disable-next-line no-new + new Action({ + name: 'example', + description: 'do something', + resolve() {}, + postProcessor: 'not-a-func', + }); + } + + expect(fn).toThrowErrorMatchingSnapshot(); + }); + + it('should pass through postProcessor if it is declared', async () => { + const setup = await generateTestSchema({ + actions: [ + new Action({ + name: 'SomeActionWithPostProcessor', + type: ACTION_TYPE_QUERY, + description: 'do something', + input: { + type: DataTypeInteger, + }, + output: { + type: buildObjectDataType({ + attributes: { + value: { + type: DataTypeInteger, + description: 'result value', + }, + }, + }), + }, + resolve(source, args) { + return { + value: args, + }; + }, + postProcessor: (error, result, action, source, payload) => { + if (payload > 1000) { + result.value *= 2; + } + }, + }), + ], + }); + + const graphqlSchema = generateGraphQLSchema(setup.configuration); + + const query = ` + query SomeActionWithPostProcessor($number: Int!) { + someActionWithPostProcessor (input: { + data: $number + }) { + result { + value + } + } + } + + + `; + + const result1 = await graphql(graphqlSchema, query, null, null, { + number: 123, + }); + expect(result1).toMatchSnapshot(); + + const result2 = await graphql(graphqlSchema, query, null, null, { + number: 1234, + }); + expect(result2).toMatchSnapshot(); + }); + }); }); diff --git a/src/engine/action/__snapshots__/Action.spec.js.snap b/src/engine/action/__snapshots__/Action.spec.js.snap index 6f0667b3..b4434075 100644 --- a/src/engine/action/__snapshots__/Action.spec.js.snap +++ b/src/engine/action/__snapshots__/Action.spec.js.snap @@ -4,6 +4,31 @@ exports[`Action isAction should recognize non-Action objects 1`] = `"Not a Actio exports[`Action permissions should throw if empty permissions are provided 1`] = `"Action 'example' has one or more empty permission definitions"`; +exports[`Action postProcessor should have a valid postProcessor function if defined 1`] = `"postProcessor of action 'example' needs to be a valid function"`; + +exports[`Action postProcessor should pass through postProcessor if it is declared 1`] = ` +Object { + "data": Object { + "someActionWithPostProcessor": Object { + "result": Object { + "value": 123, + }, + }, + }, +} +`; + +exports[`Action postProcessor should pass through postProcessor if it is declared 2`] = ` +Object { + "data": Object { + "someActionWithPostProcessor": Object { + "result": Object { + "value": 2468, + }, + }, + }, +} +`; exports[`Action preProcessor should have a valid preProcessor function if defined 1`] = `"preProcessor of of action 'example' needs to be a valid function"`; From 4d095cf4d832aed117f2d656121935787e58f7c4 Mon Sep 17 00:00:00 2001 From: Chris Kalmar Date: Wed, 25 Mar 2020 19:38:39 +0100 Subject: [PATCH 07/12] add types for test schema generator --- src/engine/entity/Entity.spec.ts | 1 - src/graphqlProtocol/test-helper.ts | 12 +++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/engine/entity/Entity.spec.ts b/src/engine/entity/Entity.spec.ts index 1b42cc85..352bceec 100644 --- a/src/engine/entity/Entity.spec.ts +++ b/src/engine/entity/Entity.spec.ts @@ -886,7 +886,6 @@ describe('Entity', () => { // create mutations to test processors ? const setup = await generateTestSchema({ entities: [SomeEntityWithPreprocess, SomeEntityWithPostprocess], - actions: [], }); const configuration = setup.configuration; graphqlSchema = generateGraphQLSchema(configuration); diff --git a/src/graphqlProtocol/test-helper.ts b/src/graphqlProtocol/test-helper.ts index 1cc2659a..17cd544f 100644 --- a/src/graphqlProtocol/test-helper.ts +++ b/src/graphqlProtocol/test-helper.ts @@ -10,6 +10,7 @@ import { Entity } from '../engine/entity/Entity'; import { Schema } from '../engine/schema/Schema'; import { StorageType } from '../engine/storage/StorageType'; import { StorageDataType } from '../engine/storage/StorageDataType'; +import { Action } from '..'; // import { StorageTypeMemory } from '../memory-connector/StorageTypeMemory'; // const { @@ -24,7 +25,16 @@ import { StorageDataType } from '../engine/storage/StorageDataType'; // context // ); -export const generateTestSchema = async ({ entities, actions } = {}) => { +type GenerateTestSchemaSetup = { + entities?: Entity[]; + actions?: Action[]; +}; + +export const generateTestSchema = async ( + setup: GenerateTestSchemaSetup = {}, +) => { + const { entities, actions } = setup; + const testEntity = new Entity({ name: 'TestEntityName', description: 'Just some description', From b89599aefac3fd15e51d4d39ced44972a01fefbf Mon Sep 17 00:00:00 2001 From: Chris Kalmar Date: Wed, 25 Mar 2020 19:42:39 +0100 Subject: [PATCH 08/12] v0.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e8d8ecc3..3d534cd1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shyft", - "version": "0.5.4", + "version": "0.6.0", "description": "Model driven GraphQL API framework", "main": "lib/index.js", "types": "lib/index.d.ts", From 767c941772a7c763510e89b97053bf166b9b51cd Mon Sep 17 00:00:00 2001 From: Chris Kalmar Date: Thu, 26 Mar 2020 12:06:52 +0100 Subject: [PATCH 09/12] use default value function in attributes regardless of 'requried' flag --- src/graphqlProtocol/action.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphqlProtocol/action.ts b/src/graphqlProtocol/action.ts index 25d837dd..b428a1e8 100644 --- a/src/graphqlProtocol/action.ts +++ b/src/graphqlProtocol/action.ts @@ -26,7 +26,7 @@ const fillSingleDefaultValues = async (param, payload, context) => { let ret = payload; if (typeof payload === 'undefined') { - if (param.required && param.defaultValue) { + if (param.defaultValue) { ret = param.defaultValue({}, context); } } From 5558a2fc5b10f0bbc7b37b08f2e5c5129efd1e21 Mon Sep 17 00:00:00 2001 From: Chris Kalmar Date: Thu, 26 Mar 2020 12:07:22 +0100 Subject: [PATCH 10/12] fix default values assignment in actions --- src/graphqlProtocol/action.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphqlProtocol/action.ts b/src/graphqlProtocol/action.ts index b428a1e8..c4dcc530 100644 --- a/src/graphqlProtocol/action.ts +++ b/src/graphqlProtocol/action.ts @@ -197,7 +197,7 @@ export const generateActions = (graphRegistry, actionTypeFilter) => { payload = args.input.data; clientMutationId = args.input.clientMutationId; - args.input.data = await fillDefaultValues(input, payload, context); + payload = await fillDefaultValues(input, payload, context); await validateActionPayload(input, payload, action, context); } From 3ff0a2d94e91e5cd8b27ce351e7e046b756d8db2 Mon Sep 17 00:00:00 2001 From: Chris Kalmar Date: Thu, 26 Mar 2020 12:07:56 +0100 Subject: [PATCH 11/12] add defaultValue tests for action inputs --- src/engine/action/Action.spec.js | 116 ++++++++++++++++++ .../action/__snapshots__/Action.spec.js.snap | 24 ++++ 2 files changed, 140 insertions(+) diff --git a/src/engine/action/Action.spec.js b/src/engine/action/Action.spec.js index f2be202b..1ed539cb 100644 --- a/src/engine/action/Action.spec.js +++ b/src/engine/action/Action.spec.js @@ -494,4 +494,120 @@ describe('Action', () => { expect(result2).toMatchSnapshot(); }); }); + + describe('defaultValue', () => { + it('should fill in provided default value', async () => { + const setup = await generateTestSchema({ + actions: [ + new Action({ + name: 'SomeAction', + type: ACTION_TYPE_QUERY, + description: 'do something', + input: { + type: DataTypeInteger, + description: 'just a number', + defaultValue: () => { + return 2000; + }, + }, + output: { + type: buildObjectDataType({ + attributes: { + value: { + type: DataTypeInteger, + description: 'result value', + }, + }, + }), + }, + resolve(source, args) { + return { + value: args, + }; + }, + }), + ], + }); + + const graphqlSchema = generateGraphQLSchema(setup.configuration); + + const query = ` + query SomeAction($number: Int) { + someAction (input: { + data: $number + }) { + result { + value + } + } + } + + + `; + + const result = await graphql(graphqlSchema, query, null, null, {}); + expect(result).toMatchSnapshot(); + }); + + it('should fill in provided default values in nested input objects', async () => { + const setup = await generateTestSchema({ + actions: [ + new Action({ + name: 'SomeAction', + type: ACTION_TYPE_QUERY, + description: 'do something', + input: { + type: buildObjectDataType({ + attributes: { + number: { + type: DataTypeInteger, + description: 'just a number', + defaultValue: () => { + return 2000; + }, + }, + }, + }), + }, + output: { + type: buildObjectDataType({ + attributes: { + value: { + type: DataTypeInteger, + description: 'result value', + }, + }, + }), + }, + resolve(source, args) { + return { + value: args.number, + }; + }, + }), + ], + }); + + const graphqlSchema = generateGraphQLSchema(setup.configuration); + + const query = ` + query SomeAction($number: Int) { + someAction (input: { + data: { + number: $number + } + }) { + result { + value + } + } + } + + + `; + + const result = await graphql(graphqlSchema, query, null, null, {}); + expect(result).toMatchSnapshot(); + }); + }); }); diff --git a/src/engine/action/__snapshots__/Action.spec.js.snap b/src/engine/action/__snapshots__/Action.spec.js.snap index b4434075..c0a540ab 100644 --- a/src/engine/action/__snapshots__/Action.spec.js.snap +++ b/src/engine/action/__snapshots__/Action.spec.js.snap @@ -1,5 +1,29 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Action defaultValue should fill in provided default value 1`] = ` +Object { + "data": Object { + "someAction": Object { + "result": Object { + "value": 2000, + }, + }, + }, +} +`; + +exports[`Action defaultValue should fill in provided default values in nested input objects 1`] = ` +Object { + "data": Object { + "someAction": Object { + "result": Object { + "value": 2000, + }, + }, + }, +} +`; + exports[`Action isAction should recognize non-Action objects 1`] = `"Not a Action object"`; exports[`Action permissions should throw if empty permissions are provided 1`] = `"Action 'example' has one or more empty permission definitions"`; From b067ee4bf0120ca7f7a0bfa5ba8f97dc6495e923 Mon Sep 17 00:00:00 2001 From: Chris Kalmar Date: Thu, 26 Mar 2020 12:27:18 +0100 Subject: [PATCH 12/12] v0.6.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3d534cd1..c71546b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shyft", - "version": "0.6.0", + "version": "0.6.1", "description": "Model driven GraphQL API framework", "main": "lib/index.js", "types": "lib/index.d.ts",