diff --git a/docs/constructs/assets/hierarchy.js b/docs/constructs/assets/hierarchy.js index 48e4dc78..3c465a21 100644 --- a/docs/constructs/assets/hierarchy.js +++ b/docs/constructs/assets/hierarchy.js @@ -1 +1 @@ -window.hierarchyData = "eJyVk7tuwjAUht/lzKYIfEu8QStVbL1tiMENhlg1NrLNhPLulSlt3RLlsmRI/pPv/5LjM3jnYgCxLjBiHM1KRBneIPBqZ1QVtbMBxBlmZbpaeVAgYHXv9lZHtzjFWtmoK5lygOBD2y2IOWUITt6AAG2j8jtZqTBtH7qr48EAgsrIEEBADNtJesvkZzI9rLXZemVBrAnbNAgIy9p0l5nNi+8yF4YK03FFvm40CBjPoEsZ1OKon7w7hh7xPDpWF2OKCL84F/gW32V5jfR4/aUxgkhZJBrGNMM9enmsn81A4X/pAc4JyEgrsEvxN9X/9wjPd+ZFhTjQJo8OUyFlcYvq8rhG+iUoy5dg9art3qg3+W7Ug4wyROdV3ylsmxm7lZTztCOU5yeis0yL9Kgi1y/QNJ/jTozm" \ No newline at end of file +window.hierarchyData = "eJyVk0tzwiAUhf/LXWM1JhBkZ9uZTnd97RwXNF4NUwQHcOXkv3ewtqU1k8eGBZx7z/mAewJnbfAgVjwnrCTZgtAyWxNwuNVYBWWNB3GCbBFXI/cIAh7v7M6oYJfHUKMJqpJRBwQ+lNmAmFNG4Og0CFAmoNvKCv20veimDnsNBCotvQcBwW8mscvkpzIe1kpvHBoQq4KtGwIFS9J0h8nm/DvM2QP9dFyQr42GACsT01vpcXlQT84efA94Kh2Lm+eUFLyIzDy/tu+ivEh6uP66MUro7HzDeU4TuwcnD/WzHgj8Tz2AORqydsMuxF9V/+sVvEj6v6APA2lS6TAUOmPXVl0cF0k/BC2zdAxfldlpfJPvGu9lkD5Yh31T2FYz9ldSTuMfoTx9ss4wLdCjglxuoGk+AbUQjM4=" \ No newline at end of file diff --git a/docs/constructs/assets/search.js b/docs/constructs/assets/search.js index a3d9fd69..f19edfc2 100644 --- a/docs/constructs/assets/search.js +++ b/docs/constructs/assets/search.js @@ -1 +1 @@ -window.searchData = "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"; \ No newline at end of file +window.searchData = "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"; \ No newline at end of file diff --git a/docs/constructs/classes/GraphQlApi.html b/docs/constructs/classes/GraphQlApi.html index 8f357e1d..bcd5d994 100644 --- a/docs/constructs/classes/GraphQlApi.html +++ b/docs/constructs/classes/GraphQlApi.html @@ -5,7 +5,7 @@

Type Parameters

Hierarchy (View Summary)

Index

Constructors

Hierarchy (View Summary)

Index

Constructors

Properties

api apiDomainName? apiFQDN? diff --git a/docs/constructs/classes/RestApi.html b/docs/constructs/classes/RestApi.html index ab143c7f..04752fc6 100644 --- a/docs/constructs/classes/RestApi.html +++ b/docs/constructs/classes/RestApi.html @@ -6,7 +6,7 @@

Type Parameters

  • PATHS

    The type definition for the API paths.

  • OPS

    The type definition for the API operations.

    -

Hierarchy (View Summary)

Index

Constructors

Hierarchy (View Summary)

Index

Constructors

Properties

