Skip to content

How to Create a Custom AppSync Query Using a Manually Added DynamoDB GSI for a @hasOne Relationship Field (Amplify Gen 1) #3261

Description

@komaki-k

Amplify CLI Version

12.10.2

Question

Hi everyone,

I'm using Amplify Gen 1 to define my AppSync schema, and I have fields with @hasOne relationships. My goal is to create a Global Secondary Index (GSI) on the underlying DynamoDB table based on the field targeted by the @hasOne relationship.

However, my understanding is that Amplify Gen 1 directives don't directly support adding an index (like @index) to a field that also has a relationship directive like @hasOne.

Because of this limitation, I manually created the required GSI on the relevant DynamoDB table using the AWS Console.

Now, I'm trying to define a custom query in AppSync that utilizes this manually created GSI. Here's what I've attempted and the issues I encountered:

Attempt 1: Defining the Custom Query in schema.graphql

  1. Context: I want to add a GSI related to the posts field on the Blog type. Since I can't add @index directly alongside @hasOne, I manually created the GSI on the BlogTable in DynamoDB.

    # Original Schema Snippet
    type Blog @model @auth(rules: [{ allow: private }, { allow: private, provider: iam }]) {
      id: ID!
      name: String!
      content: String!
      posts: [Group] @hasOne # Need GSI related to the underlying key for this relationship
    }
    
    type Group @model @auth(rules: [{ allow: private }, { allow: private, provider: iam }]) {
      id: ID!
      name: String!
    }
  2. Added Custom Query Definition to schema.graphql:

    # Added Custom Query Types and Definition
    input ListBlogsByGroupIdInput {
      blogGroupGroupId: ID # Assuming this is the GSI key name I created manually
    }
    
    type ListBlogsByGroupIdOutput @aws_iam @aws_cognito_user_pools {
      id: ID!
      name: String!
      content: String!
      # Potentially other fields from Blog
    }
    
    type Query {
      # Added custom query
      listBlogsByGroupId(input: ListBlogsByGroupIdInput): [ListBlogsByGroupIdOutput]
        @auth(rules: [{ allow: private }, { allow: private, provider: iam }])
    }
  3. Result: When I deploy this schema, Amplify automatically generates a resolver for listBlogsByGroupId. However, this auto-generated resolver has its data source set to NONE_DS, and the request mapping template is a basic one focused only on authorization, ultimately returning an empty payload:

    ## [Start] Field Authorization Steps. **
    #set( $isAuthorized = false )
    #if( $util.authType() == "IAM Authorization" )
      #foreach( $adminRole in $ctx.stash.adminRoles )
        #if( $ctx.identity.userArn.contains($adminRole) && $ctx.identity.userArn != $ctx.stash.authRole && $ctx.identity.userArn != $ctx.stash.unauthRole )
          #return($context.source.listBlogsByGroupId) # Corrected hypothetical return path
        #end
      #end
      #if( !$isAuthorized )
        #if( ($ctx.identity.userArn == $ctx.stash.authRole) || ($ctx.identity.cognitoIdentityPoolId == $ctx.stash.identityPoolId && $ctx.identity.cognitoIdentityAuthType == "authenticated") )
          #set( $isAuthorized = true )
        #end
      #end
    #end
    #if( $util.authType() == "User Pool Authorization" )
      #set( $isAuthorized = true )
    #end
    #if( !$isAuthorized )
      $util.unauthorized()
    #end
    $util.toJson({"version":"2018-05-29","payload":{}})
    ## [End] Field Authorization Steps. **

    Consequently, invoking the listBlogsByGroupId query always returns null.

Attempt 2: Using a Custom CDK Stack to Define the Resolver

