Skip to content

Commit 9e3b798

Browse files
authored
feat(appsync): implement resolvable fields for code-first schema (#9660)
Implemented interfaces and resolvable fields for code-first schema. `Field` extends `GraphqlType` and will allow you to define arguments. <details> <summary> Field Example </summary> ```gql type Node { test(argument: string): String } ``` The CDK code required would be: ```ts const field = new appsync.Field(appsync.GraphqlType.string(), { args: { argument: appsync.GraphqlType.string(), }, }); const type = new appsynce.ObjectType('Node', { definition: { test: field }, }); ``` </details> `ResolvableField` extends `Field` and will allow you to define arguments and its resolvers. [**Object Types**](#Object-Types) can have fields that resolve and perform operations on your backend. <details> <summary> Resolvable Field Example </summary> For example, if we want to create the following type: ```gql type Query { get(argument: string): String } ``` The CDK code required would be: ```ts const field = new appsync.Field(appsync.GraphqlType.string(), { args: { argument: appsync.GraphqlType.string(), }, dataSource: api.addNoneDataSource('none'), requestMappingTemplate: dummyRequest, responseMappingTemplate: dummyResponse, }); const type = new appsynce.ObjectType('Query', { definition: { get: field }, }); </details> ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent ef98b9f commit 9e3b798

8 files changed

+1119
-470
lines changed

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

+210-34
Original file line numberDiff line numberDiff line change
@@ -180,81 +180,257 @@ api.grantMutation(role, 'updateExample');
180180
api.grant(role, appsync.IamResource.ofType('Mutation', 'updateExample'), 'appsync:GraphQL');
181181
```
182182

183-
### Code-First Schema
183+
## Code-First Schema
184184

185185
CDK offers the ability to generate your schema in a code-first approach.
186186
A code-first approach offers a developer workflow with:
187187
- **modularity**: organizing schema type definitions into different files
188188
- **reusability**: simplifying down boilerplate/repetitive code
189189
- **consistency**: resolvers and schema definition will always be synced
190190

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

193-
#### Code-First Example
193+
### Code-First Example
194194

195-
We are going to reference the [example](#Example) through a code-first approach.
195+
To showcase the code-first approach. Let's try to model the following schema segment.
196+
197+
```gql
198+
interface Node {
199+
id: String
200+
}
201+
202+
type Query {
203+
allFilms(after: String, first: Int, before: String, last: Int): FilmConnection
204+
}
205+
206+
type FilmNode implements Node {
207+
filmName: String
208+
}
209+
210+
type FilmConnection {
211+
edges: [FilmEdge]
212+
films: [Film]
213+
totalCount: Int
214+
}
215+
216+
type FilmEdge {
217+
node: Film
218+
cursor: String
219+
}
220+
```
221+
222+
Above we see a schema that allows for generating paginated responses. For example,
223+
we can query `allFilms(first: 100)` since `FilmConnection` acts as an intermediary
224+
for holding `FilmEdges` we can write a resolver to return the first 100 films.
225+
226+
In a separate file, we can declare our scalar types: `scalar-types.ts`.
227+
228+
```ts
229+
import { GraphqlType } from '@aws-cdk/aws-appsync';
230+
231+
export const string = appsync.GraphqlType.string();
232+
export const int = appsync.GraphqlType.int();
233+
```
234+
235+
In another separate file, we can declare our object types and related functions.
236+
We will call this file `object-types.ts` and we will have created it in a way that
237+
allows us to generate other `XxxConnection` and `XxxEdges` in the future.
196238

197239
```ts
240+
const pluralize = require('pluralize');
241+
import * as scalar from './scalar-types.ts';
198242
import * as appsync from '@aws-cdk/aws-appsync';
199-
import * as db from '@aws-cdk/aws-dynamodb';
243+
244+
export const args = {
245+
after: scalar.string,
246+
first: scalar.int,
247+
before: scalar.string,
248+
last: scalar.int,
249+
};
250+
251+
export const Node = new appsync.InterfaceType('Node', {
252+
definition: { id: scalar.string }
253+
});
254+
export const FilmNode = new appsync.ObjectType.implementInterface('FilmNode', {
255+
interfaceTypes: [Node],
256+
definition: { filmName: scalar.string }
257+
});
258+
259+
export function generateEdgeAndConnection(base: appsync.ObjectType) {
260+
const edge = new appsync.ObjectType(`${base.name}Edge`, {
261+
definition: { node: base.attribute(), cursor: scalar.string }
262+
});
263+
const connection = new appsync.ObjectType(`${base.name}Connection`, {
264+
definition: {
265+
edges: edges.attribute({ isList: true }),
266+
[pluralize(base.name)]: base.attribute({ isList: true }),
267+
totalCount: scalar.int,
268+
}
269+
});
270+
return { edge: edge, connection: connection };
271+
}
272+
```
273+
274+
Finally, we will go to our `cdk-stack` and combine everything together
275+
to generate our schema.
276+
277+
```ts
278+
import * as appsync from '@aws-cdk/aws-appsync';
279+
import * as schema from './object-types';
200280

201281
const api = new appsync.GraphQLApi(stack, 'Api', {
202282
name: 'demo',
203283
schemaDefinition: appsync.SchemaDefinition.CODE,
204-
authorizationConfig: {
205-
defaultAuthorization: {
206-
authorizationType: appsync.AuthorizationType.IAM
207-
},
208-
},
209-
});
210-
211-
const demoTable = new db.Table(stack, 'DemoTable', {
212-
partitionKey: {
213-
name: 'id',
214-
type: db.AttributeType.STRING,
215-
},
216284
});
217285

218-
const demoDS = api.addDynamoDbDataSource('demoDataSource', 'Table for Demos', demoTable);
286+
this.objectTypes = [ schema.Node, schema.Film ];
219287

220-
// Schema Definition starts here
288+
const filmConnections = schema.generateEdgeAndConnection(schema.Film);
221289

222-
const demo = api.addType('demo', {
290+
api.addType('Query', {
223291
definition: {
224-
id: appsync.GraphqlType.string({ isRequired: true }),
225-
version: appsync.GraphqlType.string({ isRequired: true }),
226-
},
227-
});
292+
allFilms: new appsync.ResolvableField(dummyDataSource, {
293+
returnType: filmConnections.connection.attribute(),
294+
args: schema.args,
295+
requestMappingTemplate: dummyRequest,
296+
responseMappingTemplate: dummyResponse,
297+
},
298+
}
299+
});
300+
})
228301

