aws-amplify / amplify-category-api

The AWS Amplify CLI is a toolchain for simplifying serverless web and mobile development. This plugin provides functionality for the API category, allowing for the creation and management of GraphQL and REST based backends for your amplify project.
https://docs.amplify.aws/
Apache License 2.0
80 stars 69 forks source link

Support AppSync JavaScript Resolvers #1015

Open austinamorusoyardstick opened 1 year ago

austinamorusoyardstick commented 1 year ago

Is this feature request related to a new or existing Amplify category?

No response

Is this related to another service?

Appsync JavaScript resolvers

Describe the feature you'd like to request

New feature in appsync how to use within amplify?

https://aws.amazon.com/blogs/aws/aws-appsync-graphql-apis-supports-javascript-resolvers/

Describe the solution you'd like

Steps to use within amplify

Describe alternatives you've considered

Manual work around that doesn't change the way we deploy

Additional context

No response

Is this something that you'd be interested in working on?

Would this feature include a breaking change?

isaac-elvt commented 1 year ago

bump

BBopanna commented 1 year ago

Need this soon please!

james-kuihi commented 1 year ago

Third'd - this would be a great feature to have!

danfreid commented 1 year ago

Would be much appreciated by developers

alexboulay commented 1 year ago

We would appreciate this too!

dklein1211 commented 1 year ago

bump

vaughngit commented 1 year ago

bump!

tminus1-design commented 1 year ago

bump

prashant-joshi-25 commented 1 year ago

bump!!

milesnash-sky commented 1 year ago

➕ 1️⃣

mithun35h commented 11 months ago

bump

austinamorusoyardstick commented 11 months ago

Honestly changing all the vlt templates to JS would likely enable to Amplify community to issue patches easier and fixes / plugins for issues surrounding resolvers.

biller-aivy commented 9 months ago

Any plans for this rebuild of the resolvers?

PatrykMilewski commented 9 months ago

bump

jay-herrera commented 9 months ago

bump

barshopen commented 9 months ago

Seems like AWS phases out of VTL if favor of APPSYNC_JS (source: https://docs.aws.amazon.com/appsync/latest/devguide/resolver-mapping-template-reference.html),

image

are there any updates on this feature?

chrisl777 commented 8 months ago

bump

giraudvalentin commented 8 months ago

its a nightmare to do graphql with appsync.. i update to node 16, update my sls to 3, update appsync plugin to support js resolver and, what, the simulator broke everything

renebrandel commented 8 months ago

Hi - while we update our documentation to illustrate how to do this with the Amplify CLI, I do want to bring attention to the fact that the GraphQL API category is now available first-class as a CDK construct. In the CDK construct, you can use JavaScript resolvers. Check out our announcement post here: https://aws.amazon.com/blogs/mobile/announcing-aws-amplifys-graphql-api-cdk-construct-deploy-real-time-graphql-api-and-data-stack-on-aws/

ElliottAtYardstick commented 8 months ago

Yes please

travishaby commented 6 months ago

This would be lovely, yes please!

lukasulbing commented 5 months ago

bump

Dennis-Dekker commented 4 months ago

bump

armenr commented 4 months ago

BUMP

espetro commented 2 months ago

bump 🤭

armenr commented 2 months ago

@renebrandel - you mentioned there'd be updates to the documentation, but in the mean time, can we get even a rough overview/walkthrough of how to do it "by hand" for existing amplify projects that are cli-dependent/cli-generated?

Please? :)

armenr commented 2 months ago

I took a look here: https://docs.aws.amazon.com/appsync/latest/devguide/configuring-resolvers-js.html

And then I got impatient, and thought maybe I'd give this a try. I kinda took a sledgehammer to it, so please forgive me if there are more elegant ways to achieve this.

In case anybody is as masochistic as I am, I think I got pretty close with this.

I don't think it works yet (it's 4:00 AM here and I need to sleep), but it's a close-enough starting point, in case anyone wants to hack on this together.

👉 If anyone does bother to try this, and is successful (or finds ways to make it work), please share back your solution 😎.

amplify add custom --> name: MyCustomResolvers

