Skip to content

Commit 1ed119e

Browse files
authored
feat(appsync): support query & mutation generation for code-first approach (#9992)
Implemented methods in `appsync.Schema` to easily generate query/mutation fields, or bind an existing `ObjectType` as the top level query/mutation type. Fixes: #9308 Fixes: #9310 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent ba51ea3 commit 1ed119e

10 files changed

+367
-55
lines changed

packages/@aws-cdk/aws-appsync/README.md

+61-18
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ api.grantMutation(role, 'updateExample');
225225
api.grant(role, appsync.IamResource.ofType('Mutation', 'updateExample'), 'appsync:GraphQL');
226226
```
227227

228-
## Code-First Schema
228+
### Code-First Schema
229229

230230
CDK offers the ability to generate your schema in a code-first approach.
231231
A code-first approach offers a developer workflow with:
@@ -235,7 +235,7 @@ A code-first approach offers a developer workflow with:
235235

236236
The code-first approach allows for **dynamic** schema generation. You can generate your schema based on variables and templates to reduce code duplication.
237237

238-
### Code-First Example
238+
#### Code-First Example
239239

240240
To showcase the code-first approach. Let's try to model the following schema segment.
241241

@@ -331,15 +331,13 @@ this.objectTypes = [ schema.Node, schema.Film ];
331331

332332
const filmConnections = schema.generateEdgeAndConnection(schema.Film);
333333

334-
api.addType('Query', {
335-
definition: {
336-
allFilms: new appsync.ResolvableField(dummyDataSource, {
337-
returnType: filmConnections.connection.attribute(),
338-
args: schema.args,
339-
requestMappingTemplate: dummyRequest,
340-
responseMappingTemplate: dummyResponse,
341-
}),
342-
}
334+
api.addQuery('allFilms', new appsync.ResolvableField({
335+
returnType: filmConnections.connection.attribute(),
336+
args: schema.args,
337+
dataSource: dummyDataSource,
338+
requestMappingTemplate: dummyRequest,
339+
responseMappingTemplate: dummyResponse,
340+
}),
343341
});
344342

345343
this.objectTypes.map((t) => api.addType(t));
@@ -353,7 +351,7 @@ create the base Object Type (i.e. Film) and from there we can generate its respe
353351

354352
Check out a more in-depth example [here](https://github.com/BryanPan342/starwars-code-first).
355353

356-
### GraphQL Types
354+
#### GraphQL Types
357355

358356
One of the benefits of GraphQL is its strongly typed nature. We define the
359357
types within an object, query, mutation, interface, etc. as **GraphQL Types**.
@@ -369,12 +367,12 @@ More concretely, GraphQL Types are simply the types appended to variables.
369367
Referencing the object type `Demo` in the previous example, the GraphQL Types
370368
is `String!` and is applied to both the names `id` and `version`.
371369

372-
### Field and Resolvable Fields
370+
#### Field and Resolvable Fields
373371

374372
While `GraphqlType` is a base implementation for GraphQL fields, we have abstractions
375373
on top of `GraphqlType` that provide finer grain support.
376374

377-
#### Field
375+
##### Field
378376

379377
`Field` extends `GraphqlType` and will allow you to define arguments. [**Interface Types**](#Interface-Types) are not resolvable and this class will allow you to define arguments,
380378
but not its resolvers.
@@ -401,7 +399,7 @@ const type = new appsync.InterfaceType('Node', {
401399
});
402400
```
403401

404-
#### Resolvable Fields
402+
##### Resolvable Fields
405403

406404
`ResolvableField` extends `Field` and will allow you to define arguments and its resolvers.
407405
[**Object Types**](#Object-Types) can have fields that resolve and perform operations on
@@ -463,7 +461,7 @@ const query = new appsync.ObjectType('Query', {
463461

464462
Learn more about fields and resolvers [here](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-mapping-template-reference-overview.html).
465463

466-
### Intermediate Types
464+
#### Intermediate Types
467465

468466
Intermediate Types are defined by Graphql Types and Fields. They have a set of defined
469467
fields, where each field corresponds to another type in the system. Intermediate
@@ -473,7 +471,7 @@ Intermediate Types include:
473471
- [**Interface Types**](#Interface-Types)
474472
- [**Object Types**](#Object-Types)
475473

476-
### Interface Types
474+
##### Interface Types
477475

478476
**Interface Types** are abstract types that define the implementation of other
479477
intermediate types. They are useful for eliminating duplication and can be used
@@ -488,7 +486,7 @@ const node = new appsync.InterfaceType('Node', {
488486
});
489487
```
490488

491-
### Object Types
489+
##### Object Types
492490

493491
**Object Types** are types that you declare. For example, in the [code-first example](#code-first-example)
494492
the `demo` variable is an **Object Type**. **Object Types** are defined by
@@ -565,3 +563,48 @@ You can create Object Types in three ways:
565563
```
566564
> This method provides easy use and is ideal for smaller projects.
567565

566+
#### Query
567+
568+
Every schema requires a top level Query type. By default, the schema will look
569+
for the `Object Type` named `Query`. The top level `Query` is the **only** exposed
570+
type that users can access to perform `GET` operations on your Api.
571+
572+
To add fields for these queries, we can simply run the `addQuery` function to add
573+
to the schema's `Query` type.
574+
575+
```ts
576+
const string = appsync.GraphqlType.string();
577+
const int = appsync.GraphqlType.int();
578+
api.addQuery('allFilms', new appsync.ResolvableField({
579+
returnType: filmConnection.attribute(),
580+
args: { after: string, first: int, before: string, last: int},
581+
dataSource: api.addNoneDataSource('none'),
582+
requestMappingTemplate: dummyRequest,
583+
responseMappingTemplate: dummyResponse,
584+
}));
585+
```
586+
587+
To learn more about top level operations, check out the docs [here](https://docs.aws.amazon.com/appsync/latest/devguide/graphql-overview.html).
588+
589+
#### Mutation
590+
591+
Every schema **can** have a top level Mutation type. By default, the schema will look
592+
for the `Object Type` named `Mutation`. The top level `Mutation` Type is the only exposed
593+
type that users can access to perform `mutable` operations on your Api.
594+
595+
To add fields for these mutations, we can simply run the `addMutation` function to add
596+
to the schema's `Mutation` type.
597+
598+
```ts
599+
const string = appsync.GraphqlType.string();
600+
const int = appsync.GraphqlType.int();
601+
api.addMutation('addFilm', new appsync.ResolvableField({
602+
returnType: film.attribute(),
603+
args: { name: string, film_number: int },
604+
dataSource: api.addNoneDataSource('none'),
605+
requestMappingTemplate: dummyRequest,
606+
responseMappingTemplate: dummyResponse,
607+
}));
608+
```
609+
610+
To learn more about top level operations, check out the docs [here](https://docs.aws.amazon.com/appsync/latest/devguide/graphql-overview.html).

packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts

+32
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { CfnApiKey, CfnGraphQLApi, CfnGraphQLSchema } from './appsync.generated'
55
import { IGraphqlApi, GraphqlApiBase } from './graphqlapi-base';
66
import { Schema } from './schema';
77
import { IIntermediateType } from './schema-base';
8+
import { ResolvableField } from './schema-field';
9+
import { ObjectType } from './schema-intermediate';
810

911
/**
1012
* enum with all possible values for AppSync authorization type
@@ -588,4 +590,34 @@ export class GraphQLApi extends GraphqlApiBase {
588590
public addType(type: IIntermediateType): IIntermediateType {
589591
return this.schema.addType(type);
590592
}
593+
594+
/**
595+
* Add a query field to the schema's Query. If one isn't set by
596+
* the user, CDK will create an Object Type called 'Query'. For example,
597+
*
598+
* type Query {
599+
* fieldName: Field.returnType
600+
* }
601+
*
602+
* @param fieldName the name of the query
603+
* @param field the resolvable field to for this query
604+
*/
605+
public addQuery(fieldName: string, field: ResolvableField): ObjectType {
606+
return this.schema.addQuery(fieldName, field);
607+
}
608+
609+
/**
610+
* Add a mutation field to the schema's Mutation. If one isn't set by
611+
* the user, CDK will create an Object Type called 'Mutation'. For example,
612+
*
613+
* type Mutation {
614+
* fieldName: Field.returnType
615+
* }
616+
*
617+
* @param fieldName the name of the Mutation
618+
* @param field the resolvable field to for this Mutation
619+
*/
620+
public addMutation(fieldName: string, field: ResolvableField): ObjectType {
621+
return this.schema.addMutation(fieldName, field);
622+
}
591623
}

packages/@aws-cdk/aws-appsync/lib/schema.ts

+80-3
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import { readFileSync } from 'fs';
22
import { Lazy } from '@aws-cdk/core';
33
import { CfnGraphQLSchema } from './appsync.generated';
44
import { GraphQLApi } from './graphqlapi';
5-
import { SchemaMode } from './private';
5+
import { SchemaMode, shapeAddition } from './private';
66
import { IIntermediateType } from './schema-base';
7+
import { ResolvableField } from './schema-field';
8+
import { ObjectType } from './schema-intermediate';
79

810
/**
911
* The options for configuring a schema
@@ -44,7 +46,13 @@ export class Schema {
4446
*/
4547
public definition: string;
4648

47-
protected schema?: CfnGraphQLSchema;
49+
private query?: ObjectType;
50+
51+
private mutation?: ObjectType;
52+
53+
private subscription?: ObjectType;
54+
55+
private schema?: CfnGraphQLSchema;
4856

4957
private mode: SchemaMode;
5058

@@ -68,7 +76,7 @@ export class Schema {
6876
if (!this.schema) {
6977
this.schema = new CfnGraphQLSchema(api, 'Schema', {
7078
apiId: api.apiId,
71-
definition: Lazy.stringValue({ produce: () => this.definition }),
79+
definition: Lazy.stringValue({ produce: () => `${this.declareSchema()}${this.definition}` }),
7280
});
7381
}
7482
return this.schema;
@@ -92,6 +100,52 @@ export class Schema {
92100
this.definition = `${this.definition}${sep}${addition}\n`;
93101
}
94102

103+
/**
104+
* Add a query field to the schema's Query. If one isn't set by
105+
* the user, CDK will create an Object Type called 'Query'. For example,
106+
*
107+
* type Query {
108+
* fieldName: Field.returnType
109+
* }
110+
*
111+
* @param fieldName the name of the query
112+
* @param field the resolvable field to for this query
113+
*/
114+
public addQuery(fieldName: string, field: ResolvableField): ObjectType {
115+
if (this.mode !== SchemaMode.CODE) {
116+
throw new Error(`Unable to add query. Schema definition mode must be ${SchemaMode.CODE} Received: ${this.mode}`);
117+
}
118+
if (!this.query) {
119+
this.query = new ObjectType('Query', { definition: {} });
120+
this.addType(this.query);
121+
};
122+
this.query.addField(fieldName, field);
123+
return this.query;
124+
}
125+
126+
/**
127+
* Add a mutation field to the schema's Mutation. If one isn't set by
128+
* the user, CDK will create an Object Type called 'Mutation'. For example,
129+
*
130+
* type Mutation {
131+
* fieldName: Field.returnType
132+
* }
133+
*
134+
* @param fieldName the name of the Mutation
135+
* @param field the resolvable field to for this Mutation
136+
*/
137+
public addMutation(fieldName: string, field: ResolvableField): ObjectType {
138+
if (this.mode !== SchemaMode.CODE) {
139+
throw new Error(`Unable to add mutation. Schema definition mode must be ${SchemaMode.CODE} Received: ${this.mode}`);
140+
}
141+
if (!this.mutation) {
142+
this.mutation = new ObjectType('Mutation', { definition: {} });
143+
this.addType(this.mutation);
144+
};
145+
this.mutation.addField(fieldName, field);
146+
return this.mutation;
147+
}
148+
95149
/**
96150
* Add type to the schema
97151
*
@@ -106,4 +160,27 @@ export class Schema {
106160
this.addToSchema(Lazy.stringValue({ produce: () => type.toString() }));
107161
return type;
108162
}
163+
164+
/**
165+
* Set the root types of this schema if they are defined.
166+
*
167+
* For example:
168+
* schema {
169+
* query: Query
170+
* mutation: Mutation
171+
* subscription: Subscription
172+
* }
173+
*/
174+
private declareSchema(): string {
175+
if (!this.query && !this.mutation && !this.subscription) {
176+
return '';
177+
}
178+
type root = 'mutation' | 'query' | 'subscription';
179+
const list: root[] = ['query', 'mutation', 'subscription'];
180+
return shapeAddition({
181+
prefix: 'schema',
182+
fields: list.map((key: root) => this[key] ? `${key}: ${this[key]?.name}` : '')
183+
.filter((field) => field != ''),
184+
}) + '\n';
185+
}
109186
}

0 commit comments

Comments
 (0)