api apiDomainName? apiFQDN? diff --git a/docs/constructs/classes/SingleTableDatastore.html b/docs/constructs/classes/SingleTableDatastore.html index d40974f0..f40026a7 100644 --- a/docs/constructs/classes/SingleTableDatastore.html +++ b/docs/constructs/classes/SingleTableDatastore.html @@ -4,7 +4,7 @@
const datastore = new SingleTableDatastore(this, 'MyDatastore', {
design: {
primaryKey: {
partitionKey: 'PK',
sortKey: 'SK',
},
globalIndexes: [
{
indexName: 'GSI1',
partitionKey: { name: 'GSI1PK', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'GSI1SK', type: dynamodb.AttributeType.STRING },
},
],
localIndexes: [
{
indexName: 'LSI1',
sortKey: { name: 'LSI1SK', type: dynamodb.AttributeType.STRING },
},
],
timeToLiveAttribute: 'TTL',
},
encryption: dynamodb.TableEncryption.AWS_MANAGED,
});
-

Hierarchy

  • Construct
    • SingleTableDatastore

Implements

Index

Constructors

Hierarchy

  • Construct
    • SingleTableDatastore

Implements

Index

Constructors

Properties

Methods

toString diff --git a/docs/constructs/classes/Workflow.html b/docs/constructs/classes/Workflow.html index 892f2075..08d4e2e2 100644 --- a/docs/constructs/classes/Workflow.html +++ b/docs/constructs/classes/Workflow.html @@ -4,7 +4,7 @@
const workflow = new Workflow(this, 'MyWorkflow', {
definitionFileName: 'path/to/definition.asl.json',
stateMachineType: sfn.StateMachineType.STANDARD,
loggingConfiguration: {
level: sfn.LogLevel.ALL,
includeExecutionData: true,
destinations: [new logs.LogGroup(this, 'LogGroup')],
},
tracingConfiguration: {
enabled: true,
},
definitionSubstitutions: {
'${MyVariable}': 'MyValue',
},
});

const lambdaFunction = new lambda.Function(this, 'MyFunction', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda'),
});

workflow.grantPrincipal.grantInvoke(lambdaFunction);
-

Hierarchy

  • Construct
    • Workflow

Implements

  • IGrantable
Index

Constructors

Hierarchy

  • Construct
    • Workflow

Implements

  • IGrantable
Index

Constructors

Properties

