Skip to content

Commit 8d71fa1

Browse files
authored
feat(appsync): separating schema from graphql api (#9903)
Separating GraphQL Schema from GraphQL Api to simplify GraphQL Api Props. `GraphQL Schema` is now its own class and employs static functions to construct GraphQL API. By default, GraphQL Api will be configured to a code-first approach. To override this, use the `schema` property to specify a method of schema declaration. For example, ```ts const api = appsync.GraphQLApi(stack, 'api', { name: 'myApi', schema: appsync.Schema.fromAsset(join(__dirname, 'schema.graphl')), }); ``` **BREAKING CHANGES**: AppSync GraphQL Schema declared through static functions as opposed to two separate properties - **appsync**: props `SchemaDefinition` and `SchemaDefinitionFile` have been condensed down to a singular property `schema` - **appsync**: no longer directly exposes `CfnGraphQLSchema` from `GraphQLApi.schema` ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent c7f57a7 commit 8d71fa1

23 files changed

+609
-291
lines changed

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

+57-16
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@ import * as db from '@aws-cdk/aws-dynamodb';
4747

4848
const api = new appsync.GraphQLApi(stack, 'Api', {
4949
name: 'demo',
50-
schemaDefinition: appsync.SchemaDefinition.FILE,
51-
schemaDefinitionFile: join(__dirname, 'schema.graphql'),
50+
schema: appsync.Schema.fromAsset(join(__dirname, 'schema.graphql')),
5251
authorizationConfig: {
5352
defaultAuthorization: {
5453
authorizationType: appsync.AuthorizationType.IAM
@@ -83,7 +82,53 @@ demoDS.createResolver({
8382
});
8483
```
8584

86-
## Imports
85+
### Schema
86+
87+
Every GraphQL Api needs a schema to define the Api. CDK offers `appsync.Schema`
88+
for static convenience methods for various types of schema declaration: code-first
89+
or schema-first.
90+
91+
#### Code-First
92+
93+
When declaring your GraphQL Api, CDK defaults to a code-first approach if the
94+
`schema` property is not configured.
95+
96+
```ts
97+
const api = new appsync.GraphQLApi(stack, 'api', { name: 'myApi' });
98+
```
99+
100+
CDK will declare a `Schema` class that will give your Api access functions to
101+
define your schema code-first: `addType`, `addObjectType`, `addToSchema`, etc.
102+
103+
You can also declare your `Schema` class outside of your CDK stack, to define
104+
your schema externally.
105+
106+
```ts
107+
const schema = new appsync.Schema();
108+
schema.addObjectType('demo', {
109+
definition: { id: appsync.GraphqlType.id() },
110+
});
111+
const api = new appsync.GraphQLApi(stack, 'api', {
112+
name: 'myApi',
113+
schema
114+
});
115+
```
116+
117+
See the [code-first schema](#Code-First-Schema) section for more details.
118+
119+
#### Schema-First
120+
121+
You can define your GraphQL Schema from a file on disk. For convenience, use
122+
the `appsync.Schema.fromAsset` to specify the file representing your schema.
123+
124+
```ts
125+
const api = appsync.GraphQLApi(stack, 'api', {
126+
name: 'myApi',
127+
schema: appsync.Schema.fromAsset(join(__dirname, 'schema.graphl')),
128+
});
129+
```
130+
131+
### Imports
87132

88133
Any GraphQL Api that has been created outside the stack can be imported from
89134
another stack into your CDK app. Utilizing the `fromXxx` function, you have
@@ -101,7 +146,7 @@ If you don't specify `graphqlArn` in `fromXxxAttributes`, CDK will autogenerate
101146
the expected `arn` for the imported api, given the `apiId`. For creating data
102147
sources and resolvers, an `apiId` is sufficient.
103148

104-
## Permissions
149+
### Permissions
105150

106151
When using `AWS_IAM` as the authorization type for GraphQL API, an IAM Role
107152
with correct permissions must be used for access to API.
@@ -153,7 +198,7 @@ const api = new appsync.GraphQLApi(stack, 'API', {
153198
api.grant(role, appsync.IamResource.custom('types/Mutation/fields/updateExample'), 'appsync:GraphQL')
154199
```
155200

156-
### IamResource
201+
#### IamResource
157202

158203
In order to use the `grant` functions, you need to use the class `IamResource`.
159204

@@ -163,7 +208,7 @@ In order to use the `grant` functions, you need to use the class `IamResource`.
163208

164209
- `IamResource.all()` permits ALL resources.
165210

166-
### Generic Permissions
211+
#### Generic Permissions
167212

168213
Alternatively, you can use more generic `grant` functions to accomplish the same usage.
169214

@@ -280,7 +325,6 @@ import * as schema from './object-types';
280325

281326
const api = new appsync.GraphQLApi(stack, 'Api', {
282327
name: 'demo',
283-
schemaDefinition: appsync.SchemaDefinition.CODE,
284328
});
285329

286330
this.objectTypes = [ schema.Node, schema.Film ];
@@ -294,13 +338,12 @@ api.addType('Query', {
294338
args: schema.args,
295339
requestMappingTemplate: dummyRequest,
296340
responseMappingTemplate: dummyResponse,
297-
},
341+
}),
298342
}
299-
});
300-
})
343+
});
301344