cdk-stack.ts

import type { Construct } from 'constructs'
import * as AmplifyHelpers from '@aws-amplify/cli-extensibility-helper'
import * as cdk from 'aws-cdk-lib'
import * as appsync from 'aws-cdk-lib/aws-appsync'
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'
import type { AmplifyDependentResourcesAttributes } from '../../types/amplify-dependent-resources-ref'

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 */
    const _env = new cdk.CfnParameter(this, 'env', {
      type: 'String',
      description: 'Current Amplify CLI env name',
    })

    // Get the project name from AmplifyHelper
    // and create a standard prefix for all resources - "projectName-env"
    const { projectName, envName } = AmplifyHelpers.getProjectInfo()
    const _resourcePrefix = `${projectName}-${envName}`

    /*
    * 🛑 📢
    * There is a known bug (https://github.com/aws-amplify/amplify-cli/issues/13532) that *may* prevent
    * the use of the addResourceDependency function from AmplifyHelpers.
    *
    * If you run into that bug, you can use the following workaround to reference dependent resources in your CDK stack.
    *
    * The formula is: category + resourceName + outputName
    *
    * For a GraphQL API example: apiMyApiNameGraphQLAPIIdOutput
    * For an S3 Storage example: storageMyProjectStorageStorageBucketName
    *
    * Where:
    *   category: api
    *   resourceName: <YOUR_API_NAME>
    *   outputName: GraphQLAPIIdOutput
    *
    * const resourceCategory = 'api'
    * const resourceName = 'myprojectV2AwesomeAPI'
    * const outputName = 'GraphQLAPIIdOutput'
    * const GraphQLAPIIdOutput = resourceCategory + resourceName + outputName
    *
    */

    const apiResourceReference: AmplifyDependentResourcesAttributes = AmplifyHelpers.addResourceDependency(
      this,
      amplifyResourceProps.category,
      amplifyResourceProps.resourceName,
      [
        {
          category: 'api',
          resourceName: 'myprojectV2AwesomeAPI',
        },
      ],
    )

    // Instantiate a reference to the API using the API ID from the Amplify-generated stack
    const api = appsync.GraphqlApi.fromGraphqlApiAttributes(this, 'api', {
      graphqlApiId: apiResourceReference.api.myprojectV2AwesomeAPI.GraphQLAPIIdOutput,
    })

    // Time to get our CloudFormation on - there's no escaping it!
    const graphqlApiId = apiResourceReference.api.myprojectV2AwesomeAPI.GraphQLAPIIdOutput
    const tableName = 'UserTable'
    const importString = `\${${graphqlApiId}}:GetAtt:${tableName}:Name`

    /* Get the name of the table using cloudformation stack outputs
    *  We are attempting to mimic this:
    *
    *   "Fn::ImportValue": {
    *     "Fn::Sub": "${apimyprojectV2AwesomeAPIGraphQLAPIIdOutput}:GetAtt:UserTable:Name"
    *   }
    */

    // Get the name of the table using cloudformation stack outputs - e.g. User-5mt2wm3hzrgptjlq7qcbryxcgi-mybackend
    const UserTableName = cdk.Fn.importValue(cdk.Fn.sub(importString))

    // Instantiate a reference to the DynamoDB table using the table name from the Amplify-generated stack
    const userTableDDB = dynamodb.Table.fromTableName(this, 'table', UserTableName)

    // Create a new AppSync function and pass in the API and the DynamoDB table objects
    const appsyncFuncScanUsersTable = new appsync.AppsyncFunction(this, 'func-scan-users', {
      name: 'scan_users_func_1',
      api,
      dataSource: api.addDynamoDbDataSource('table-for-posts', userTableDDB),
      code: appsync.Code.fromInline(`
          export function request(ctx) {
            return { operation: 'Scan' };
          }

          export function response(ctx) {
            return ctx.result.items;
          }
      `),
      runtime: appsync.FunctionRuntime.JS_1_0_0,
    })

    // Create a new AppSync resolver
    const _resolver = new appsync.Resolver(this, 'custom-resolver', {
      api,
      typeName: 'Query',
      fieldName: 'getPost',
      code: appsync.Code.fromInline(`
          export function request(ctx) {
            return {};
          }

          export function response(ctx) {
            return ctx.prev.result;
          }
      `),
      runtime: appsync.FunctionRuntime.JS_1_0_0,
      pipelineConfig: [appsyncFuncScanUsersTable],
    })
  }
}

