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
81 stars 71 forks source link

Import custom utils in data resource handlers #2661

Closed jewells07 closed 1 day ago

jewells07 commented 1 week ago

Amplify CLI Version

12.12.2

Question

Im using (Nuxt 3 + Amplify gen 2). I have custom mutation with handler

./amplify/data/resource.ts

CreatingTodo: a
      .mutation()
      .arguments({
        id: a.string(),
        title: a.string().required(),
      })
      .returns(a.ref('Todo'))
      .handler([
        a.handler.custom({
          entry: './Todo/createTodo.js',
          dataSource: a.ref('Todo'),
        }),
      ])
      .authorization((allow) => [allow.authenticated()]),

./amplify/data/Todo/createTodo.js

import { util } from '@aws-appsync/utils';
import * as ddb from '@aws-appsync/utils/dynamodb';
import { decodeValue } from '@/server/utils/encodeDecode';
......<rest code>

It keeps giving me an error.

The CloudFormation deployment has failed.
Caused By: ❌ Deployment failed: BadRequestException: The code contains one or more errors.

Resolution: Find more information in the CloudFormation AWS Console for this stack.

If I remove import { decodeValue } from @/server/utils/encodeDecode. then its working fine. but I need that decodeValue util. I checked in CloudFormation, and there was no failed stack. Can someone explain to me how to check the error detail?

chrisbonifacio commented 1 week ago

Hi @jewells07, the The code contains one or more errors is a bit difficult to troubleshoot. Can you share a bit more info about the decodeValue function being imported and what it does?

Where is it being imported from? The @ in the import is a pattern I've seen in JS frameworks like Next/Nuxt and is configurable through a tsconfig file, but I'm not sure if it will work or apply to the bundling of the Lambda. Maybe try using a relative path rather than an implicit one and move the utility to the same folder as the lambda.

jewells07 commented 1 week ago

Yes, that util is from outside the amplify folder. and in Nuxt 3 server folder @/server/utils/encodeDecode i'm using some packages to encode decode some secret values. that file also including


import { parseAmplifyConfig } from 'aws-amplify/utils';
import type { Schema } from '~/amplify/data/resource';
import { generateClient } from 'aws-amplify/api/server';
import outputs from '~/amplify_outputs.json';
const amplifyConfig = parseAmplifyConfig(outputs);
const gqlServerClient = generateClient<Schema>({ config: amplifyConfig });
    .....<rest code>

So you mean to say that whatever files are available in the amplify folder that can only be imported, not from outside the amplify folder?

chrisbonifacio commented 1 week ago

So you mean to say that whatever files are available in the amplify folder that can only be imported, not from outside the amplify folder?

It should be possible to import from outside the amplify folder, but I think only using a relative path. So instead of @/server/utils/encodeDecode, try a path like ../../server/utils/encodeDecode. If that doesn't work then colocating the utils in the amplify and/or lambda folder might help. I think the main issue is the @/ part of the path.

jewells07 commented 1 week ago

import { decodeStatus } from '../../../server/utils/encodeDecodeBookingStatus'; This is also giving me an error.

The CloudFormation deployment has failed.
Caused By: ❌ Deployment failed: BadRequestException: The code contains one or more errors.
Resolution: Find more information in the CloudFormation AWS Console for this stack.
jewells07 commented 1 week ago

One of the packages I'm using is 'js-base64', and I'm trying to import it import { decode } from 'js-base64';. This is also giving me the same error. NOTE: I have installed js-base64 in my Nuxt, not in the amplify folder. The package.json in the amplify folder is still empty.

{
  "type": "module"
}
chrisbonifacio commented 1 week ago

I mistook your handler for a Lambda, but you're actually using an AppSync JS resolver.

I dug into this a little and import is not allowed (outside of @aws-appsync/utils which is a special package provided by AppSync), all code has to be in the same file for AppSync JS resolvers.

You will have to use esbuild to bundle the resolver and make sure that it is bundled as ESM modules, as AppSync doesn't support CommonJS.

Here's an example:

$ esbuild --bundle \
--sourcemap=inline \
--sources-content=false \
--target=esnext \
--platform=node \
--format=esm \
--external:@aws-appsync/utils \
--outdir=out/appsync \
 src/appsync/getPost.resolver.js

For more information, please refer to the AppSync docs:

https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-overview-js.html

jewells07 commented 3 days ago

Do I need to create everything from scratch, or can I just build what I want to import and then use it for importing? I'm not sure how this process works.