302+
this.objectTypes.map((t) => api.appendToSchema(t));
303+
Object.keys(filmConnections).forEach((key) => api.appendToSchema(filmConnections[key]));
229304
```
230305
231-
#### GraphQL Types
306+
Notice how we can utilize the `generateEdgeAndConnection` function to generate
307+
Object Types. In the future, if we wanted to create more Object Types, we can simply
308+
create the base Object Type (i.e. Film) and from there we can generate its respective
309+
`Connections` and `Edges`.
310+
311+
Check out a more in-depth example [here](https://github.com/BryanPan342/starwars-code-first).
312+
313+
### GraphQL Types
232314
233315
One of the benefits of GraphQL is its strongly typed nature. We define the
234316
types within an object, query, mutation, interface, etc. as **GraphQL Types**.
235317
236318
GraphQL Types are the building blocks of types, whether they are scalar, objects,
237319
interfaces, etc. GraphQL Types can be:
238320
- [**Scalar Types**](https://docs.aws.amazon.com/appsync/latest/devguide/scalars.html): Id, Int, String, AWSDate, etc.
239-
- **Object Types**: types that you generate (i.e. `demo` from the example above)
240-
- **Interface Types**: abstract types that define the base implementation of other
321+
- [**Object Types**](#Object-Types): types that you generate (i.e. `demo` from the example above)
322+
- [**Interface Types**](#Interface-Types): abstract types that define the base implementation of other
241323
Intermediate Types
242324
243325
More concretely, GraphQL Types are simply the types appended to variables.
244326
Referencing the object type `Demo` in the previous example, the GraphQL Types
245327
is `String!` and is applied to both the names `id` and `version`.
246328
247-
#### Intermediate Types
329+
### Field and Resolvable Fields
330+
331+
While `GraphqlType` is a base implementation for GraphQL fields, we have abstractions
332+
on top of `GraphqlType` that provide finer grain support.
333+
334+
#### Field
335+
336+
`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,
337+
but not its resolvers.
338+
339+
For example, if we want to create the following type:
340+
341+
```gql
342+
type Node {
343+
test(argument: string): String
344+
}
345+
```
346+
347+
The CDK code required would be:
348+
349+
```ts
350+
const field = new appsync.Field({
351+
returnType: appsync.GraphqlType.string(),
352+
args: {
353+
argument: appsync.GraphqlType.string(),
354+
},
355+
});
356+
const type = new appsync.InterfaceType('Node', {
357+
definition: { test: field },
358+
});
359+
```
360+
361+
#### Resolvable Fields
362+
363+
`ResolvableField` extends `Field` and will allow you to define arguments and its resolvers.
364+
[**Object Types**](#Object-Types) can have fields that resolve and perform operations on
365+
your backend.
366+
367+
You can also create resolvable fields for object types.
368+
369+
```gql
370+
type Info {
371+
node(id: String): String
372+
}
373+
```
374+
375+
The CDK code required would be:
376+
377+
```ts
378+
const info = new appsync.ObjectType('Info', {
379+
definition: {
380+
node: new appsync.ResolvableField({
381+
returnType: appsync.GraphqlType.string(),
382+
args: {
383+
id: appsync.GraphqlType.string(),
384+
},
385+
dataSource: api.addNoneDataSource('none'),
386+
requestMappingTemplate: dummyRequest,
387+
responseMappingTemplate: dummyResponse,
388+
}),
389+
},
390+
});
391+
```
392+
393+
To nest resolvers, we can also create top level query types that call upon
394+
other types. Building off the previous example, if we want the following graphql
395+
type definition:
396+
397+
```gql
398+
type Query {
399+
get(argument: string): Info
400+
}
401+
```
402+
403+
The CDK code required would be:
404+
405+
```ts
406+
const query = new appsync.ObjectType('Query', {
407+
definition: {
408+
get: new appsync.ResolvableField({
409+
returnType: appsync.GraphqlType.string(),
410+
args: {
411+
argument: appsync.GraphqlType.string(),
412+
},
413+
dataSource: api.addNoneDataSource('none'),
414+
requestMappingTemplate: dummyRequest,
415+
responseMappingTemplate: dummyResponse,
416+
}),
417+
},
418+
});
419+
```
420+
421+
Learn more about fields and resolvers [here](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-mapping-template-reference-overview.html).
248422
249-
Intermediate Types are abstractions above Scalar Types. They have a set of defined
423+
### Intermediate Types
424+
425+
Intermediate Types are defined by Graphql Types and Fields. They have a set of defined
250426
fields, where each field corresponds to another type in the system. Intermediate
251427
Types will be the meat of your GraphQL Schema as they are the types defined by you.
252428
253429
Intermediate Types include:
254430
- [**Interface Types**](#Interface-Types)
255431
- [**Object Types**](#Object-Types)
256432
257-
#### Interface Types
433+
### Interface Types
258434
259435
**Interface Types** are abstract types that define the implementation of other
260436
intermediate types. They are useful for eliminating duplication and can be used
@@ -269,7 +445,7 @@ const node = new appsync.InterfaceType('Node', {
269445
});
270446
```
271447
272-
#### Object Types
448+
### Object Types
273449
274450
**Object Types** are types that you declare. For example, in the [code-first example](#code-first-example)
275451
the `demo` variable is an **Object Type**. **Object Types** are defined by
@@ -297,7 +473,7 @@ You can create Object Types in three ways:
297473
298474
`scalar-types.ts` - a file for scalar type definitions
299475
```ts
300-
export const required_string = new appsync.GraphqlType.string({ isRequired: true });
476+
export const required_string = appsync.GraphqlType.string({ isRequired: true });
301477
```
302478
303479
`object-types.ts` - a file for object type definitions
@@ -324,7 +500,7 @@ You can create Object Types in three ways:
324500
id: appsync.GraphqlType.string({ isRequired: true }),
325501
},
326502
});
327-
const demo = new appsync.ObjectType.implementInterface('Demo', {
503+
const demo = new appsync.ObjectType('Demo', {
328504
interfaceTypes: [ node ],
329505
defintion: {
330506
version: appsync.GraphqlType.string({ isRequired: true }),
@@ -347,4 +523,4 @@ You can create Object Types in three ways:
347523
});
348524
```
349525
> This method provides easy use and is ideal for smaller projects.
350-
526+

0 commit comments

Comments
 (0)