grantPrincipal node role diff --git a/docs/constructs/hierarchy.html b/docs/constructs/hierarchy.html index 5fef70ef..729ab6d0 100644 --- a/docs/constructs/hierarchy.html +++ b/docs/constructs/hierarchy.html @@ -1 +1 @@ -cdk-serverless
cdk-serverless
    Preparing search index...
    +cdk-serverless
    cdk-serverless
      Preparing search index...
      diff --git a/docs/constructs/interfaces/GraphQlApiProps.html b/docs/constructs/interfaces/GraphQlApiProps.html index e3bb0bf3..e26e6769 100644 --- a/docs/constructs/interfaces/GraphQlApiProps.html +++ b/docs/constructs/interfaces/GraphQlApiProps.html @@ -1,4 +1,4 @@ -GraphQlApiProps | cdk-serverless
      cdk-serverless
        Preparing search index...

        Interface GraphQlApiProps

        interface GraphQlApiProps {
            additionalEnv?: { [key: string]: string };
            apiHostname?: string;
            apiName: string;
            assetCdn?: AssetCdn;
            authentication?: IJwtAuthentication | ICognitoAuthentication;
            definitionFileName: string;
            domainName?: string;
            hostedZone?: IHostedZone;
            lambdaOptions?: LambdaOptions;
            lambdaTracing?: LambdaTracingOptions;
            monitoring?: boolean;
            singleTableDatastore?: ISingleTableDatastore;
            stageName: string;
        }

        Hierarchy (View Summary)

        Index

        Properties

        additionalEnv? +GraphQlApiProps | cdk-serverless
        cdk-serverless
          Preparing search index...

          Interface GraphQlApiProps

          interface GraphQlApiProps {
              additionalEnv?: { [key: string]: string };
              apiHostname?: string;
              apiName: string;
              assetCdn?: AssetCdn;
              authentication?: IJwtAuthentication | ICognitoAuthentication;
              definitionFileName: string;
              domainName?: string;
              hostedZone?: IHostedZone;
              lambdaOptions?: LambdaOptions;
              lambdaTracing?: LambdaTracingOptions;
              monitoring?: boolean;
              singleTableDatastore?: ISingleTableDatastore;
              stageName: string;
              useNestedStacks?: boolean;
          }

          Hierarchy (View Summary)

          Index

          Properties

          additionalEnv?: { [key: string]: string }

          Additional environment variables of all Lambda functions

          apiHostname?: string

          Hostname of the API if a domain name is specified

          api
          @@ -47,4 +48,20 @@
           
          stageName: string

          Deployment stage (e.g. dev)

          -
          +
          useNestedStacks?: boolean

          If true, resolvers and Lambda functions will be created in nested stacks +grouped by type (Query, Mutation, Subscription, Field).

          +

          WARNING: Only enable this if you are approaching or hitting CloudFormation's +500 resource limit. Nested stacks have significant downsides:

          +
            +
          • Slower deployments due to additional stack operations
          • +
          • More complex debugging and error tracing
          • +
          • Harder to navigate in the CloudFormation console
          • +
          • Cross-stack references add complexity
          • +
          +

          WARNING: Changing this setting on an existing stack requires manual intervention. +CloudFormation cannot automatically move resources between stacks. You may need +to delete and recreate resolvers, or perform a two-step deployment.

          +
          false
          +
          + +
          diff --git a/docs/constructs/interfaces/RestApiProps.html b/docs/constructs/interfaces/RestApiProps.html index 864ca7e8..72313169 100644 --- a/docs/constructs/interfaces/RestApiProps.html +++ b/docs/constructs/interfaces/RestApiProps.html @@ -1,4 +1,4 @@ -RestApiProps | cdk-serverless
          cdk-serverless
            Preparing search index...

            Interface RestApiProps<OPS>

            interface RestApiProps<OPS> {
                additionalEnv?: { [key: string]: string };
                apiHostname?: string;
                apiName: string;
                assetCdn?: AssetCdn;
                authentication?: IJwtAuthentication | ICognitoAuthentication;
                autoGenerateRoutes?: boolean;
                cors: boolean;
                definitionFileName: string;
                domainName?: string;
                hostedZone?: IHostedZone;
                lambdaOptions?: LambdaOptions;
                lambdaOptionsByOperation?: {
                    [operationId in string | number | symbol]?: LambdaOptions
                };
                lambdaTracing?: LambdaTracingOptions;
                monitoring?: boolean;
                restApiProps?: RestApiBaseProps;
                singleTableDatastore?: ISingleTableDatastore;
                stageName: string;
            }

            Type Parameters

            • OPS

            Hierarchy (View Summary)

            Index

            Properties

            additionalEnv? +RestApiProps | cdk-serverless
            cdk-serverless
              Preparing search index...

              Interface RestApiProps<OPS>

              interface RestApiProps<OPS> {
                  additionalEnv?: { [key: string]: string };
                  apiHostname?: string;
                  apiName: string;
                  assetCdn?: AssetCdn;
                  authentication?: IJwtAuthentication | ICognitoAuthentication;
                  autoGenerateRoutes?: boolean;
                  cors: boolean;
                  definitionFileName: string;
                  domainName?: string;
                  hostedZone?: IHostedZone;
                  lambdaOptions?: LambdaOptions;
                  lambdaOptionsByOperation?: {
                      [operationId in string | number | symbol]?: LambdaOptions
                  };
                  lambdaTracing?: LambdaTracingOptions;
                  monitoring?: boolean;
                  restApiProps?: RestApiBaseProps;
                  singleTableDatastore?: ISingleTableDatastore;
                  stageName: string;
              }

              Type Parameters

              • OPS

              Hierarchy (View Summary)

              Index

              Properties

              additionalEnv? apiHostname? apiName assetCdn? diff --git a/src/constructs/graphql.ts b/src/constructs/graphql.ts index c1147c9c..592d4ab5 100644 --- a/src/constructs/graphql.ts +++ b/src/constructs/graphql.ts @@ -2,7 +2,7 @@ import { SpawnSyncOptions, spawnSync } from 'child_process'; import * as fs from 'fs'; import * as os from 'os'; -import { AssetHashType, BundlingOptions, BundlingOutput, CfnOutput, DockerImage, Tags, aws_appsync, aws_certificatemanager, aws_iam, aws_logs, aws_route53 } from 'aws-cdk-lib'; +import { AssetHashType, BundlingOptions, BundlingOutput, CfnOutput, DockerImage, NestedStack, Tags, aws_appsync, aws_certificatemanager, aws_iam, aws_logs, aws_route53 } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { CognitoAuthentication } from './authentication'; import { BaseApi, BaseApiProps } from './base-api'; @@ -11,6 +11,25 @@ import { CFN_OUTPUT_SUFFIX_GRAPHQL_DOMAINNAME } from '../shared/outputs'; export interface GraphQlApiProps extends BaseApiProps { definitionFileName: string; + + /** + * If true, resolvers and Lambda functions will be created in nested stacks + * grouped by type (Query, Mutation, Subscription, Field). + * + * WARNING: Only enable this if you are approaching or hitting CloudFormation's + * 500 resource limit. Nested stacks have significant downsides: + * - Slower deployments due to additional stack operations + * - More complex debugging and error tracing + * - Harder to navigate in the CloudFormation console + * - Cross-stack references add complexity + * + * WARNING: Changing this setting on an existing stack requires manual intervention. + * CloudFormation cannot automatically move resources between stacks. You may need + * to delete and recreate resolvers, or perform a two-step deployment. + * + * @default false + */ + useNestedStacks?: boolean; } export interface VtlResolverOptions { @@ -90,10 +109,16 @@ export class GraphQlApi extends BaseApi { */ private _functions: { [operationId: string]: LambdaFunction } = {}; + /** + * Nested stacks for resolvers, grouped by type name. + * Only used when useNestedStacks is true. + */ + private readonly nestedStacks: { [typeName: string]: NestedStack } = {}; + /** * The Cognito authentication configuration. */ - private cognitoAuth: CognitoAuthentication; + private readonly cognitoAuth: CognitoAuthentication; /** * Creates an instance of GraphQlApi. @@ -102,7 +127,7 @@ export class GraphQlApi extends BaseApi { * @param id - The scoped construct ID. * @param props - The properties of the GraphQlApi construct. */ - constructor(scope: Construct, id: string, private props: GraphQlApiProps) { + constructor(scope: Construct, id: string, private readonly props: GraphQlApiProps) { super(scope, id, props); this.cognitoAuth = props.authentication as CognitoAuthentication; @@ -274,7 +299,9 @@ export class GraphQlApi extends BaseApi { this.createEntryFile(entryFile, typeName as string, fieldName as string); } - const fn = new LambdaFunction(this, `Fn${operationId}`, { + const scope = this.getScopeForTypeName(typeName as string); + + const fn = new LambdaFunction(scope, `Fn${operationId}`, { stageName: this.props.stageName, additionalEnv: this.props.additionalEnv, entry: entryFile, @@ -303,13 +330,13 @@ export class GraphQlApi extends BaseApi { // this.monitoring.lambdaErrorsWidget.addLeftMetric(fn.metricThrottles()); // } - const dataSource = new aws_appsync.LambdaDataSource(this, `LambdaDS${operationId}`, { + const dataSource = new aws_appsync.LambdaDataSource(scope, `LambdaDS${operationId}`, { api: this.api, name: `Lambda_${typeName as string}_${fieldName as String}`, lambdaFunction: fn, }); - new aws_appsync.Resolver(this, `Resolver${operationId}`, { + new aws_appsync.Resolver(scope, `Resolver${operationId}`, { api: this.api, typeName: typeName as string, fieldName: fieldName as string, @@ -354,6 +381,8 @@ export class GraphQlApi extends BaseApi { const operationId = `${typeName as string}.${fieldName as String}`; const description = `Type ${typeName as string} Field ${fieldName as String} Resolver`; + const scope = this.getScopeForTypeName(typeName as string); + const resolverDir = './src/js-resolver/'; const functions: JsResolverConfig[] = []; @@ -383,7 +412,7 @@ export class GraphQlApi extends BaseApi { this.createJSResolverFile(fn, typeName as string, fieldName as string); } - const jsFunction = new aws_appsync.AppsyncFunction(this, fn.functionId, { + const jsFunction = new aws_appsync.AppsyncFunction(scope, fn.functionId, { api: this.api, name: operationId.replace(/\./g, ''), description, @@ -424,7 +453,7 @@ export class GraphQlApi extends BaseApi { pipelineConfig.push(jsFunction); } - new aws_appsync.Resolver(this, `Resolver${operationId}`, { + new aws_appsync.Resolver(scope, `Resolver${operationId}`, { api: this.api, typeName: typeName as string, fieldName: fieldName as string, @@ -463,7 +492,9 @@ ${Object.entries(options?.stashValues ?? []).map(val => ` ctx.stash.${val[0 fs.writeFileSync(mappingResFile, '$util.toJson($ctx.result)', { encoding: 'utf-8' }); } - new aws_appsync.Resolver(this, `Resolver${operationId}`, { + const scope = this.getScopeForTypeName(typeName as string); + + new aws_appsync.Resolver(scope, `Resolver${operationId}`, { api: this.api, typeName: typeName as string, fieldName: fieldName as string, @@ -473,6 +504,34 @@ ${Object.entries(options?.stashValues ?? []).map(val => ` ctx.stash.${val[0 }); } + /** + * Returns the appropriate scope for creating resources for the given type name. + * If useNestedStacks is enabled, returns a nested stack for the type name, + * creating it if it doesn't exist. Otherwise, returns this construct. + * + * Query, Mutation, and Subscription each get their own nested stack. + * All other types (field resolvers) share a single "Field" nested stack. + * + * @param typeName - The GraphQL type name (e.g., 'Query', 'Mutation', 'Subscription', or a custom type) + * @returns The construct to use as scope for creating resources + */ + private getScopeForTypeName(typeName: string): Construct { + if (!this.props.useNestedStacks) { + return this; + } + + const rootTypes = ['Query', 'Mutation', 'Subscription']; + const stackKey = rootTypes.includes(typeName) ? typeName : 'Field'; + + if (!this.nestedStacks[stackKey]) { + this.nestedStacks[stackKey] = new NestedStack(this, `${stackKey}Resolvers`, { + description: `Nested stack for ${stackKey} resolvers`, + }); + } + + return this.nestedStacks[stackKey]; + } + private substVariables(data: string, vars: { [name: string]: string } = {}): string { let res = data; for (const name in vars) { diff --git a/test/constructs/graphql.test.ts b/test/constructs/graphql.test.ts new file mode 100644 index 00000000..a294aa86 --- /dev/null +++ b/test/constructs/graphql.test.ts @@ -0,0 +1,360 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { App, Stack } from 'aws-cdk-lib'; +import { Template } from 'aws-cdk-lib/assertions'; +import { CognitoAuthentication, GraphQlApi } from '../../src/constructs'; + +// Define a resolver type for testing +interface TestResolvers { + Query: { + getUser: unknown; + listUsers: unknown; + }; + Mutation: { + createUser: unknown; + updateUser: unknown; + }; + Subscription: { + onUserCreated: unknown; + }; + User: { + posts: unknown; + }; + Post: { + comments: unknown; + }; +} + +// Mock the LambdaFunction to avoid NodejsFunction bundling issues +jest.mock('../../src/constructs/func', () => { + const awsLambda = jest.requireActual('aws-cdk-lib/aws-lambda'); + const { Construct } = jest.requireActual('constructs'); + + class MockLambdaFunction extends awsLambda.Function { + constructor(scope: typeof Construct, id: string, props: any) { + super(scope, id, { + runtime: awsLambda.Runtime.NODEJS_18_X, + handler: 'index.handler', + code: awsLambda.Code.fromInline('exports.handler = async () => {}'), + description: props.description, + }); + } + } + + return { + LambdaFunction: MockLambdaFunction, + }; +}); + +// Define the lambda files that should be created for the test resolvers +const lambdaFiles = [ + 'Query.getUser.ts', + 'Query.listUsers.ts', + 'Mutation.createUser.ts', + 'Mutation.updateUser.ts', + 'Subscription.onUserCreated.ts', + 'User.posts.ts', + 'Post.comments.ts', +]; + +describe('GraphQlApi', () => { + let app: App; + let stack: Stack; + let authentication: CognitoAuthentication; + const schemaPath = path.join(__dirname, 'test-schema.graphql'); + const lambdaDir = path.join(__dirname, '../../src/lambda'); + + beforeAll(() => { + // Create a minimal test schema file for the tests + fs.writeFileSync(schemaPath, ` +type Query { + getUser(id: ID!): User + listUsers: [User] +} + +type Mutation { + createUser(name: String!): User + updateUser(id: ID!, name: String!): User +} + +type Subscription { + onUserCreated: User +} + +type User { + id: ID! + name: String + posts: [Post] +} + +type Post { + id: ID! + title: String + comments: [String] +} +`); + + // Ensure lambda directory exists and create dummy entry files + fs.mkdirSync(lambdaDir, { recursive: true }); + + const dummyLambdaContent = 'export const handler = async () => {};'; + + + for (const file of lambdaFiles) { + const filePath = path.join(lambdaDir, file); + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, dummyLambdaContent); + } + } + }); + + afterAll(() => { + // Clean up the test schema file + if (fs.existsSync(schemaPath)) { + fs.unlinkSync(schemaPath); + } + + for (const file of lambdaFiles) { + const filePath = path.join(lambdaDir, file); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + } + }); + + beforeEach(() => { + app = new App(); + stack = new Stack(app, 'TestStack'); + authentication = new CognitoAuthentication(stack, 'Auth', { + userPoolName: 'TestUserPool', + identityPool: {}, + }); + }); + + describe('without useNestedStacks', () => { + test('creates GraphQL API in the main stack', () => { + new GraphQlApi(stack, 'TestApi', { + apiName: 'TestAPI', + stageName: 'test', + definitionFileName: schemaPath, + authentication, + }); + + const template = Template.fromStack(stack); + + // Verify GraphQL API is created + template.hasResourceProperties('AWS::AppSync::GraphQLApi', { + Name: 'TestAPI [test]', + }); + + // Verify no nested stacks are created + template.resourceCountIs('AWS::CloudFormation::Stack', 0); + }); + + test('creates Lambda resolver in the main stack', () => { + const api = new GraphQlApi(stack, 'TestApi', { + apiName: 'TestAPI', + stageName: 'test', + definitionFileName: schemaPath, + authentication, + }); + + api.addLambdaResolver('Query', 'getUser'); + + const template = Template.fromStack(stack); + + // Verify resolver is created in the main stack + template.hasResourceProperties('AWS::AppSync::Resolver', { + TypeName: 'Query', + FieldName: 'getUser', + }); + + // Verify Lambda function is created + template.hasResourceProperties('AWS::Lambda::Function', { + Description: '[test] Type Query Field getUser Resolver', + }); + + // Verify no nested stacks + template.resourceCountIs('AWS::CloudFormation::Stack', 0); + }); + + test('creates multiple resolvers in the main stack', () => { + const api = new GraphQlApi(stack, 'TestApi', { + apiName: 'TestAPI', + stageName: 'test', + definitionFileName: schemaPath, + authentication, + }); + + api.addLambdaResolver('Query', 'getUser'); + api.addLambdaResolver('Query', 'listUsers'); + api.addLambdaResolver('Mutation', 'createUser'); + + const template = Template.fromStack(stack); + + // Count resolvers in main stack + template.resourceCountIs('AWS::AppSync::Resolver', 3); + + // Verify no nested stacks + template.resourceCountIs('AWS::CloudFormation::Stack', 0); + }); + }); + + describe('with useNestedStacks enabled', () => { + test('creates nested stacks for resolvers', () => { + const api = new GraphQlApi(stack, 'TestApi', { + apiName: 'TestAPI', + stageName: 'test', + definitionFileName: schemaPath, + authentication, + useNestedStacks: true, + }); + + api.addLambdaResolver('Query', 'getUser'); + api.addLambdaResolver('Mutation', 'createUser'); + + const template = Template.fromStack(stack); + + // Verify nested stacks are created (one for Query, one for Mutation) + template.resourceCountIs('AWS::CloudFormation::Stack', 2); + }); + + test('groups Query resolvers in QueryResolvers nested stack', () => { + const api = new GraphQlApi(stack, 'TestApi', { + apiName: 'TestAPI', + stageName: 'test', + definitionFileName: schemaPath, + authentication, + useNestedStacks: true, + }); + + api.addLambdaResolver('Query', 'getUser'); + api.addLambdaResolver('Query', 'listUsers'); + + const template = Template.fromStack(stack); + + // Only one nested stack for Query resolvers + template.resourceCountIs('AWS::CloudFormation::Stack', 1); + + // Main stack should not have resolvers directly + template.resourceCountIs('AWS::AppSync::Resolver', 0); + }); + + test('groups Mutation resolvers in MutationResolvers nested stack', () => { + const api = new GraphQlApi(stack, 'TestApi', { + apiName: 'TestAPI', + stageName: 'test', + definitionFileName: schemaPath, + authentication, + useNestedStacks: true, + }); + + api.addLambdaResolver('Mutation', 'createUser'); + api.addLambdaResolver('Mutation', 'updateUser'); + + const template = Template.fromStack(stack); + + // Only one nested stack for Mutation resolvers + template.resourceCountIs('AWS::CloudFormation::Stack', 1); + }); + + test('groups field resolvers in FieldResolvers nested stack', () => { + const api = new GraphQlApi(stack, 'TestApi', { + apiName: 'TestAPI', + stageName: 'test', + definitionFileName: schemaPath, + authentication, + useNestedStacks: true, + }); + + // Field resolvers on custom types should go to Field nested stack + api.addLambdaResolver('User', 'posts'); + + const template = Template.fromStack(stack); + + // One nested stack for Field resolvers + template.resourceCountIs('AWS::CloudFormation::Stack', 1); + }); + + test('creates separate nested stacks for Query, Mutation, Subscription, and Field', () => { + const api = new GraphQlApi(stack, 'TestApi', { + apiName: 'TestAPI', + stageName: 'test', + definitionFileName: schemaPath, + authentication, + useNestedStacks: true, + }); + + api.addLambdaResolver('Query', 'getUser'); + api.addLambdaResolver('Mutation', 'createUser'); + api.addLambdaResolver('Subscription', 'onUserCreated'); + api.addLambdaResolver('User', 'posts'); + + const template = Template.fromStack(stack); + + // Four nested stacks: Query, Mutation, Subscription, Field + template.resourceCountIs('AWS::CloudFormation::Stack', 4); + }); + + test('multiple custom types share the same Field nested stack', () => { + const api = new GraphQlApi(stack, 'TestApi', { + apiName: 'TestAPI', + stageName: 'test', + definitionFileName: schemaPath, + authentication, + useNestedStacks: true, + }); + + // Both should go to the same Field nested stack + api.addLambdaResolver('User', 'posts'); + api.addLambdaResolver('Post', 'comments'); + + const template = Template.fromStack(stack); + + // Only one Field nested stack for all custom type resolvers + template.resourceCountIs('AWS::CloudFormation::Stack', 1); + }); + + test('GraphQL API remains in the main stack', () => { + const api = new GraphQlApi(stack, 'TestApi', { + apiName: 'TestAPI', + stageName: 'test', + definitionFileName: schemaPath, + authentication, + useNestedStacks: true, + }); + + api.addLambdaResolver('Query', 'getUser'); + + const template = Template.fromStack(stack); + + // GraphQL API should be in the main stack, not nested + template.hasResourceProperties('AWS::AppSync::GraphQLApi', { + Name: 'TestAPI [test]', + }); + }); + }); + + describe('useNestedStacks defaults to false', () => { + test('does not create nested stacks when useNestedStacks is not specified', () => { + const api = new GraphQlApi(stack, 'TestApi', { + apiName: 'TestAPI', + stageName: 'test', + definitionFileName: schemaPath, + authentication, + // useNestedStacks not specified - should default to false + }); + + api.addLambdaResolver('Query', 'getUser'); + api.addLambdaResolver('Mutation', 'createUser'); + + const template = Template.fromStack(stack); + + // No nested stacks + template.resourceCountIs('AWS::CloudFormation::Stack', 0); + + // Resolvers in main stack + template.resourceCountIs('AWS::AppSync::Resolver', 2); + }); + }); +});