chrisbonifacio commented 3 days ago

You can write your resolver as js or take file like normal, importing whatever you need into your resolver. Then you would use esbuild to bundle your resolver and everything it depends on into one file.

Try using the example command above and bundle a resolver that imports your utility and check the output.

The last argument of the command is the file(s) you want to bundle.

Here's an example of bundling multiple files from the AppSync docs:


$ esbuild --bundle \
--sourcemap=inline \
--sources-content=false \
--target=esnext \
--platform=node \
--format=esm \
--external:@aws-appsync/utils \
--outdir=out/appsync \
 src/appsync/**/*.ts
jewells07 commented 3 days ago

I have created the file using esbuild in ./amplify/data/Todo/esbuildResolver/createTodo.js and changed the entry path in custom mutation

CreatingTodo: a
      .mutation()
      .arguments({
        id: a.string(),
        title: a.string().required(),
      })
      .returns(a.ref('Todo'))
      .handler([
        a.handler.custom({
          entry: './Todo/esbuildResolver/createTodo.js',
          dataSource: a.ref('Todo'),
        }),
      ])
      .authorization((allow) => [allow.authenticated()]),      

I encountered the same error again.

The CloudFormation deployment has failed.
Caused By: ❌ Deployment failed: BadRequestException: Code must be 32768 bytes or less

Resolution: Find more information in the CloudFormation AWS Console for this stack.

and however, when I reverted the code and ran esbuild, it worked as it did before.

chrisbonifacio commented 3 days ago

It looks like you ran into a bundle size limitation. Does your utility import other dependencies?

In any case, even if you reduce the size of the bundle, you will probably still run into compatibility issues with the code you or dependencies are trying to execute.

AppSync JS resolvers are simply meant to tell AppSync how to translate an incoming GraphQL request into an operation for your data source and how to translate the response from that data source back into a GraphQL response.

This being said, if you need to import code that may contain unsupported features like async await, try/catch, etc, then you should consider using a Lambda function as a data source and connect to the database from it instead.

https://docs.aws.amazon.com/appsync/latest/devguide/supported-features.html

This part of the AppSync docs explains the use cases and when to choose between JS resolvers and Lambda functions:

https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-overview-js.html#choosing-data-source

It also mentions the maximum size of code being 32,000 characters which seems aligned with the error message.

jewells07 commented 3 days ago

My utils contain the following code:

import { encode, decode } from 'js-base64';
import { gqlServerClient } from '@/utils/amplifyUtils';
....<rest code>

I also tried removing my utils and simply importing import { decode } from 'js-base64'; in my resolver. but I am still encountering the same error.

chrisbonifacio commented 3 days ago

Yeah that's an external module from npm, which could be using unsupported js runtime features that AppSync JS resolvers don't support.

Also, the size of that package alone is too large and exceeds the 32,000 byte limit.

image

I would recommend using a Lambda instead.

jewells07 commented 3 days ago

Is the issue due to size or unsupported js runtime features? The error details are not visible anywhere, making it very annoying. How can I check the details when this type of error occurs? Do you mean we can use Lambda, or should we write our own code that does not include external packages?

chrisbonifacio commented 3 days ago

Probably both.

The original error message in the issue description was

Caused By: ❌ Deployment failed: BadRequestException: The code contains one or more errors.

This could be for a lot of reasons, including attempting to use unsupported JS features or even simply syntax errors. It's not a very helpful message. There is a linter that you can use that might help catch these errors before deployment, also mentioned in the AppSync docs.

When you bundled the code to include the js-base64 module in the resolver, the size inflated past the limit, resulting in the second error message:

Caused By: ❌ Deployment failed: BadRequestException: Code must be 32768 bytes or less

I'm suggesting using a Lambda for your resolver instead because it doesn't have the limitations of support for external modules and the bundle size limit is much higher (I think 50MB?)

jewells07 commented 3 days ago

Yes, I plan to use a Lambda function, but first, I want to try using a JavaScript resolver. I attempted to write the code in simple JavaScript, but even a simple while loop is causing the same error.

chrisbonifacio commented 3 days ago

Funny enough, while loops are not supported either 😅

image

https://docs.aws.amazon.com/appsync/latest/devguide/supported-features.html

jewells07 commented 3 days ago

That's wild! 😱🤣 Looks like Lambda is the only option left. 😄

github-actions[bot] commented 1 day ago

This issue is now closed. Comments on closed issues are hard for our team to see. If you need more assistance, please open a new issue that references this one.