diff --git a/src/engine/subscription/Subscription.spec.ts b/src/engine/subscription/Subscription.spec.ts index 2edd3d48..4f48e124 100644 --- a/src/engine/subscription/Subscription.spec.ts +++ b/src/engine/subscription/Subscription.spec.ts @@ -8,12 +8,16 @@ import { SUBSCRIPTION_TYPE_UPDATE, SUBSCRIPTION_TYPE_DELETE, processEntitySubscriptions, + pubsub, } from './Subscription'; import { Entity } from '../entity/Entity'; import { DataTypeString } from '../datatype/dataTypes'; import { passOrThrow } from '../util'; +import { generateTestSchema } from '../../graphqlProtocol/test-helper'; +import { generateGraphQLSchema } from '../../graphqlProtocol/generator'; +import { subscribe, parse } from 'graphql'; -describe('Mutation', () => { +describe('Subscription', () => { const entity = new Entity({ name: 'SomeEntityName', description: 'Just some description', @@ -92,7 +96,7 @@ describe('Mutation', () => { const subscription = new Subscription({ name: 'example', type: SUBSCRIPTION_TYPE_CREATE, - description: 'mutate the world', + description: 'subscribe the world', attributes: ['anything', { foo: 'bar' }], }); @@ -107,7 +111,7 @@ describe('Mutation', () => { const subscription = new Subscription({ name: 'example', type: SUBSCRIPTION_TYPE_UPDATE, - description: 'mutate the world', + description: 'subscribe the world', attributes: [], }); @@ -119,7 +123,7 @@ describe('Mutation', () => { const subscription = new Subscription({ name: 'example', type: SUBSCRIPTION_TYPE_DELETE, - description: 'mutate the world', + description: 'subscribe the world', attributes: [], }); @@ -154,35 +158,35 @@ describe('Mutation', () => { expect(String(subscription)).toBe('example'); }); - // it('should have a valid preProcessor function', () => { - // function fn() { - // // eslint-disable-next-line no-new - // new Subscription({ - // name: 'example', - // type: SUBSCRIPTION_TYPE_CREATE, - // description: 'mutate the world', - // attributes: ['anything'], - // preProcessor: 'not-a-function', - // }); - // } - - // expect(fn).toThrowErrorMatchingSnapshot(); - // }); - - // it('should have a valid postProcessor function', () => { - // function fn() { - // // eslint-disable-next-line no-new - // new Subscription({ - // name: 'example', - // type: SUBSCRIPTION_TYPE_CREATE, - // description: 'mutate the world', - // attributes: ['anything'], - // postProcessor: 'not-a-function', - // }); - // } - - // expect(fn).toThrowErrorMatchingSnapshot(); - // }); + it('should have a valid preProcessor function', () => { + function fn() { + // eslint-disable-next-line no-new + new Subscription({ + name: 'example', + type: SUBSCRIPTION_TYPE_CREATE, + description: 'mutate the world', + attributes: ['anything'], + preProcessor: 'not-a-function', + }); + } + + expect(fn).toThrowErrorMatchingSnapshot(); + }); + + it('should have a valid postProcessor function', () => { + function fn() { + // eslint-disable-next-line no-new + new Subscription({ + name: 'example', + type: SUBSCRIPTION_TYPE_CREATE, + description: 'mutate the world', + attributes: ['anything'], + postProcessor: 'not-a-function', + }); + } + + expect(fn).toThrowErrorMatchingSnapshot(); + }); describe('isSubscription', () => { const subscription = new Subscription({ @@ -190,8 +194,12 @@ describe('Mutation', () => { type: SUBSCRIPTION_TYPE_UPDATE, description: 'mutate the world', attributes: ['anything'], - // preProcessor() {}, - // postProcessor() {}, + preProcessor() { + return {}; + }, + postProcessor() { + return {}; + }, }); it('should recognize objects of type Subscription', () => { @@ -220,27 +228,6 @@ describe('Mutation', () => { }); describe('processEntitySubscriptions', () => { - // const subscriptionTypeCreateDefinition = { - // type: SUBSCRIPTION_TYPE_CREATE, - // name: 'build', - // description: 'on built item', - // attributes: ['someAttribute'], - // }; - - // const subscriptionTypeUpdateDefinition = { - // type: SUBSCRIPTION_TYPE_UPDATE, - // name: 'change', - // description: 'on changed item', - // attributes: ['id', 'someAttribute'], - // }; - - // const subscriptionTypeDeleteDefinition = { - // type: SUBSCRIPTION_TYPE_DELETE, - // name: 'drop', - // description: 'on dropped item', - // attributes: ['id'], - // }; - it('should throw if provided with an invalid list of subscriptions', () => { const subscriptions = { foo: [{}], @@ -263,7 +250,7 @@ describe('Mutation', () => { expect(fn).toThrowErrorMatchingSnapshot(); }); - it('should throw if required attribute (without defaultValue) is missing in CREATE type subscriptions', () => { + it('should throw if required attribute is missing in CREATE type subscriptions', () => { function fn() { const otherEntity = new Entity({ name: 'SomeEntityName', @@ -282,14 +269,14 @@ describe('Mutation', () => { subscriptions: [ new Subscription({ type: SUBSCRIPTION_TYPE_CREATE, - name: 'build', + name: 'onBuild', description: 'build item', attributes: ['someAttribute'], }), ], }); - otherEntity.getMutationByName('build'); + otherEntity.getSubscriptionByName('onBuild'); } expect(fn).toThrowErrorMatchingSnapshot(); @@ -299,13 +286,13 @@ describe('Mutation', () => { const subscriptions = [ new Subscription({ type: SUBSCRIPTION_TYPE_CREATE, - name: 'build', + name: 'onBuild', description: 'build item', attributes: ['someAttribute'], }), new Subscription({ type: SUBSCRIPTION_TYPE_DELETE, - name: 'build', + name: 'onBuild', description: 'build item', attributes: ['someAttribute'], }), @@ -322,7 +309,7 @@ describe('Mutation', () => { const subscriptions = [ new Subscription({ type: SUBSCRIPTION_TYPE_CREATE, - name: 'build', + name: 'onBuild', description: 'build item', attributes: ['doesNotExist'], }), @@ -339,7 +326,7 @@ describe('Mutation', () => { const subscriptions = [ new Subscription({ type: SUBSCRIPTION_TYPE_DELETE, - name: 'drop', + name: 'onDrop', description: 'drop item', attributes: [], }), @@ -348,4 +335,94 @@ describe('Mutation', () => { processEntitySubscriptions(entity, subscriptions); }); }); + + describe('preProcessor', () => { + it('should pass through preProcessor if it is declared', async () => { + const testEntity = new Entity({ + name: 'SomeTestsEntityName', + description: 'Just some description', + attributes: { + someAttribute: { + type: DataTypeString, + description: 'Just some description', + required: true, + }, + anotherAttribute: { + type: DataTypeString, + description: 'Just some description', + }, + }, + subscriptions: [ + new Subscription({ + name: 'SomeSubWithPreProcessor', + type: SUBSCRIPTION_TYPE_CREATE, + description: 'build item', + attributes: ['someAttribute'], + delimiter: '/', + wildCard: '', + pattern: '', + // pattern: 'someAttribute/anotherAttribute', + preProcessor: (_entity, _source, payload) => { + if (payload === 13) { + throw new Error('13 brings bad luck'); + } + return payload; + }, + }), + ], + }); + + const subscriptionByName = testEntity.getSubscriptionByName( + 'SomeSubWithPreProcessor', + ); + + expect(subscriptionByName).toMatchSnapshot(); + + const setup = await generateTestSchema({ entities: [testEntity] }); + + const graphqlSchema = generateGraphQLSchema(setup.configuration); + + const subscriptionDoc = parse(` + subscription someSubWithPreProcessorSomeTestsEntityName($input: SomeSubWithPreProcessorSomeTestsEntityNameInput!) { + someSubWithPreProcessorSomeTestsEntityName(input: $input) { + someTestsEntityName { + someAttribute + } + } + }`); + + const subscription = (await subscribe({ + schema: graphqlSchema, + document: subscriptionDoc, + variableValues: { + input: { + someTestsEntityName: { + someAttribute: 'test', + }, + }, + }, + contextValue: { pubsub }, + })) as AsyncIterableIterator; + + // if (subscription.errors) { + // console.log({ errors: subscription.errors }); + // } + + const pending = subscription.next(); + + await pubsub.publish('SomeSubWithPreProcessorSomeTestsEntityName', { + // someTestsEntityName: { + someAttribute: 'hello', + anotherAttribute: 'world', + // }, + }); + + const result = await pending; + // console.log('should pass through preProcessor if it is declared', result); + + expect(result).toMatchSnapshot(); + + expect(await subscription.return()).toMatchSnapshot(); + }); + }); }); diff --git a/src/engine/subscription/__snapshots__/Subscription.spec.ts.snap b/src/engine/subscription/__snapshots__/Subscription.spec.ts.snap index 7f297de3..a37992fb 100644 --- a/src/engine/subscription/__snapshots__/Subscription.spec.ts.snap +++ b/src/engine/subscription/__snapshots__/Subscription.spec.ts.snap @@ -1,25 +1,65 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Mutation isSubscription should recognize non-Subscription objects 1`] = `"Not a Subscription object"`; +exports[`Subscription isSubscription should recognize non-Subscription objects 1`] = `"Not a Subscription object"`; -exports[`Mutation processEntitySubscriptions should throw if provided with an invalid list of subscriptions 1`] = `"Entity 'SomeEntityName' subscriptions definition needs to be an array of subscriptions"`; +exports[`Subscription preProcessor should pass through preProcessor if it is declared 1`] = ` +Subscription { + "attributes": Array [ + "someAttribute", + ], + "delimiter": "/", + "description": "build item", + "isTypeCreate": true, + "name": "SomeSubWithPreProcessor", + "preProcessor": [Function], + "type": "onCreate", +} +`; -exports[`Mutation processEntitySubscriptions should throw if provided with an invalid subscription 1`] = `"Invalid subscription definition for entity 'SomeEntityName' at position '0'"`; +exports[`Subscription preProcessor should pass through preProcessor if it is declared 2`] = ` +Object { + "done": false, + "value": Object { + "data": Object { + "someSubWithPreProcessorSomeTestsEntityName": Object { + "someTestsEntityName": Object { + "someAttribute": "hello", + }, + }, + }, + }, +} +`; -exports[`Mutation processEntitySubscriptions should throw if required attribute (without defaultValue) is missing in CREATE type subscriptions 1`] = `"Invalid setup property 'subscriptions' in entity 'SomeEntityName'"`; +exports[`Subscription preProcessor should pass through preProcessor if it is declared 3`] = ` +Object { + "done": true, + "value": undefined, +} +`; -exports[`Mutation processEntitySubscriptions should throw if unknown attributes are used 1`] = `"Cannot use attribute 'SomeEntityName.doesNotExist' in subscription 'SomeEntityName.build' as it does not exist"`; +exports[`Subscription processEntitySubscriptions should throw if provided with an invalid list of subscriptions 1`] = `"Entity 'SomeEntityName' subscriptions definition needs to be an array of subscriptions"`; -exports[`Mutation processEntitySubscriptions should throw on duplicate subscription names 1`] = `"Duplicate subscription name 'build' found in 'SomeEntityName'"`; +exports[`Subscription processEntitySubscriptions should throw if provided with an invalid subscription 1`] = `"Invalid subscription definition for entity 'SomeEntityName' at position '0'"`; -exports[`Mutation should have a description 1`] = `"Missing description for subscription 'example'"`; +exports[`Subscription processEntitySubscriptions should throw if required attribute is missing in CREATE type subscriptions 1`] = `"Missing required attributes in subscription 'SomeEntityName.onBuild' need to have a defaultValue() function: [ neededAttribute ]"`; -exports[`Mutation should have a list of unique attribute names 1`] = `"Subscription 'SomeEntityName.example' needs to have a list of unique attribute names"`; +exports[`Subscription processEntitySubscriptions should throw if unknown attributes are used 1`] = `"Cannot use attribute 'SomeEntityName.doesNotExist' in subscription 'SomeEntityName.onBuild' as it does not exist"`; -exports[`Mutation should have a list of valid attribute names 1`] = `"Subscription 'SomeEntityName.example' needs to have a list of attribute names"`; +exports[`Subscription processEntitySubscriptions should throw on duplicate subscription names 1`] = `"Duplicate subscription name 'onBuild' found in 'SomeEntityName'"`; -exports[`Mutation should have a name 1`] = `"Missing subscription name"`; +exports[`Subscription should have a description 1`] = `"Missing description for subscription 'example'"`; -exports[`Mutation should have a type 1`] = `"Missing type for subscription 'example'"`; +exports[`Subscription should have a list of unique attribute names 1`] = `"Subscription 'SomeEntityName.example' needs to have a list of unique attribute names"`; -exports[`Mutation should have a valid type 1`] = `"Unknown subscription type '12346' used, try one of these: 'onCreate, onUpdate, onDelete'"`; +exports[`Subscription should have a list of valid attribute names 1`] = `"Subscription 'SomeEntityName.example' needs to have a list of attribute names"`; + +exports[`Subscription should have a name 1`] = `"Missing subscription name"`; + +exports[`Subscription should have a type 1`] = `"Missing type for subscription 'example'"`; + +exports[`Subscription should have a valid postProcessor function 1`] = `"postProcessor of subscription 'example' needs to be a valid function"`; + +exports[`Subscription should have a valid preProcessor function 1`] = `"preProcessor of subscription 'example' needs to be a valid function"`; + +exports[`Subscription should have a valid type 1`] = `"Unknown subscription type '12346' used, try one of these: 'onCreate, onUpdate, onDelete'"`; diff --git a/src/graphqlProtocol/generator.ts b/src/graphqlProtocol/generator.ts index 61950c19..9a6fe5d2 100644 --- a/src/graphqlProtocol/generator.ts +++ b/src/graphqlProtocol/generator.ts @@ -374,7 +374,6 @@ export const generateGraphQLSchema = configuration => { fields: () => { const mutations = generateMutations(graphRegistry); const actions = generateActions(graphRegistry, ACTION_TYPE_MUTATION); - // console.log('generate mutations', { mutations }); return { ...mutations, @@ -389,6 +388,13 @@ export const generateGraphQLSchema = configuration => { fields: () => { const subscriptions = generateSubscriptions(graphRegistry); + // Object.keys(subscriptions).forEach(sub => { + // console.log('generate subscriptions', { + // name: sub, + // args: subscriptions[sub].args, + // }); + // }); + return subscriptions; }, }); diff --git a/src/graphqlProtocol/resolver.ts b/src/graphqlProtocol/resolver.ts index e08abaad..e308e6ff 100644 --- a/src/graphqlProtocol/resolver.ts +++ b/src/graphqlProtocol/resolver.ts @@ -1,10 +1,10 @@ +import * as _ from 'lodash'; import { addRelayTypePromoterToList, addRelayTypePromoterToInstanceFn, translateList, translateInstanceFn, } from './util'; - import { ProtocolGraphQL } from './ProtocolGraphQL'; import { ProtocolGraphQLConfiguration } from './ProtocolGraphQLConfiguration'; @@ -16,8 +16,6 @@ import { import { transformFilterLevel } from './filter'; -import * as _ from 'lodash'; - import { addRelayTypePromoterToInstance, translateInstance } from './util'; import { @@ -34,6 +32,7 @@ import { SUBSCRIPTION_TYPE_CREATE, SUBSCRIPTION_TYPE_UPDATE, // SUBSCRIPTION_TYPE_DELETE, + pubsub, } from '../engine/subscription/Subscription'; import { CustomError } from '../engine/CustomError'; import { @@ -475,7 +474,12 @@ export const getSubscriptionResolver = ( storageType, ); - return async (source, args, context, info) => { + return async ( + source, + args, + context, + info, + ): Promise> => { checkRequiredI18nInputs( entity, entitySubscription, @@ -539,20 +543,35 @@ export const getSubscriptionResolver = ( // ); // } - const delimiter = entitySubscription.delimiter || '/'; + let topic; + if (entitySubscription.pattern) { + const params = entitySubscription.pattern + .split(entitySubscription.delimiter) + .reduce((acc, curr) => (acc[curr] = args.input[typeName][curr]), {}); + console.log('getSubscriptionResolver', { params }); - const params = entitySubscription.pattern - .split(delimiter) - .reduce((acc, curr) => (acc[curr] = args.input[typeName][curr]), {}); + const filled = Object.values(params).join(entitySubscription.delimiter); - const filled = Object.values(params).join(entitySubscription.delimiter); + console.log('getSubscriptionResolver', { filled }); - const topic = `${entitySubscription.name}/${filled}${ - entitySubscription ? delimiter + entitySubscription.wildCard : '' - }`; + topic = `${entitySubscription.name}${entity.name}/${filled}${ + entitySubscription.wildCard + ? entitySubscription.delimiter + entitySubscription.wildCard + : '' + }`; + } else { + topic = `${entitySubscription.name}${entity.name}${ + entitySubscription.wildCard + ? entitySubscription.delimiter + entitySubscription.wildCard + : '' + }`; + } + + // console.log('getSubscriptionResolver', { topic }); - return context.pubsub ? context.pubsub.asyncIterator(topic) : null; - // : pubsub.asyncIterator(topic); + return context.pubsub + ? context.pubsub.asyncIterator(topic) + : pubsub.asyncIterator(topic); }; }; @@ -562,9 +581,11 @@ export const getSubscriptionPayloadResolver = ( typeName, ) => { return async (source, args, context, info) => { - let ret = { - clientSubscriptionId: args.input.clientSubscriptionId, - }; + // let ret = { + // clientSubscriptionId: args.input.clientSubscriptionId, + // }; + + let ret = {}; let result; if (entitySubscription.postProcessor) { @@ -591,6 +612,8 @@ export const getSubscriptionPayloadResolver = ( ret[typeName] = result; } + // console.log('getSubscriptionPayloadResolver', { ret }); + return ret; }; };