Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/strong-toes-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@aws-amplify/backend-data': minor
'@aws-amplify/backend': minor
---

Add GraphQL API ID and Amplify environment name to custom JS resolver stash
50 changes: 49 additions & 1 deletion packages/backend-data/src/convert_js_resolvers.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Template } from 'aws-cdk-lib/assertions';
import { Match, Template } from 'aws-cdk-lib/assertions';
import assert from 'node:assert';
import { beforeEach, describe, it } from 'node:test';
import { App, Duration, Stack } from 'aws-cdk-lib';
Expand Down Expand Up @@ -158,4 +158,52 @@ void describe('convertJsResolverDefinition', () => {

template.resourceCountIs('AWS::AppSync::Resolver', 1);
});

void it('adds api id and environment name to stash', () => {
const absolutePath = resolve(
fileURLToPath(import.meta.url),
'../../lib/assets',
'js_resolver_handler.js'
);

const schema = a.schema({
customQuery: a
.query()
.authorization((allow) => allow.publicApiKey())
.returns(a.string())
.handler(
a.handler.custom({
entry: absolutePath,
})
),
});
const { jsFunctions } = schema.transform();
convertJsResolverDefinition(stack, amplifyApi, jsFunctions);

const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::AppSync::Resolver', {
Runtime: {
Name: 'APPSYNC_JS',
RuntimeVersion: '1.0.0',
},
Kind: 'PIPELINE',
TypeName: 'Query',
FieldName: 'customQuery',
Code: {
'Fn::Join': [
'',
[
"\n /**\n * Pipeline resolver request handler\n */\n export const request = (ctx) => {\n ctx.stash.awsAppsyncApiId = '",
{
'Fn::GetAtt': [
Match.stringLikeRegexp('amplifyDataGraphQLAPI.*'),
'ApiId',
],
},
"';\n ctx.stash.awsAmplifyEnvironmentName = 'NONE';\n return {};\n };\n /**\n * Pipeline resolver response handler\n */\n export const response = (ctx) => {\n return ctx.prev.result;\n };\n ",
],
],
},
});
});
});
52 changes: 27 additions & 25 deletions packages/backend-data/src/convert_js_resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,12 @@ import { Construct } from 'constructs';
import { AmplifyData } from '@aws-amplify/data-construct';
import { CfnFunctionConfiguration, CfnResolver } from 'aws-cdk-lib/aws-appsync';
import { JsResolver } from '@aws-amplify/data-schema-types';
import { resolve } from 'path';
import { fileURLToPath } from 'node:url';
import { Asset } from 'aws-cdk-lib/aws-s3-assets';
import { resolveEntryPath } from './resolve_entry_path.js';

const APPSYNC_PIPELINE_RESOLVER = 'PIPELINE';
const APPSYNC_JS_RUNTIME_NAME = 'APPSYNC_JS';
const APPSYNC_JS_RUNTIME_VERSION = '1.0.0';
const JS_PIPELINE_RESOLVER_HANDLER = './assets/js_resolver_handler.js';

/**
*
* This returns the top-level passthrough resolver request/response handler (see: https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-overview-js.html#anatomy-of-a-pipeline-resolver-js)
* It's required for defining a pipeline resolver. The only purpose it serves is returning the output of the last function in the pipeline back to the client.
*
* Customer-provided handlers are added as a Functions list in `pipelineConfig.functions`
*/
const defaultJsResolverAsset = (scope: Construct): Asset => {
const resolvedTemplatePath = resolve(
fileURLToPath(import.meta.url),
'../../lib',
JS_PIPELINE_RESOLVER_HANDLER
);

return new Asset(scope, 'default_js_resolver_handler_asset', {
path: resolveEntryPath(resolvedTemplatePath),
});
};

/**
* Converts JS Resolver definition emitted by data-schema into AppSync pipeline
Expand All @@ -44,8 +22,6 @@ export const convertJsResolverDefinition = (
return;
}

const jsResolverTemplateAsset = defaultJsResolverAsset(scope);

for (const resolver of jsResolvers) {
const functions: string[] = resolver.handlers.map((handler, idx) => {
const fnName = `Fn_${resolver.typeName}_${resolver.fieldName}_${idx + 1}`;
Expand All @@ -71,12 +47,38 @@ export const convertJsResolverDefinition = (

const resolverName = `Resolver_${resolver.typeName}_${resolver.fieldName}`;

const amplifyEnvironmentName =
scope.node.tryGetContext('amplifyEnvironmentName') ?? 'NONE';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This context var amplifyEnvironmentName looks like is set by data construct exclusively? Is this the branchName for CI/CD deployments and NONE for sandbox?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is this the branchName for CI/CD deployments and NONE for sandbox.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

than we are using the wrong naming here. amplifyEnvironmentName suggests amplify wide name for the environment which is not the case here. May I suggest amplifyApiEnvironmentName?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value is already used in the data construct. To use the new name without a breaking change we would need to set both variables, then it might be confusing why there are two variables with the same value. I think it is a better option to continue using the existing one.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not asking to change in the data construct but rather the new variables created in the backend-data to which this value is assigned. As well as the new variable set in the convert_js_resolvers.ts. It's not a branch name since it's applicable for sandboxes as well.

Copy link
Contributor Author

@dpilch dpilch Dec 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sorry, you mean const amplifyEnvironmentName not what is in context?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I understand context vars are harder to change, but we should not create more vars with this naming (such as amplifyBranchName)

new CfnResolver(scope, resolverName, {
apiId: amplifyApi.apiId,
fieldName: resolver.fieldName,
typeName: resolver.typeName,
kind: APPSYNC_PIPELINE_RESOLVER,
codeS3Location: jsResolverTemplateAsset.s3ObjectUrl,
/**
* The top-level passthrough resolver request/response handler (see: https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-overview-js.html#anatomy-of-a-pipeline-resolver-js)
* It's required for defining a pipeline resolver. Adds the GraphQL API ID and Amplify environment name to the context stash.
* Returns the output of the last function in the pipeline back to the client.
*
* Customer-provided handlers are added as a Functions list in `pipelineConfig.functions`
*
* Uses synth-time inline code to avoid circular dependency when adding the API ID as an environment variable.
*/
code: `
/**
* Pipeline resolver request handler
*/
export const request = (ctx) => {
ctx.stash.awsAppsyncApiId = '${amplifyApi.apiId}';
ctx.stash.awsAmplifyEnvironmentName = '${amplifyEnvironmentName}';
return {};
};
/**
* Pipeline resolver response handler
*/
export const response = (ctx) => {
return ctx.prev.result;
};
`,
runtime: {
name: APPSYNC_JS_RUNTIME_NAME,
runtimeVersion: APPSYNC_JS_RUNTIME_VERSION,
Expand Down