diff --git a/packages/@aws-cdk/aws-appsync/README.md b/packages/@aws-cdk/aws-appsync/README.md index 5ab39d8ec6375..a771fb43243fa 100644 --- a/packages/@aws-cdk/aws-appsync/README.md +++ b/packages/@aws-cdk/aws-appsync/README.md @@ -367,6 +367,21 @@ 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`. +#### Directives + +`Directives` are attached to a field or type and affect the execution of queries, +mutations, and types. With AppSync, we use `Directives` to configure authorization. +CDK provides static functions to add directives to your Schema. + +- `Directive.iam()` sets a type or field's authorization to be validated through `Iam` +- `Directive.apiKey()` sets a type or field's authorization to be validated through a `Api Key` +- `Directive.oidc()` sets a type or field's authorization to be validated through `OpenID Connect` +- `Directive.cognito(...groups: string[])` sets a type or field's authorization to be validated +through `Cognito User Pools` + - `groups` the name of the cognito groups to give access + +To learn more about authorization and directives, read these docs [here](https://docs.aws.amazon.com/appsync/latest/devguide/security.html). + #### Field and Resolvable Fields While `GraphqlType` is a base implementation for GraphQL fields, we have abstractions @@ -548,21 +563,7 @@ You can create Object Types in three ways: }, }); ``` - > This method allows for reusability and modularity, ideal for reducing code duplication. - -3. Object Types can be created ***internally*** within the GraphQL API. - ```ts - const api = new appsync.GraphqlApi(stack, 'Api', { - name: 'demo', - }); - api.addType('Demo', { - defintion: { - id: appsync.GraphqlType.string({ isRequired: true }), - version: appsync.GraphqlType.string({ isRequired: true }), - }, - }); - ``` - > This method provides easy use and is ideal for smaller projects. + > This method allows for reusability and modularity, ideal for reducing code duplication. ##### Input Types diff --git a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts index 052e3ecd53392..f3ae3546ff130 100644 --- a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts +++ b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts @@ -376,6 +376,11 @@ export class GraphqlApi extends GraphqlApiBase { */ public readonly schema: Schema; + /** + * The Authorization Types for this GraphQL Api + */ + public readonly modes: AuthorizationType[]; + /** * the configured API key, if present * @@ -395,6 +400,8 @@ export class GraphqlApi extends GraphqlApiBase { const additionalModes = props.authorizationConfig?.additionalAuthorizationModes ?? []; const modes = [defaultMode, ...additionalModes]; + this.modes = modes.map((mode) => mode.authorizationType ); + this.validateAuthorizationProps(modes); this.api = new CfnGraphQLApi(this, 'Resource', { diff --git a/packages/@aws-cdk/aws-appsync/lib/private.ts b/packages/@aws-cdk/aws-appsync/lib/private.ts index 25e99fa93b753..981076113be6e 100644 --- a/packages/@aws-cdk/aws-appsync/lib/private.ts +++ b/packages/@aws-cdk/aws-appsync/lib/private.ts @@ -1,3 +1,4 @@ +import { AuthorizationType } from './graphqlapi'; import { Directive } from './schema-base'; import { InterfaceType } from './schema-intermediate'; @@ -47,6 +48,10 @@ export interface SchemaAdditionOptions { * the fields to reduce onto the addition */ readonly fields: string[]; + /** + * the authorization modes for this graphql type + */ + readonly modes?: AuthorizationType[]; } /** @@ -67,7 +72,10 @@ export interface SchemaAdditionOptions { export function shapeAddition(options: SchemaAdditionOptions): string { const typeName = (): string => { return options.name ? ` ${options.name}` : ''; }; const interfaces = generateInterfaces(options.interfaceTypes); - const directives = generateDirectives(options.directives); + const directives = generateDirectives({ + directives: options.directives, + modes: options.modes, + }); return options.fields.reduce((acc, field) => `${acc} ${field}\n`, `${options.prefix}${typeName()}${interfaces}${directives} {\n`) + '}'; } @@ -197,14 +205,33 @@ function generateInterfaces(interfaceTypes?: InterfaceType[]): string { `${acc} ${interfaceType.name},`, ' implements').slice(0, -1); } +/** + * options to generate directives + */ +interface generateDirectivesOptions { + /** + * the directives of a given type + */ + readonly directives?: Directive[]; + /** + * thee separator betweeen directives + * + * @default - a space + */ + readonly delimiter?: string; + /** + * the authorization modes + */ + readonly modes?: AuthorizationType[]; +} + /** * Utility function to generate directives - * - * @param directives the directives of a given type - * @param delimiter the separator betweeen directives (by default we will add a space) */ -function generateDirectives(directives?: Directive[], delimiter?: string): string { - if (!directives || directives.length === 0) return ''; - return directives.reduce((acc, directive) => - `${acc}${directive.statement}${delimiter ?? ' '}`, ' ').slice(0, -1); +function generateDirectives(options: generateDirectivesOptions): string { + if (!options.directives || options.directives.length === 0) return ''; + // reduce over all directives and get string version of the directive + // pass in the auth modes for checks to happen on compile time + return options.directives.reduce((acc, directive) => + `${acc}${directive.toString(options.modes)}${options.delimiter ?? ' '}`, ' ').slice(0, -1); } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/lib/schema-base.ts b/packages/@aws-cdk/aws-appsync/lib/schema-base.ts index 940f21bc9be7e..628cbf80af8c3 100644 --- a/packages/@aws-cdk/aws-appsync/lib/schema-base.ts +++ b/packages/@aws-cdk/aws-appsync/lib/schema-base.ts @@ -1,3 +1,4 @@ +import { AuthorizationType } from './graphqlapi'; import { Resolver } from './resolver'; import { ResolvableFieldOptions, BaseTypeOptions, GraphqlType } from './schema-field'; import { InterfaceType } from './schema-intermediate'; @@ -61,6 +62,13 @@ export interface IField { * Generate the arguments for this field */ argsToString(): string; + + /** + * Generate the directives for this field + * + * @param modes the authorization modes of the graphql api + */ + directivesToString(modes?: AuthorizationType[]): string } /** @@ -141,8 +149,10 @@ export interface IIntermediateType { /** * Generate the string of this object type + * + * @param modes the authorization modes for the graphql api */ - toString(): string; + toString(modes?: AuthorizationType[]): string; /** * Add a field to this Intermediate Type @@ -164,7 +174,37 @@ export class Directive { * Add the @aws_iam directive */ public static iam(): Directive { - return new Directive('@aws_iam'); + return new Directive('@aws_iam', AuthorizationType.IAM); + } + + /** + * Add the @aws_oidc directive + */ + public static oidc(): Directive { + return new Directive('@aws_oidc', AuthorizationType.OIDC); + } + + /** + * Add the @aws_api_key directive + */ + public static apiKey(): Directive { + return new Directive('@aws_api_key', AuthorizationType.API_KEY); + } + + /** + * Add the @aws_auth or @aws_cognito_user_pools directive + * + * @param groups the groups to allow access to + */ + public static cognito(...groups: string[]): Directive { + if (groups.length === 0) { + throw new Error(`Cognito authorization requires at least one Cognito group to be supplied. Received: ${groups.length}`); + } + // 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 new Directive(`@aws_auth(cognito_groups: ${stringify(groups)})`, AuthorizationType.USER_POOL); } /** @@ -179,9 +219,28 @@ export class Directive { /** * the directive statement */ - public readonly statement: string; + private statement: string; - private constructor(statement: string) { this.statement = statement; } + private readonly mode?: AuthorizationType; + + private constructor(statement: string, mode?: AuthorizationType) { + this.statement = statement; + this.mode = mode; + } + + /** + * Generate the directive statement + * @param modes the authorization modes of the graphql api + */ + public toString(modes?: AuthorizationType[]): string { + if (modes && this.mode && !modes.some((mode) => mode === this.mode)) { + throw new Error(`No Authorization Type ${this.mode} declared in GraphQL Api.`); + } + if (this.mode === AuthorizationType.USER_POOL && modes && modes.length > 1) { + this.statement = this.statement.replace('@aws_auth', '@aws_cognito_user_pools'); + } + return this.statement; + } } /** diff --git a/packages/@aws-cdk/aws-appsync/lib/schema-field.ts b/packages/@aws-cdk/aws-appsync/lib/schema-field.ts index ba22dd085fe56..4b5f8c0f3af62 100644 --- a/packages/@aws-cdk/aws-appsync/lib/schema-field.ts +++ b/packages/@aws-cdk/aws-appsync/lib/schema-field.ts @@ -1,6 +1,7 @@ import { BaseDataSource } from './data-source'; +import { AuthorizationType } from './graphqlapi'; import { MappingTemplate } from './mapping-template'; -import { Type, IField, IIntermediateType } from './schema-base'; +import { Type, IField, IIntermediateType, Directive } from './schema-base'; /** * Base options for GraphQL Types @@ -321,6 +322,13 @@ export class GraphqlType implements IField { public argsToString(): string { return ''; } + + /** + * Generate the directives for this field + */ + public directivesToString(_modes?: AuthorizationType[]): string { + return ''; + } } /** @@ -345,6 +353,12 @@ export interface FieldOptions { * @default - no arguments */ readonly args?: { [key: string]: GraphqlType }; + /** + * the directives for this field + * + * @default - no directives + */ + readonly directives?: Directive[]; } /** @@ -375,13 +389,17 @@ export class Field extends GraphqlType implements IField { */ public argsToString(): string { if (!this.fieldOptions || !this.fieldOptions.args) { return ''; } - let args = '('; - Object.keys(this.fieldOptions?.args ?? {}).forEach((key) => { - const type = this.fieldOptions?.args?.[key].toString(); - args = `${args}${key}: ${type} `; - }); - args = args.slice(0, -1); - return `${args})`; + return Object.keys(this.fieldOptions.args).reduce((acc, key) => + `${acc}${key}: ${this.fieldOptions?.args?.[key].toString()} `, '(').slice(0, -1) + ')'; + } + + /** + * Generate the directives for this field + */ + public directivesToString(modes?: AuthorizationType[]): string { + if (!this.fieldOptions || !this.fieldOptions.directives) { return ''; } + return this.fieldOptions.directives.reduce((acc, directive) => + `${acc}${directive.toString(modes)} `, '\n ').slice(0, -1); } } diff --git a/packages/@aws-cdk/aws-appsync/lib/schema-intermediate.ts b/packages/@aws-cdk/aws-appsync/lib/schema-intermediate.ts index 0b243160cc8c1..4f30e506b6406 100644 --- a/packages/@aws-cdk/aws-appsync/lib/schema-intermediate.ts +++ b/packages/@aws-cdk/aws-appsync/lib/schema-intermediate.ts @@ -1,3 +1,4 @@ +import { AuthorizationType } from './graphqlapi'; import { shapeAddition } from './private'; import { Resolver } from './resolver'; import { Directive, IField, IIntermediateType, AddFieldOptions } from './schema-base'; @@ -8,6 +9,7 @@ import { BaseTypeOptions, GraphqlType, ResolvableFieldOptions } from './schema-f * * @param definition - the variables and types that define this type * i.e. { string: GraphqlType, string: GraphqlType } + * @param directives - the directives for this object type * * @experimental */ @@ -16,6 +18,12 @@ export interface IntermediateTypeProps { * the attributes of this type */ readonly definition: { [key: string]: IField }; + /** + * the directives for this object type + * + * @default - no directives + */ + readonly directives?: Directive[]; } /** @@ -33,10 +41,17 @@ export class InterfaceType implements IIntermediateType { * the attributes of this type */ public readonly definition: { [key: string]: IField }; + /** + * the directives for this object type + * + * @default - no directives + */ + public readonly directives?: Directive[]; public constructor(name: string, props: IntermediateTypeProps) { this.name = name; this.definition = props.definition; + this.directives = props.directives; } /** @@ -59,12 +74,16 @@ export class InterfaceType implements IIntermediateType { /** * Generate the string of this object type */ - public toString(): string { + public toString(modes?: AuthorizationType[]): string { return shapeAddition({ prefix: 'interface', name: this.name, - fields: Object.keys(this.definition).map((key) => - `${key}${this.definition[key].argsToString()}: ${this.definition[key].toString()}`), + directives: this.directives, + fields: Object.keys(this.definition).map((key) => { + const field = this.definition[key]; + return `${key}${field.argsToString()}: ${field.toString()}${field.directivesToString(modes)}`; + }), + modes, }); } @@ -100,12 +119,6 @@ export interface ObjectTypeProps extends IntermediateTypeProps { * @default - no interface types */ readonly interfaceTypes?: InterfaceType[]; - /** - * the directives for this object type - * - * @default - no directives - */ - readonly directives?: Directive[]; } /** @@ -120,12 +133,6 @@ export class ObjectType extends InterfaceType implements IIntermediateType { * @default - no interface types */ public readonly interfaceTypes?: InterfaceType[]; - /** - * the directives for this object type - * - * @default - no directives - */ - public readonly directives?: Directive[]; /** * The resolvers linked to this data source */ @@ -136,10 +143,10 @@ export class ObjectType extends InterfaceType implements IIntermediateType { definition: props.interfaceTypes?.reduce((def, interfaceType) => { return Object.assign({}, def, interfaceType.definition); }, props.definition) ?? props.definition, + directives: props.directives, }; super(name, options); this.interfaceTypes = props.interfaceTypes; - this.directives = props.directives; this.resolvers = []; Object.keys(this.definition).forEach((fieldName) => { @@ -167,14 +174,17 @@ export class ObjectType extends InterfaceType implements IIntermediateType { /** * Generate the string of this object type */ - public toString(): string { + public toString(modes?: AuthorizationType[]): string { return shapeAddition({ prefix: 'type', name: this.name, interfaceTypes: this.interfaceTypes, directives: this.directives, - fields: Object.keys(this.definition).map((key) => - `${key}${this.definition[key].argsToString()}: ${this.definition[key].toString()}`), + fields: Object.keys(this.definition).map((key) => { + const field = this.definition[key]; + return `${key}${field.argsToString()}: ${field.toString()}${field.directivesToString(modes)}`; + }), + modes, }); } @@ -182,16 +192,15 @@ export class ObjectType extends InterfaceType implements IIntermediateType { * Generate the resolvers linked to this Object Type */ protected generateResolver(fieldName: string, options?: ResolvableFieldOptions): void { - if (options?.dataSource) { - if (!this.resolvers) { this.resolvers = []; } - this.resolvers.push(options.dataSource.createResolver({ - typeName: this.name, - fieldName: fieldName, - pipelineConfig: options.pipelineConfig, - requestMappingTemplate: options.requestMappingTemplate, - responseMappingTemplate: options.responseMappingTemplate, - })); - } + if (!options?.dataSource) return; + if (!this.resolvers) { this.resolvers = []; } + this.resolvers.push(options.dataSource.createResolver({ + typeName: this.name, + fieldName: fieldName, + pipelineConfig: options.pipelineConfig, + requestMappingTemplate: options.requestMappingTemplate, + responseMappingTemplate: options.responseMappingTemplate, + })); } } @@ -236,7 +245,7 @@ export class InputType implements IIntermediateType { /** * Generate the string of this input type */ - public toString(): string { + public toString(_modes?: AuthorizationType[]): string { return shapeAddition({ prefix: 'input', name: this.name, diff --git a/packages/@aws-cdk/aws-appsync/lib/schema.ts b/packages/@aws-cdk/aws-appsync/lib/schema.ts index 9f1a2e9d2988a..53b77ea408b8e 100644 --- a/packages/@aws-cdk/aws-appsync/lib/schema.ts +++ b/packages/@aws-cdk/aws-appsync/lib/schema.ts @@ -56,6 +56,8 @@ export class Schema { private mode: SchemaMode; + private types: IIntermediateType[]; + public constructor(options?: SchemaOptions) { if (options?.filePath) { this.mode = SchemaMode.FILE; @@ -64,6 +66,7 @@ export class Schema { this.mode = SchemaMode.CODE; this.definition = ''; } + this.types = []; } /** @@ -76,7 +79,12 @@ export class Schema { if (!this.schema) { this.schema = new CfnGraphQLSchema(api, 'Schema', { apiId: api.apiId, - definition: Lazy.stringValue({ produce: () => `${this.declareSchema()}${this.definition}` }), + definition: this.mode === SchemaMode.CODE ? + Lazy.stringValue({ + produce: () => this.types.reduce((acc, type) => { return `${acc}${type.toString(api.modes)}\n`; }, + `${this.declareSchema()}${this.definition}`), + }) + : this.definition, }); } return this.schema; @@ -157,7 +165,7 @@ export class Schema { if (this.mode !== SchemaMode.CODE) { throw new Error('API cannot add type because schema definition mode is not configured as CODE.'); } - this.addToSchema(Lazy.stringValue({ produce: () => type.toString() })); + this.types.push(type); return type; } diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-directives.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-directives.test.ts new file mode 100644 index 0000000000000..3c47e0b63cf6d --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/appsync-directives.test.ts @@ -0,0 +1,140 @@ +import '@aws-cdk/assert/jest'; +import * as cdk from '@aws-cdk/core'; +import * as cognito from '@aws-cdk/aws-cognito'; +import * as appsync from '../lib'; +import * as t from './scalar-type-defintions'; + +const iam = [appsync.Directive.iam()]; +const apiKey = [appsync.Directive.apiKey()]; +const oidc = [appsync.Directive.oidc()]; +const cognito_default = [appsync.Directive.cognito('test', 'test2')]; +const cognito_additional = [appsync.Directive.cognito('test', 'test2')]; +const custom = [appsync.Directive.custom('custom')]; + +const generateField = (directives: appsync.Directive[]): appsync.Field => { + return new appsync.Field({ + returnType: t.string, + directives, + }); +}; + +const generateRField = (directives: appsync.Directive[]): appsync.ResolvableField => { + return new appsync.ResolvableField({ + returnType: t.string, + directives, + }); +}; + +let stack: cdk.Stack; + +let api_apiKey: appsync.GraphqlApi, api_iam: appsync.GraphqlApi, api_oidc: appsync.GraphqlApi, + api_auth: appsync.GraphqlApi, api_cognito: appsync.GraphqlApi; +beforeEach(() => { + // GIVEN + stack = new cdk.Stack(); + const userPool = new cognito.UserPool(stack, 'userpool'); + api_apiKey = new appsync.GraphqlApi(stack, 'api_apiKey', { + name: 'api', + }); + api_iam = new appsync.GraphqlApi(stack, 'api_iam', { + name: 'api', + authorizationConfig: { + defaultAuthorization: { + authorizationType: appsync.AuthorizationType.IAM, + }, + }, + }); + api_oidc = new appsync.GraphqlApi(stack, 'api_oidc', { + name: 'api', + authorizationConfig: { + defaultAuthorization: { + authorizationType: appsync.AuthorizationType.OIDC, + openIdConnectConfig: { oidcProvider: 'test' }, + }, + }, + }); + api_auth = new appsync.GraphqlApi(stack, 'api_cognito_default', { + name: 'api', + authorizationConfig: { + defaultAuthorization: { + authorizationType: appsync.AuthorizationType.USER_POOL, + userPoolConfig: { userPool }, + }, + }, + }); + api_cognito = new appsync.GraphqlApi(stack, 'api_cognito_additional', { + name: 'api', + authorizationConfig: { + additionalAuthorizationModes: [ + { + authorizationType: appsync.AuthorizationType.USER_POOL, + userPoolConfig: { userPool }, + }, + ], + }, + }); +}); + +const testObjectType = (IApi: appsync.GraphqlApi, directives: appsync.Directive[], tag: string): any => { + // WHEN + IApi.addType(new appsync.ObjectType('Test', { + definition: { + field: generateField(directives), + rfield: generateRField(directives), + }, + directives: directives, + })); + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `type Test ${tag} {\n field: String\n ${tag}\n rfield: String\n ${tag}\n}\n`, + }); +}; + +const testInterfaceType = (IApi: appsync.GraphqlApi, directives: appsync.Directive[], tag: string): any => { + // WHEN + IApi.addType(new appsync.InterfaceType('Test', { + definition: { + field: generateField(directives), + rfield: generateRField(directives), + }, + directives: directives, + })); + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `interface Test ${tag} {\n field: String\n ${tag}\n rfield: String\n ${tag}\n}\n`, + }); +}; + +describe('Basic Testing of Directives for Code-First', () => { + test('Iam directive configures in Object Type', () => { testObjectType(api_iam, iam, '@aws_iam'); }); + + test('Iam directive configures in Interface Type', () => { testInterfaceType(api_iam, iam, '@aws_iam'); }); + + test('Api Key directive configures in Object Type', () => { testObjectType(api_apiKey, apiKey, '@aws_api_key'); }); + + test('Api Key directive configures in Interface Type', () => { testInterfaceType(api_apiKey, apiKey, '@aws_api_key'); }); + + test('OIDC directive configures in Object Type', () => { testObjectType(api_oidc, oidc, '@aws_oidc'); }); + + test('OIDC directive configures in Interface Type', () => { testInterfaceType(api_oidc, oidc, '@aws_oidc'); }); + + test('Cognito as default directive configures in Object Type', () => { + testObjectType(api_auth, cognito_default, '@aws_auth(cognito_groups: ["test", "test2"])'); + }); + + test('Cognito as default directive configures in Interface Type', () => { + testInterfaceType(api_auth, cognito_default, '@aws_auth(cognito_groups: ["test", "test2"])'); + }); + + test('Cognito as additional directive configures in Object Type', () => { + testObjectType(api_cognito, cognito_additional, '@aws_cognito_user_pools(cognito_groups: ["test", "test2"])'); + }); + + test('Custom directive configures in Object Type', () => { + testObjectType(api_cognito, custom, 'custom'); + }); + + test('Custom directive configures in Interface Type', () => { + testInterfaceType(api_cognito, custom, 'custom'); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-interface-type.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-interface-type.test.ts index 15525bdafe098..89a578fc0bd0b 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync-interface-type.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync-interface-type.test.ts @@ -89,6 +89,56 @@ describe('testing InterfaceType properties', () => { }); }); + test('Interface Type can generate Fields with Directives', () => { + // WHEN + const test = new appsync.InterfaceType('Test', { + definition: { + test: t.string, + }, + }); + test.addField({ + fieldName: 'resolve', + field: new appsync.Field({ + returnType: t.string, + directives: [appsync.Directive.apiKey()], + }), + }); + + api.addType(test); + const out = 'interface Test {\n test: String\n resolve: String\n @aws_api_key\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('Interface Type can generate ResolvableFields with Directives, but not the resolver', () => { + // WHEN + const test = new appsync.InterfaceType('Test', { + definition: { + test: t.string, + }, + }); + test.addField({ + fieldName: 'resolve', + field: new appsync.ResolvableField({ + returnType: t.string, + directives: [appsync.Directive.apiKey()], + dataSource: api.addNoneDataSource('none'), + }), + }); + + api.addType(test); + const out = 'interface Test {\n test: String\n resolve: String\n @aws_api_key\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + expect(stack).not.toHaveResource('AWS::AppSync::Resolver'); + }); + test('appsync fails addField with InterfaceType missing fieldName', () => { // WHEN const test = new appsync.InterfaceType('Test', { definition: {} }); diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-object-type.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-object-type.test.ts index 9f18d68e4753d..77da20a58ca4c 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync-object-type.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync-object-type.test.ts @@ -29,8 +29,8 @@ describe('testing Object Type properties', () => { directives: [appsync.Directive.custom('@test')], }); - api.addToSchema(baseTest.toString()); - api.addToSchema(objectTest.toString()); + api.addType(baseTest); + api.addType(objectTest); const gql_interface = 'interface baseTest {\n id: ID\n}\n'; const gql_object = 'type objectTest implements baseTest @test {\n id2: ID\n id: ID\n}\n'; const out = `${gql_interface}${gql_object}`; @@ -56,9 +56,9 @@ describe('testing Object Type properties', () => { }, }); - api.addToSchema(baseTest.toString()); - api.addToSchema(anotherTest.toString()); - api.addToSchema(objectTest.toString()); + api.addType(baseTest); + api.addType(anotherTest); + api.addType(objectTest); const gql_interface = 'interface baseTest {\n id: ID\n}\ninterface anotherTest {\n id2: ID\n}\n'; const gql_object = 'type objectTest implements anotherTest, baseTest {\n id3: ID\n id2: ID\n id: ID\n}\n'; @@ -83,7 +83,7 @@ describe('testing Object Type properties', () => { test: graphqlType, }, }); - api.addToSchema(test.toString()); + api.addType(test); const out = 'type Test {\n test: baseTest\n}\n'; // THEN @@ -107,7 +107,7 @@ describe('testing Object Type properties', () => { resolve: field, }, }); - api.addToSchema(test.toString()); + api.addType(test); const out = 'type Test {\n test: String\n resolve(arg: Int): String\n}\n'; // THEN @@ -131,7 +131,7 @@ describe('testing Object Type properties', () => { resolve: field, }, }); - api.addToSchema(test.toString()); + api.addType(test); const out = 'type Test {\n test: String\n resolve(arg: Int): String\n}\n'; // THEN @@ -154,7 +154,7 @@ describe('testing Object Type properties', () => { }), }, }); - api.addToSchema(test.toString()); + api.addType(test); // THEN expect(stack).toHaveResourceLike('AWS::AppSync::Resolver', { @@ -168,9 +168,7 @@ describe('testing Object Type properties', () => { const field = new appsync.ResolvableField({ returnType: t.string, dataSource: api.addNoneDataSource('none'), - args: { - arg: t.int, - }, + args: { arg: t.int }, }); const test = new appsync.ObjectType('Test', { definition: { @@ -181,7 +179,7 @@ describe('testing Object Type properties', () => { // test.addField('resolve', field); test.addField({ fieldName: 'dynamic', field: t.string }); - api.addToSchema(test.toString()); + api.addType(test); const out = 'type Test {\n test: String\n resolve(arg: Int): String\n dynamic: String\n}\n'; // THEN @@ -191,31 +189,49 @@ describe('testing Object Type properties', () => { expect(stack).toHaveResource('AWS::AppSync::Resolver'); }); - test('Object Type can dynamically add Fields', () => { + test('Object Type can generate Fields with Directives', () => { // WHEN - const garbage = new appsync.InterfaceType('Garbage', { + const test = new appsync.ObjectType('Test', { definition: { - garbage: t.string, + test: t.string, }, }); + test.addField({ + fieldName: 'resolve', + field: new appsync.Field({ + returnType: t.string, + directives: [appsync.Directive.apiKey()], + }), + }); + + api.addType(test); + const out = 'type Test {\n test: String\n resolve: String\n @aws_api_key\n}\n'; + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { + Definition: `${out}`, + }); + }); + + test('Object Type can generate ResolvableFields with Directives', () => { + // WHEN const test = new appsync.ObjectType('Test', { definition: { test: t.string, }, }); const field = new appsync.ResolvableField({ - returnType: garbage.attribute(), + returnType: t.string, + directives: [appsync.Directive.apiKey()], dataSource: api.addNoneDataSource('none'), args: { - arg: garbage.attribute(), + arg: t.string, }, }); test.addField({ fieldName: 'resolve', field }); - // test.addField('resolve', field); - test.addField({ fieldName: 'dynamic', field: garbage.attribute() }); - api.addToSchema(test.toString()); - const out = 'type Test {\n test: String\n resolve(arg: Garbage): Garbage\n dynamic: Garbage\n}\n'; + api.addType(test); + const out = 'type Test {\n test: String\n resolve(arg: String): String\n @aws_api_key\n}\n'; // THEN expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLSchema', { diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql-schema.ts b/packages/@aws-cdk/aws-appsync/test/integ.graphql-schema.ts index 870af80a51e06..2e36caec12b2c 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.graphql-schema.ts +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql-schema.ts @@ -21,13 +21,15 @@ const stack = new cdk.Stack(app, 'code-first-schema'); const schema = new appsync.Schema(); -const node = schema.addType(new appsync.InterfaceType('Node', { +const node = new appsync.InterfaceType('Node', { definition: { created: ScalarType.string, edited: ScalarType.string, id: ScalarType.required_id, }, -})); +}); + +schema.addType(node); const api = new appsync.GraphqlApi(stack, 'code-first-api', { name: 'api', @@ -44,7 +46,7 @@ const table = new db.Table(stack, 'table', { const tableDS = api.addDynamoDbDataSource('planets', table); const planet = ObjectType.planet; -schema.addToSchema(planet.toString()); +schema.addType(planet); api.addType(new appsync.ObjectType('Species', { interfaceTypes: [node],