package.json

{
  "name": "custom-resource",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "dependencies": {
    "@aws-amplify/cli-extensibility-helper": "^3.0.25",
    "aws-cdk-lib": "~2.80.0",
    "constructs": "^10.3.0"
  },
  "devDependencies": {
    "typescript": "^5.4.2"
  },
  "resolutions": {
    "aws-cdk-lib": "~2.80.0"
  }
}
armenr commented 2 months ago

^^ This gets close, but it won't work. There's 16 hours of my life I won't get back. Maybe someone can figure out what I'm doing wrong by trying it themselves. I'm good and stuck.

Abandon all hope, ye who enter here. Waiting for any help or instruction from @renebrandel & our Amplify friends... 🫠

renebrandel commented 2 months ago

Hi - @armenr - sorry for the dropping the ball here. So here's a working sample that should work for you.

For the JS resolvers, I build a quick "echo this message" resolver example. The graphQL schema looks like this:

input AMPLIFY { globalAuthRule: AuthRule = { allow: public } } # FOR TESTING ONLY!

type Todo @model {
  id: ID!
  name: String!
  description: String
}

type Query {
  echo(message: String): String # your custom queries here
}

The custom stack looks like this:

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';

const jsResolverTemplate = `
export function request(ctx) {
  return {
    payload: null
  }
}

export function response(ctx) {
  return ctx.arguments.message
}
`

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'
    });

    // Access other Amplify Resources
    const retVal: AmplifyDependentResourcesAttributes =
      AmplifyHelpers.addResourceDependency(
        this,
        amplifyResourceProps.category,
        amplifyResourceProps.resourceName,
        [
          {
            category: 'api',
            resourceName: 'gen1jsresolver'
          }
        ]
      );

    const resolver = new appsync.CfnResolver(this, 'CustomResolver', {
      // apiId: retVal.api.new.GraphQLAPIIdOutput,
      // https://github.com/aws-amplify/amplify-cli/issues/9391#event-5843293887
      // If you use Amplify you can access the parameter via Ref since it's a CDK parameter passed from the root stack.
      // Previously the ApiId is the variable Name which is wrong , it should be variable value as below
      apiId: cdk.Fn.ref(retVal.api.gen1jsresolver.GraphQLAPIIdOutput),
      fieldName: 'echo',
      typeName: 'Query', // Query | Mutation | Subscription
      code: jsResolverTemplate,
      dataSourceName: 'NONE_DS', // DataSource name
      runtime: {
        name: 'APPSYNC_JS',
        runtimeVersion: '1.0.0'
      }
    });
  }
}

Do you mind sharing more about the exact error you're facing? My guess is that this line is probably where the error is coming from:

      graphqlApiId: apiResourceReference.api.myprojectV2AwesomeAPI.GraphQLAPIIdOutput,

My guess is that the resolution of the reference doesn't happen correctly, thus the API won't be referenced. It should be this:

      graphqlApiId: cdk.Fn.ref(apiResourceReference.api.myprojectV2AwesomeAPI.GraphQLAPIIdOutput),
renebrandel commented 2 months ago

@armenr - also cut a docs PR https://github.com/aws-amplify/docs/pull/7087

armenr commented 2 months ago

Dude! @renebrandel - this is fantastic. Thanks for the quick reply and the Customer Obsession!

Thank you 😎 I'm going to try it out and post back.

I'm sure it's going to work.

armenr commented 2 months ago

@renebrandel - This was great. Seems to work just fine! 👍🏼

One last question, just to be sure. Let's say I run the following command:

aws appsync list-data-sources --api-id <MY_API_ID> --no-cli-pager | grep name

And it provides the following output:

... # other stuff
  name: UserTable

This means, that if I wanted the resolver to be hooked up to the User model/table, then I can replace dataSourceName: 'NONE_DS', // DataSource name with dataSourceName: 'UserTable', right?

Would I require any further changes or would that be it? Thanks again, a lot!

renebrandel commented 2 months ago

That's correct. We're making this experience significantly more seamless in Gen 2. That should help in the future. Gen 2 is still in dev preview. https://docs.amplify.aws/gen2/build-a-backend/data/custom-business-logic/

armenr commented 2 months ago

@renebrandel - Heck yes. I excitedly check in on the gen2 dashboard + the docs every week...and am very excitedly waiting for it to go GA and out of Preview.

I guess since I've got your attention...one more question (sorry if it doesn't belong on this Issue thread):

Will we be able to export/eject/migrate our gen1 project into gen2 when gen2 is ready? We're making full use of everything in gen1...

renebrandel commented 2 months ago

Hi - let's open a separate issue on the amplify-backend repo to discuss Gen 2 issues if this response isn't sufficient for now. Just to keep this thread focused around JS resolvers.

We're working on the migration strategy for Gen 1 to Gen 2. At the time of Gen 2 GA, you'll be able to rebuild most use cases with Gen 2 and build out significantly more (for example, JS resolvers). We're building up detailed feature matrix, so customers will understand which use case is supported in:

After Gen 2 GA, our highest priority item is building a migration path for auth, data, and storage. Specifically, our plan is to migrate the stateful resources from your Gen 1 project to Gen 2. I.e. porting over your user pool, database tables, buckets. For "stateless" services (Lambda functions, AppSync API), we'll provide a guide on how to recreate in functional parity in Gen 2.

Dennis-Dekker commented 2 months ago

Thank you both for the input on this thread, I have just caught up with the issue after my holiday.

We are currently also working on getting the JS resolvers up and running and were also able to get this working for queries and mutations that we manually added to the schema. However, we would like to replace the amplify generated VTL resolvers that are made during the build. Do you have any guidance on how to do this? I created an issue for this some time ago but haven't received any replies on that: #2211

armenr commented 2 months ago

@renebrandel - no problem, I didn't mean to veer us of course. I'm super grateful for the help and clarification here.

Just a few more specific questions about this implementation, and then I'll buzz off:

  1. Is there a specific reason to use const resolver = new appsync.CfnResolver VS const _resolver = new appsync.Resolver( - or stated differently - is there a specific reason not to use VS the other?

  2. If there is, in fact, a constraint which dictates we have to use that, is there any reason why you would recommend against us doing something like this?

    // Read the resolver code from a file
    const resolverCodePath = path.join(__dirname, 'resolverCode.js');
    const jsResolverTemplate = readFileSync(resolverCodePath, 'utf8');

    const resolver = new appsync.CfnResolver(this, 'CustomResolver', {
      apiId: cdk.Fn.ref(retVal.api.gen1jsresolver.GraphQLAPIIdOutput),
      fieldName: 'echo',
      typeName: 'Query', // Query | Mutation | Subscription
      code: jsResolverTemplate,
      dataSourceName: 'NONE_DS', // DataSource name
      runtime: {
        name: 'APPSYNC_JS',
        runtimeVersion: '1.0.0'
      }
    });

Customer reason/use-case: For us, the idea would be to write the resolvers in TypeScript, and then have a pre-push hook take care of transpiling the resolvers before the amplify push happens. For that reason, we'd want to be able to pass the code in as a file (or read in the file and pass it to the resolver constructor).

naedx commented 2 months ago

I'm also interested in this use case. Typescript is integral to how we work with JS code for Lambda, etc.

armenr commented 2 months ago

@naedx - I think I've found a useful pattern for that. I'll post back with our approach shortly.

renebrandel commented 2 months ago

@armenr - sorry for the late reply. The core difference is that the CfnResolver is a "L1 construct" vs. Resolver is an "L2 construct". Using the L1 construct in Gen 1 is simpler for referencing the API but you're not prohibited to use the L2 construct. Either case though the TypeScript compilation workflow would likely remain the same using your suggestion of leveraging a prePush command hook.