Skip to content

Commit

Permalink
feat(appsync): support query & mutation generation for code-first app…
Browse files Browse the repository at this point in the history
…roach (#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*
  • Loading branch information
BryanPan342 committed Aug 29, 2020
1 parent ba51ea3 commit 1ed119e
Show file tree
Hide file tree
Showing 10 changed files with 367 additions and 55 deletions.
79 changes: 61 additions & 18 deletions packages/@aws-cdk/aws-appsync/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ api.grantMutation(role, 'updateExample');
api.grant(role, appsync.IamResource.ofType('Mutation', 'updateExample'), 'appsync:GraphQL');
```

## Code-First Schema
### Code-First Schema

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

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

### Code-First Example
#### Code-First Example

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

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

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

api.addType('Query', {
definition: {
allFilms: new appsync.ResolvableField(dummyDataSource, {
returnType: filmConnections.connection.attribute(),
args: schema.args,
requestMappingTemplate: dummyRequest,
responseMappingTemplate: dummyResponse,
}),
}
api.addQuery('allFilms', new appsync.ResolvableField({
returnType: filmConnections.connection.attribute(),
args: schema.args,
dataSource: dummyDataSource,
requestMappingTemplate: dummyRequest,
responseMappingTemplate: dummyResponse,
}),
});

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

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

### GraphQL Types
#### GraphQL Types

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

### Field and Resolvable Fields
#### Field and Resolvable Fields

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

#### Field
##### Field

`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,
but not its resolvers.
Expand All @@ -401,7 +399,7 @@ const type = new appsync.InterfaceType('Node', {
});
```

#### Resolvable Fields
##### Resolvable Fields

`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
Expand Down Expand Up @@ -463,7 +461,7 @@ const query = new appsync.ObjectType('Query', {

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

### Intermediate Types
#### Intermediate Types

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

### Interface Types
##### Interface Types

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

### Object Types
##### Object Types

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

Every schema requires a top level Query type. By default, the schema will look
for the `Object Type` named `Query`. The top level `Query` is the **only** exposed
type that users can access to perform `GET` operations on your Api.

To add fields for these queries, we can simply run the `addQuery` function to add
to the schema's `Query` type.

```ts
const string = appsync.GraphqlType.string();
const int = appsync.GraphqlType.int();
api.addQuery('allFilms', new appsync.ResolvableField({
returnType: filmConnection.attribute(),
args: { after: string, first: int, before: string, last: int},
dataSource: api.addNoneDataSource('none'),
requestMappingTemplate: dummyRequest,
responseMappingTemplate: dummyResponse,
}));
```

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

#### Mutation

Every schema **can** have a top level Mutation type. By default, the schema will look
for the `Object Type` named `Mutation`. The top level `Mutation` Type is the only exposed
type that users can access to perform `mutable` operations on your Api.

To add fields for these mutations, we can simply run the `addMutation` function to add
to the schema's `Mutation` type.

```ts
const string = appsync.GraphqlType.string();
const int = appsync.GraphqlType.int();
api.addMutation('addFilm', new appsync.ResolvableField({
returnType: film.attribute(),
args: { name: string, film_number: int },
dataSource: api.addNoneDataSource('none'),
requestMappingTemplate: dummyRequest,
responseMappingTemplate: dummyResponse,
}));
```

To learn more about top level operations, check out the docs [here](https://docs.aws.amazon.com/appsync/latest/devguide/graphql-overview.html).
32 changes: 32 additions & 0 deletions packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { CfnApiKey, CfnGraphQLApi, CfnGraphQLSchema } from './appsync.generated'
import { IGraphqlApi, GraphqlApiBase } from './graphqlapi-base';
import { Schema } from './schema';
import { IIntermediateType } from './schema-base';
import { ResolvableField } from './schema-field';
import { ObjectType } from './schema-intermediate';

/**
* enum with all possible values for AppSync authorization type
Expand Down Expand Up @@ -588,4 +590,34 @@ export class GraphQLApi extends GraphqlApiBase {
public addType(type: IIntermediateType): IIntermediateType {
return this.schema.addType(type);
}

/**
* 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,
*
* type Query {
* fieldName: Field.returnType
* }
*
* @param fieldName the name of the query
* @param field the resolvable field to for this query
*/
public addQuery(fieldName: string, field: ResolvableField): ObjectType {
return this.schema.addQuery(fieldName, field);
}

/**
* 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,
*
* type Mutation {
* fieldName: Field.returnType
* }
*
* @param fieldName the name of the Mutation
* @param field the resolvable field to for this Mutation
*/
public addMutation(fieldName: string, field: ResolvableField): ObjectType {
return this.schema.addMutation(fieldName, field);
}
}
83 changes: 80 additions & 3 deletions packages/@aws-cdk/aws-appsync/lib/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { readFileSync } from 'fs';
import { Lazy } from '@aws-cdk/core';
import { CfnGraphQLSchema } from './appsync.generated';
import { GraphQLApi } from './graphqlapi';
import { SchemaMode } from './private';
import { SchemaMode, shapeAddition } from './private';
import { IIntermediateType } from './schema-base';
import { ResolvableField } from './schema-field';
import { ObjectType } from './schema-intermediate';

/**
* The options for configuring a schema
Expand Down Expand Up @@ -44,7 +46,13 @@ export class Schema {
*/
public definition: string;

protected schema?: CfnGraphQLSchema;
private query?: ObjectType;

private mutation?: ObjectType;

private subscription?: ObjectType;

private schema?: CfnGraphQLSchema;

private mode: SchemaMode;

Expand All @@ -68,7 +76,7 @@ export class Schema {
if (!this.schema) {
this.schema = new CfnGraphQLSchema(api, 'Schema', {
apiId: api.apiId,
definition: Lazy.stringValue({ produce: () => this.definition }),
definition: Lazy.stringValue({ produce: () => `${this.declareSchema()}${this.definition}` }),
});
}
return this.schema;
Expand All @@ -92,6 +100,52 @@ export class Schema {
this.definition = `${this.definition}${sep}${addition}\n`;
}

/**
* 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,
*
* type Query {
* fieldName: Field.returnType
* }
*
* @param fieldName the name of the query
* @param field the resolvable field to for this query
*/
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}`);
}
if (!this.query) {
this.query = new ObjectType('Query', { definition: {} });
this.addType(this.query);
};
this.query.addField(fieldName, field);
return this.query;
}

/**
* 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,
*
* type Mutation {
* fieldName: Field.returnType
* }
*
* @param fieldName the name of the Mutation
* @param field the resolvable field to for this Mutation
*/
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}`);
}
if (!this.mutation) {
this.mutation = new ObjectType('Mutation', { definition: {} });
this.addType(this.mutation);
};
this.mutation.addField(fieldName, field);
return this.mutation;
}

/**
* Add type to the schema
*
Expand All @@ -106,4 +160,27 @@ export class Schema {
this.addToSchema(Lazy.stringValue({ produce: () => type.toString() }));
return type;
}

/**
* Set the root types of this schema if they are defined.
*
* For example:
* schema {
* query: Query
* mutation: Mutation
* subscription: Subscription
* }
*/
private declareSchema(): string {
if (!this.query && !this.mutation && !this.subscription) {
return '';
}
type root = 'mutation' | 'query' | 'subscription';
const list: root[] = ['query', 'mutation', 'subscription'];
return shapeAddition({
prefix: 'schema',
fields: list.map((key: root) => this[key] ? `${key}: ${this[key]?.name}` : '')
.filter((field) => field != ''),
}) + '\n';
}
}

0 comments on commit 1ed119e

Please sign in to comment.