(Note: I decided against using a Lambda resolver due to potential performance implications.)

  1. Approach: Based on the Amplify documentation for overriding resolvers (https://docs.amplify.aws/gen1/angular/build-a-backend/graphqlapi/custom-business-logic/#override-amplify-generated-resolvers), it seems the process involves modifying schema.graphql and creating a custom CDK stack to define the resolver logic.

  2. Implementation: I kept the schema.graphql changes from Attempt 1 and created a custom CDK stack (/amplify/backend/api/<api-name>/stacks/cdk-stack.ts) to define the resolver for listBlogsByGroupId, pointing it to the correct DynamoDB table (BlogTable) and using the AppSync JS runtime (code adapted from Unit Resolver examples):

    import * as cdk from 'aws-cdk-lib';
    import * as AmplifyHelpers from '@aws-amplify/cli-extensibility-helper';
    import * as appsync from 'aws-cdk-lib/aws-appsync';
    import { AmplifyDependentResourcesAttributes } from '../../types/amplify-dependent-resources-ref';
    import { Construct } from 'constructs';
    
    // Basic JS request/response template (needs actual GSI query logic)
    const jsResolverTemplate = `
    import { util } from '@aws-appsync/utils';
    
    export function request(ctx) {
      // TODO: Replace with actual GSI query logic
      // Example structure for querying GSI:
      // return {
      //   operation: 'Query',
      //   index: 'your-gsi-name', // Replace with your actual GSI name
      //   query: {
      //     expression: '#key = :value',
      //     expressionNames: {
      //       '#key': 'blogGroupGroupId' // Replace with your GSI partition key name
      //     },
      //     expressionValues: {
      //       ':value': util.dynamodb.toDynamoDB(ctx.args.input.blogGroupGroupId)
      //     }
      //   },
      //   // Add limit, nextToken etc. as needed
      // };
      console.log('Placeholder request resolver');
      return { operation: 'Scan' }; // Placeholder - THIS IS WRONG for GSI
    }
    
    export function response(ctx) {
      // TODO: Process results from GSI query
      if (ctx.error) {
        util.error(ctx.error.message, ctx.error.type);
      }
      return ctx.result.items; // Assuming query returns items
    }
    `;
    
    export class cdkStack extends cdk.Stack {
      constructor(
        scope: Construct,
        id: string,
        props?: cdk.StackProps,
        amplifyResourceProps?: AmplifyHelpers.AmplifyResourceProps
      ) {
        super(scope, id, props);
        /* Do not remove - Amplify CLI automatically injects the current deployment environment in this input parameter */
        new cdk.CfnParameter(this, 'env', {
          type: 'String',
          description: 'Current Amplify CLI env name'
        });
    
        const retVal: AmplifyDependentResourcesAttributes =
          AmplifyHelpers.addResourceDependency(
            this,
            amplifyResourceProps.category,
            amplifyResourceProps.resourceName,
            [
              {
                category: 'api',
                resourceName: 'testApi' // Replace with your actual API name
              }
            ]
          );
    
        // Define the JS resolver
        new appsync.Resolver(this, 'ListBlogsByGroupIdResolver', {
          apiId: cdk.Fn.ref(retVal.api.testApi.GraphQLAPIIdOutput),
          fieldName: 'listBlogsByGroupId',
          typeName: 'Query', // Query | Mutation | Subscription
          code: jsResolverTemplate,
          dataSourceName: 'ProjectTable', // DataSource name
          runtime: {
            name: 'APPSYNC_JS',
            runtimeVersion: '1.0.0'
          }
        });
      }
    }
  3. Result: When I try to deploy both the schema.graphql containing the custom query and this custom CDK stack, the deployment fails with an error like:
    Resource handler returned message: "No field named listBlogsByGroupId found on type Query (Service: AppSync, Status Code: 404, Request ID: xxxx) (SDK Attempt Count: 1)" (RequestToken: xxxx, HandlerErrorCode: NotFound)

    This error suggests either:

    • Amplify is trying to create the resolver based on the schema before the schema definition is fully registered or recognized.
    • There's a conflict because both the schema definition implicitly asks for a resolver and the CDK explicitly defines one.

Question:

What is the correct way within the Amplify Gen 1 framework to define a custom AppSync query (listBlogsByGroupId) that utilizes a manually created GSI on a DynamoDB table, especially when the GSI relates to a field managed by @hasOne? How can I define both the query in the schema and its corresponding resolver (pointing to the correct DynamoDB data source and using the GSI) without causing deployment conflicts or the NONE_DS issue?

Thanks for any guidance!

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions