Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(appsync): add support for subscriptions for code-first schema generation #10078

Merged
merged 27 commits into from
Sep 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
de064ac
prelim addition of subscriptions
BryanPan342 Aug 31, 2020
f3838ef
first pass changes to readme
BryanPan342 Aug 31, 2020
f3c77e3
refactor(appsync): graphQLApi to graphqlApi for better snakecasing
BryanPan342 Aug 31, 2020
51edc01
Revert "refactor(appsync): graphQLApi to graphqlApi for better snakec…
BryanPan342 Aug 31, 2020
884e185
Merge remote-tracking branch 'upstream/master'
BryanPan342 Aug 31, 2020
9ad9d36
Merge remote-tracking branch 'upstream/master'
BryanPan342 Sep 1, 2020
d9ebafb
Merge remote-tracking branch 'upstream/master'
BryanPan342 Sep 1, 2020
64db87e
Merge remote-tracking branch 'upstream/master'
BryanPan342 Sep 1, 2020
b3622b0
Merge remote-tracking branch 'upstream/master'
BryanPan342 Sep 1, 2020
4cdd244
Merge remote-tracking branch 'upstream/master'
BryanPan342 Sep 2, 2020
6608704
Merge remote-tracking branch 'upstream/master'
BryanPan342 Sep 3, 2020
f85ce81
Merge remote-tracking branch 'upstream/master'
BryanPan342 Sep 4, 2020
d6c4d1e
Merge branch 'master' into subscription
BryanPan342 Sep 4, 2020
4f32d9d
integ changes
BryanPan342 Sep 4, 2020
69eb74b
add directives for subscriptions
BryanPan342 Sep 4, 2020
2b6d915
update readme with directive
BryanPan342 Sep 4, 2020
e8cfc0c
Merge remote-tracking branch 'upstream/master'
BryanPan342 Sep 8, 2020
1423bc6
Merge remote-tracking branch 'upstream/master'
BryanPan342 Sep 8, 2020
2ea72ed
Merge remote-tracking branch 'upstream/master'
BryanPan342 Sep 8, 2020
5b3e44d
edit readme
BryanPan342 Sep 9, 2020
322d9a1
Merge remote-tracking branch 'upstream/master'
BryanPan342 Sep 9, 2020
0f1b683
Merge remote-tracking branch 'upstream/master'
BryanPan342 Sep 10, 2020
7495b0f
address suggestions
BryanPan342 Sep 10, 2020
8b087ef
Merge branch 'master' into subscription
BryanPan342 Sep 10, 2020
d1f4956
Merge remote-tracking branch 'upstream/master'
BryanPan342 Sep 11, 2020
056df67
Merge remote-tracking branch 'upstream/master'
BryanPan342 Sep 11, 2020
f5bced6
Merge branch 'master' into subscription
BryanPan342 Sep 11, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions packages/@aws-cdk/aws-appsync/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -779,3 +779,25 @@ api.addMutation('addFilm', new appsync.ResolvableField({
```

To learn more about top level operations, check out the docs [here](https://docs.aws.amazon.com/appsync/latest/devguide/graphql-overview.html).

#### Subscription

Every schema **can** have a top level Subscription type. The top level `Subscription` Type
BryanPan342 marked this conversation as resolved.
Show resolved Hide resolved
is the only exposed type that users can access to invoke a response to a mutation. `Subscriptions`
notify users when a mutation specific mutation is called. This means you can make any data source
real time by specify a GraphQL Schema directive on a mutation.

**Note**: The AWS AppSync client SDK automatically handles subscription connection management.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this something users need to consider when using subscriptions? it's not something they can affect in through their CDK app right?

Copy link
Contributor Author

@BryanPan342 BryanPan342 Sep 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think its nice to have something here because subscriptions are unique to AppSync. Thus for a new person scanning over the README, it would be nice to get a gloss of how that would work.


To add fields for these subscriptions, we can simply run the `addSubscription` function to add
to the schema's `Subscription` type.

```ts
api.addSubscription('addedFilm', new appsync.ResolvableField({
returnType: film.attribute(),
args: { id: appsync.GraphqlType.id({ isRequired: true }) },
directive: [appsync.Directive.subscribe('addFilm')],
}));
```

To learn more about top level operations, check out the docs [here](https://docs.aws.amazon.com/appsync/latest/devguide/real-time-data.html).
23 changes: 19 additions & 4 deletions packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -594,8 +594,8 @@ export class GraphqlApi extends GraphqlApiBase {
}

/**
* Add a query field to the schema's Query. If one isn't set by
* the user, CDK will create an Object Type called 'Query'. For example,
* Add a query field to the schema's Query. CDK will create an
* Object Type called 'Query'. For example,
*
* type Query {
* fieldName: Field.returnType
Expand All @@ -609,8 +609,8 @@ export class GraphqlApi extends GraphqlApiBase {
}

/**
* Add a mutation field to the schema's Mutation. If one isn't set by
* the user, CDK will create an Object Type called 'Mutation'. For example,
* Add a mutation field to the schema's Mutation. CDK will create an
* Object Type called 'Mutation'. For example,
*
* type Mutation {
* fieldName: Field.returnType
Expand All @@ -622,4 +622,19 @@ export class GraphqlApi extends GraphqlApiBase {
public addMutation(fieldName: string, field: ResolvableField): ObjectType {
return this.schema.addMutation(fieldName, field);
}

/**
* Add a subscription field to the schema's Subscription. CDK will create an
* Object Type called 'Subscription'. For example,
*
* type Subscription {
* fieldName: Field.returnType
* }
*
* @param fieldName the name of the Subscription
* @param field the resolvable field to for this Subscription
*/
public addSubscription(fieldName: string, field: ResolvableField): ObjectType {
return this.schema.addSubscription(fieldName, field);
}
}
59 changes: 50 additions & 9 deletions packages/@aws-cdk/aws-appsync/lib/schema-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,18 @@ export interface IIntermediateType {
addField(options: AddFieldOptions): void;
}

interface DirectiveOptions {
/**
* The authorization type of this directive
*/
readonly mode?: AuthorizationType;

/**
* Mutation fields for a subscription directive
*/
readonly mutationFields?: string[];
}

/**
* Directives for types
*
Expand All @@ -181,21 +193,21 @@ export class Directive {
* Add the @aws_iam directive
*/
public static iam(): Directive {
return new Directive('@aws_iam', AuthorizationType.IAM);
return new Directive('@aws_iam', { mode: AuthorizationType.IAM });
}

/**
* Add the @aws_oidc directive
*/
public static oidc(): Directive {
return new Directive('@aws_oidc', AuthorizationType.OIDC);
return new Directive('@aws_oidc', { mode: AuthorizationType.OIDC });
}

/**
* Add the @aws_api_key directive
*/
public static apiKey(): Directive {
return new Directive('@aws_api_key', AuthorizationType.API_KEY);
return new Directive('@aws_api_key', { mode: AuthorizationType.API_KEY });
}

/**
Expand All @@ -209,9 +221,25 @@ export class Directive {
}
// this function creates the cognito groups as a string (i.e. ["group1", "group2", "group3"])
const stringify = (array: string[]): string => {
return array.reduce((acc, element) => `${acc}"${element}", `, '[').slice(0, -2) + ']';
return array.reduce((acc, element) => `${acc}"${element}", `, '').slice(0, -2);
};
return new Directive(`@aws_auth(cognito_groups: [${stringify(groups)}])`, { mode: AuthorizationType.USER_POOL });
}

/**
* Add the @aws_subscribe directive. Only use for top level Subscription type.
*
* @param mutations the mutation fields to link to
*/
public static subscribe(...mutations: string[]): Directive {
if (mutations.length === 0) {
throw new Error(`Subscribe directive requires at least one mutation field to be supplied. Received: ${mutations.length}`);
}
// this function creates the subscribe directive as a string (i.e. ["mutation_field_1", "mutation_field_2"])
const stringify = (array: string[]): string => {
return array.reduce((acc, mutation) => `${acc}"${mutation}", `, '').slice(0, -2);
};
return new Directive(`@aws_auth(cognito_groups: ${stringify(groups)})`, AuthorizationType.USER_POOL);
return new Directive(`@aws_subscribe(mutations: [${stringify(mutations)}])`, { mutationFields: mutations });
}

/**
Expand All @@ -223,6 +251,20 @@ export class Directive {
return new Directive(statement);
}

/**
* The authorization type of this directive
*
* @default - not an authorization directive
*/
public readonly mode?: AuthorizationType;

/**
* Mutation fields for a subscription directive
*
* @default - not a subscription directive
*/
public readonly mutationFields?: string[];

/**
* the directive statement
*/
Expand All @@ -233,11 +275,10 @@ export class Directive {
*/
protected modes?: AuthorizationType[];

private readonly mode?: AuthorizationType;

private constructor(statement: string, mode?: AuthorizationType) {
private constructor(statement: string, options?: DirectiveOptions) {
this.statement = statement;
this.mode = mode;
this.mode = options?.mode;
this.mutationFields = options?.mutationFields;
}

/**
Expand Down
39 changes: 33 additions & 6 deletions packages/@aws-cdk/aws-appsync/lib/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ export class Schema {
}

/**
* Add a query field to the schema's Query. If one isn't set by
* the user, CDK will create an Object Type called 'Query'. For example,
* Add a query field to the schema's Query. CDK will create an
* Object Type called 'Query'. For example,
*
* type Query {
* fieldName: Field.returnType
Expand All @@ -121,7 +121,7 @@ export class Schema {
*/
public addQuery(fieldName: string, field: ResolvableField): ObjectType {
if (this.mode !== SchemaMode.CODE) {
throw new Error(`Unable to add query. Schema definition mode must be ${SchemaMode.CODE} Received: ${this.mode}`);
throw new Error(`Unable to add query. Schema definition mode must be ${SchemaMode.CODE}. Received: ${this.mode}`);
}
if (!this.query) {
this.query = new ObjectType('Query', { definition: {} });
Expand All @@ -132,8 +132,8 @@ export class Schema {
}

/**
* Add a mutation field to the schema's Mutation. If one isn't set by
* the user, CDK will create an Object Type called 'Mutation'. For example,
* Add a mutation field to the schema's Mutation. CDK will create an
* Object Type called 'Mutation'. For example,
*
* type Mutation {
* fieldName: Field.returnType
Expand All @@ -144,7 +144,7 @@ export class Schema {
*/
public addMutation(fieldName: string, field: ResolvableField): ObjectType {
if (this.mode !== SchemaMode.CODE) {
throw new Error(`Unable to add mutation. Schema definition mode must be ${SchemaMode.CODE} Received: ${this.mode}`);
throw new Error(`Unable to add mutation. Schema definition mode must be ${SchemaMode.CODE}. Received: ${this.mode}`);
}
if (!this.mutation) {
this.mutation = new ObjectType('Mutation', { definition: {} });
Expand All @@ -154,6 +154,33 @@ export class Schema {
return this.mutation;
}

/**
* Add a subscription field to the schema's Subscription. CDK will create an
* Object Type called 'Subscription'. For example,
*
* type Subscription {
* fieldName: Field.returnType
* }
*
* @param fieldName the name of the Subscription
* @param field the resolvable field to for this Subscription
*/
public addSubscription(fieldName: string, field: ResolvableField): ObjectType {
if (this.mode !== SchemaMode.CODE) {
throw new Error(`Unable to add subscription. Schema definition mode must be ${SchemaMode.CODE}. Received: ${this.mode}`);
}
if (!this.subscription) {
this.subscription = new ObjectType('Subscription', { definition: {} });
this.addType(this.subscription);
}
const directives = field.fieldOptions?.directives?.filter((directive) => directive.mutationFields);
if (directives && directives.length > 1) {
throw new Error(`Subscription fields must not have more than one @aws_subscribe directives. Received: ${directives.length}`);
}
this.subscription.addField({ fieldName, field });
return this.subscription;
}

/**
* Add type to the schema
*
Expand Down
67 changes: 65 additions & 2 deletions packages/@aws-cdk/aws-appsync/test/appsync-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,56 @@ describe('basic testing schema definition mode `code`', () => {
Definition: 'schema {\n mutation: Mutation\n}\ntype Mutation {\n test: String\n}\n',
});
});

test('definition mode `code` allows for api to addSubscription', () => {
// WHEN
const api = new appsync.GraphqlApi(stack, 'API', {
name: 'demo',
});
api.addSubscription('test', new appsync.ResolvableField({
returnType: t.string,
}));

// THEN
expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', {
Definition: 'schema {\n subscription: Subscription\n}\ntype Subscription {\n test: String\n}\n',
});
});

test('definition mode `code` allows for schema to addSubscription', () => {
// WHEN
const schema = new appsync.Schema();
new appsync.GraphqlApi(stack, 'API', {
name: 'demo',
schema,
});
schema.addSubscription('test', new appsync.ResolvableField({
returnType: t.string,
}));

// THEN
expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', {
Definition: 'schema {\n subscription: Subscription\n}\ntype Subscription {\n test: String\n}\n',
});
});

test('definition mode `code` addSubscription w/ @aws_subscribe', () => {
// WHE
const api = new appsync.GraphqlApi(stack, 'API', {
name: 'demo',
});
api.addSubscription('test', new appsync.ResolvableField({
returnType: t.string,
directives: [appsync.Directive.subscribe('test1')],
}));

const out = 'schema {\n subscription: Subscription\n}\ntype Subscription {\n test: String\n @aws_subscribe(mutations: ["test1"])\n}\n';

// THEN
expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', {
Definition: out,
});
});
});

describe('testing schema definition mode `file`', () => {
Expand Down Expand Up @@ -194,7 +244,7 @@ describe('testing schema definition mode `file`', () => {
// THEN
expect(() => {
api.addQuery('blah', new appsync.ResolvableField({ returnType: t.string }));
}).toThrowError('Unable to add query. Schema definition mode must be CODE Received: FILE');
}).toThrowError('Unable to add query. Schema definition mode must be CODE. Received: FILE');
});

test('definition mode `file` errors when addMutation is called', () => {
Expand All @@ -207,6 +257,19 @@ describe('testing schema definition mode `file`', () => {
// THEN
expect(() => {
api.addMutation('blah', new appsync.ResolvableField({ returnType: t.string }));
}).toThrowError('Unable to add mutation. Schema definition mode must be CODE Received: FILE');
}).toThrowError('Unable to add mutation. Schema definition mode must be CODE. Received: FILE');
});

test('definition mode `file` errors when addSubscription is called', () => {
// WHEN
const api = new appsync.GraphqlApi(stack, 'API', {
name: 'demo',
schema: appsync.Schema.fromAsset(join(__dirname, 'appsync.test.graphql')),
});

// THEN
expect(() => {
api.addSubscription('blah', new appsync.ResolvableField({ returnType: t.string }));
}).toThrowError('Unable to add subscription. Schema definition mode must be CODE. Received: FILE');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"ApiId"
]
},
"Definition": "schema {\n query: Query\n mutation: Mutation\n}\ninterface Node {\n created: String\n edited: String\n id: ID!\n}\ntype Planet {\n name: String\n diameter: Int\n rotationPeriod: Int\n orbitalPeriod: Int\n gravity: String\n population: [String]\n climates: [String]\n terrains: [String]\n surfaceWater: Float\n created: String\n edited: String\n id: ID!\n}\ntype Species implements Node {\n name: String\n classification: String\n designation: String\n averageHeight: Float\n averageLifespan: Int\n eyeColors: [String]\n hairColors: [String]\n skinColors: [String]\n language: String\n homeworld: Planet\n created: String\n edited: String\n id: ID!\n}\ntype Query {\n getPlanets: [Planet]\n}\ntype Mutation {\n addPlanet(name: String diameter: Int rotationPeriod: Int orbitalPeriod: Int gravity: String population: [String] climates: [String] terrains: [String] surfaceWater: Float): Planet\n}\ninput AwesomeInput {\n awesomeInput: String\n}\nenum Episodes {\n The_Phantom_Menace\n Attack_of_the_Clones\n Revenge_of_the_Sith\n A_New_Hope\n The_Empire_Strikes_Back\n Return_of_the_Jedi\n The_Force_Awakens\n The_Last_Jedi\n The_Rise_of_Skywalker\n}\nunion Union = Species | Planet\n"
"Definition": "schema {\n query: Query\n mutation: Mutation\n subscription: Subscription\n}\ninterface Node {\n created: String\n edited: String\n id: ID!\n}\ntype Planet {\n name: String\n diameter: Int\n rotationPeriod: Int\n orbitalPeriod: Int\n gravity: String\n population: [String]\n climates: [String]\n terrains: [String]\n surfaceWater: Float\n created: String\n edited: String\n id: ID!\n}\ntype Species implements Node {\n name: String\n classification: String\n designation: String\n averageHeight: Float\n averageLifespan: Int\n eyeColors: [String]\n hairColors: [String]\n skinColors: [String]\n language: String\n homeworld: Planet\n created: String\n edited: String\n id: ID!\n}\ntype Query {\n getPlanets: [Planet]\n}\ntype Mutation {\n addPlanet(name: String diameter: Int rotationPeriod: Int orbitalPeriod: Int gravity: String population: [String] climates: [String] terrains: [String] surfaceWater: Float): Planet\n}\ntype Subscription {\n addedPlanets(id: ID!): Planet\n @aws_subscribe(mutations: [\"addPlanet\"])\n}\ninput AwesomeInput {\n awesomeInput: String\n}\nenum Episodes {\n The_Phantom_Menace\n Attack_of_the_Clones\n Revenge_of_the_Sith\n A_New_Hope\n The_Empire_Strikes_Back\n Return_of_the_Jedi\n The_Force_Awakens\n The_Last_Jedi\n The_Rise_of_Skywalker\n}\nunion Union = Species | Planet\n"
}
},
"codefirstapiDefaultApiKey89863A80": {
Expand Down
5 changes: 5 additions & 0 deletions packages/@aws-cdk/aws-appsync/test/integ.graphql-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ api.addMutation('addPlanet', new appsync.ResolvableField({
responseMappingTemplate: appsync.MappingTemplate.dynamoDbResultItem(),
}));

api.addSubscription('addedPlanets', new appsync.ResolvableField({
returnType: planet.attribute(),
args: { id: ScalarType.required_id },
directives: [appsync.Directive.subscribe('addPlanet')],
}));
Comment on lines +106 to +110
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are there any additional changes to the verification steps because of this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unless we want to build out an entire app to check if subscriptions work then no..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

api.addType(new appsync.InputType('AwesomeInput', {
definition: { awesomeInput: ScalarType.string },
}));
Expand Down