302-
this.objectTypes.map((t) => api.appendToSchema(t));
303-
Object.keys(filmConnections).forEach((key) => api.appendToSchema(filmConnections[key]));
345+
this.objectTypes.map((t) => api.addType(t));
346+
Object.keys(filmConnections).forEach((key) => api.addType(filmConnections[key]));
304347
```
305348

306349
Notice how we can utilize the `generateEdgeAndConnection` function to generate
@@ -457,7 +500,6 @@ You can create Object Types in three ways:
457500
```ts
458501
const api = new appsync.GraphQLApi(stack, 'Api', {
459502
name: 'demo',
460-
schemaDefinition: appsync.SchemaDefinition.CODE,
461503
});
462504
const demo = new appsync.ObjectType('Demo', {
463505
defintion: {
@@ -466,7 +508,7 @@ You can create Object Types in three ways:
466508
},
467509
});
468510

469-
api.appendToSchema(object.toString());
511+
api.addType(object);
470512
```
471513
> This method allows for reusability and modularity, ideal for larger projects.
472514
For example, imagine moving all Object Type definition outside the stack.
@@ -490,7 +532,7 @@ You can create Object Types in three ways:
490532
`cdk-stack.ts` - a file containing our cdk stack
491533
```ts
492534
import { demo } from './object-types';
493-
api.appendToSchema(demo.toString());
535+
api.addType(demo);
494536
```
495537

496538
2. Object Types can be created ***externally*** from an Interface Type.
@@ -513,7 +555,6 @@ You can create Object Types in three ways:
513555
```ts
514556
const api = new appsync.GraphQLApi(stack, 'Api', {
515557
name: 'demo',
516-
schemaDefinition: appsync.SchemaDefinition.CODE,
517558
});
518559
api.addType('Demo', {
519560
defintion: {

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

+22-78
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { readFileSync } from 'fs';
21
import { IUserPool } from '@aws-cdk/aws-cognito';
32
import { ManagedPolicy, Role, ServicePrincipal, Grant, IGrantable } from '@aws-cdk/aws-iam';
43
import { CfnResource, Construct, Duration, IResolvable, Stack } from '@aws-cdk/core';
54
import { CfnApiKey, CfnGraphQLApi, CfnGraphQLSchema } from './appsync.generated';
65
import { IGraphqlApi, GraphqlApiBase } from './graphqlapi-base';
7-
import { ObjectType, ObjectTypeProps } from './schema-intermediate';
6+
import { Schema } from './schema';
7+
import { IIntermediateType } from './schema-base';
88

99
/**
1010
* enum with all possible values for AppSync authorization type
@@ -201,21 +201,6 @@ export interface LogConfig {
201201
readonly fieldLogLevel?: FieldLogLevel;
202202
}
203203

204-
/**
205-
* Enum containing the different modes of schema definition
206-
*/
207-
export enum SchemaDefinition {
208-
/**
209-
* Define schema through functions like addType, addQuery, etc.
210-
*/
211-
CODE = 'CODE',
212-
213-
/**
214-
* Define schema in a file, i.e. schema.graphql
215-
*/
216-
FILE = 'FILE',
217-
}
218-
219204
/**
220205
* Properties for an AppSync GraphQL API
221206
*/
@@ -242,18 +227,13 @@ export interface GraphQLApiProps {
242227
/**
243228
* GraphQL schema definition. Specify how you want to define your schema.
244229
*
245-
* SchemaDefinition.CODE allows schema definition through CDK
246-
* SchemaDefinition.FILE allows schema definition through schema.graphql file
230+
* Schema.fromFile(filePath: string) allows schema definition through schema.graphql file
247231
*
248-
* @experimental
249-
*/
250-
readonly schemaDefinition: SchemaDefinition;
251-
/**
252-
* File containing the GraphQL schema definition. You have to specify a definition or a file containing one.
232+
* @default - schema will be generated code-first (i.e. addType, addObjectType, etc.)
253233
*
254-
* @default - Use schemaDefinition
234+
* @experimental
255235
*/
256-
readonly schemaDefinitionFile?: string;
236+
readonly schema?: Schema;
257237
/**
258238
* A flag indicating whether or not X-Ray tracing is enabled for the GraphQL API.
259239
*
@@ -390,9 +370,9 @@ export class GraphQLApi extends GraphqlApiBase {
390370
public readonly name: string;
391371

392372
/**
393-
* underlying CFN schema resource
373+
* the schema attached to this api
394374
*/
395-
public readonly schema: CfnGraphQLSchema;
375+
public readonly schema: Schema;
396376

397377
/**
398378
* the configured API key, if present
@@ -401,9 +381,9 @@ export class GraphQLApi extends GraphqlApiBase {
401381
*/
402382
public readonly apiKey?: string;
403383

404-
private schemaMode: SchemaDefinition;
384+
private schemaResource: CfnGraphQLSchema;
405385
private api: CfnGraphQLApi;
406-
private _apiKey?: CfnApiKey;
386+
private apiKeyResource?: CfnApiKey;
407387

408388
constructor(scope: Construct, id: string, props: GraphQLApiProps) {
409389
super(scope, id);
@@ -429,16 +409,16 @@ export class GraphQLApi extends GraphqlApiBase {
429409
this.arn = this.api.attrArn;
430410
this.graphQlUrl = this.api.attrGraphQlUrl;
431411
this.name = this.api.name;
432-
this.schemaMode = props.schemaDefinition;
433-
this.schema = this.defineSchema(props.schemaDefinitionFile);
412+
this.schema = props.schema ?? new Schema();
413+
this.schemaResource = this.schema.bind(this);
434414

435415
if (modes.some((mode) => mode.authorizationType === AuthorizationType.API_KEY)) {
436416
const config = modes.find((mode: AuthorizationMode) => {
437417
return mode.authorizationType === AuthorizationType.API_KEY && mode.apiKeyConfig;
438418
})?.apiKeyConfig;
439-
this._apiKey = this.createAPIKey(config);
440-
this._apiKey.addDependsOn(this.schema);
441-
this.apiKey = this._apiKey.attrApiKey;
419+
this.apiKeyResource = this.createAPIKey(config);
420+
this.apiKeyResource.addDependsOn(this.schemaResource);
421+
this.apiKey = this.apiKeyResource.attrApiKey;
442422
}
443423
}
444424

@@ -515,7 +495,7 @@ export class GraphQLApi extends GraphqlApiBase {
515495
* @param construct the dependee
516496
*/
517497
public addSchemaDependency(construct: CfnResource): boolean {
518-
construct.addDependsOn(this.schema);
498+
construct.addDependsOn(this.schemaResource);
519499
return true;
520500
}
521501

@@ -584,29 +564,6 @@ export class GraphQLApi extends GraphqlApiBase {
584564
});
585565
}
586566

587-
/**
588-
* Define schema based on props configuration
589-
* @param file the file name/s3 location of Schema
590-
*/
591-
private defineSchema(file?: string): CfnGraphQLSchema {
592-
let definition;
593-
594-
if ( this.schemaMode === SchemaDefinition.FILE && !file) {
595-
throw new Error('schemaDefinitionFile must be configured if using FILE definition mode.');
596-
} else if ( this.schemaMode === SchemaDefinition.FILE && file ) {
597-
definition = readFileSync(file).toString('utf-8');
598-
} else if ( this.schemaMode === SchemaDefinition.CODE && !file ) {
599-
definition = '';
600-
} else if ( this.schemaMode === SchemaDefinition.CODE && file) {
601-
throw new Error('definition mode CODE is incompatible with file definition. Change mode to FILE/S3 or unconfigure schemaDefinitionFile');
602-
}
603-
604-
return new CfnGraphQLSchema(this, 'Schema', {
605-
apiId: this.apiId,
606-
definition,
607-
});
608-
}
609-
610567
/**
611568
* Escape hatch to append to Schema as desired. Will always result
612569
* in a newline.
@@ -617,31 +574,18 @@ export class GraphQLApi extends GraphqlApiBase {
617574
*
618575
* @experimental
619576
*/
620-
public appendToSchema(addition: string, delimiter?: string): void {
621-
if ( this.schemaMode !== SchemaDefinition.CODE ) {
622-
throw new Error('API cannot append to schema because schema definition mode is not configured as CODE.');
623-
}
624-
const sep = delimiter ?? '';
625-
this.schema.definition = `${this.schema.definition}${sep}${addition}\n`;
577+
public addToSchema(addition: string, delimiter?: string): void {
578+
this.schema.addToSchema(addition, delimiter);
626579
}
627580

628581
/**
629-
* Add an object type to the schema
582+
* Add type to the schema
630583
*
631-
* @param name the name of the object type
632-
* @param props the definition
584+
* @param type the intermediate type to add to the schema
633585
*
634586
* @experimental
635587
*/
636-
public addType(name: string, props: ObjectTypeProps): ObjectType {
637-
if ( this.schemaMode !== SchemaDefinition.CODE ) {
638-
throw new Error('API cannot add type because schema definition mode is not configured as CODE.');
639-
};
640-
const type = new ObjectType(name, {
641-
definition: props.definition,
642-
directives: props.directives,
643-
});
644-
this.appendToSchema(type.toString());
645-
return type;
588+
public addType(type: IIntermediateType): IIntermediateType {
589+
return this.schema.addType(type);
646590
}
647591
}

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

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from './key';
44
export * from './data-source';
55
export * from './mapping-template';
66
export * from './resolver';
7+
export * from './schema';
78
export * from './schema-intermediate';
89
export * from './schema-field';
910
export * from './schema-base';

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

+8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ function concatAndDedup<T>(left: T[], right: T[]): T[] {
44
});
55
}
66

7+
/**
8+
* Utility enum for Schema class
9+
*/
10+
export enum SchemaMode {
11+
FILE = 'FILE',
12+
CODE = 'CODE',
13+
};
14+
715
/**
816
* Utility class to represent DynamoDB key conditions.
917
*/

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

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Resolver } from './resolver';
2-
import { ResolvableFieldOptions } from './schema-field';
2+
import { ResolvableFieldOptions, BaseTypeOptions, GraphqlType } from './schema-field';
33
import { InterfaceType } from './schema-intermediate';
44

55
/**
@@ -105,6 +105,16 @@ export interface IIntermediateType {
105105
*/
106106
readonly intermediateType?: InterfaceType;
107107

108+
/**
109+
* Create an GraphQL Type representing this Intermediate Type
110+
*
111+
* @param options the options to configure this attribute
112+
* - isList
113+
* - isRequired
114+
* - isRequiredList
115+
*/
116+
attribute(options?: BaseTypeOptions): GraphqlType;
117+
108118
/**
109119
* Generate the string of this object type
110120
*/

0 commit comments

Comments
 (0)