Skip to content

Commit

Permalink
feat: add api id and amplify environment name to stash (#2273)
Browse files Browse the repository at this point in the history
  • Loading branch information
dpilch authored Dec 19, 2024
1 parent 107600b commit fbf209e
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 12 deletions.
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
1 change: 1 addition & 0 deletions .eslint_dictionary.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"hotswappable",
"hotswapped",
"hotswapping",
"href",
"iamv2",
"identitypool",
"idps",
Expand Down
4 changes: 3 additions & 1 deletion packages/backend-data/src/assets/js_resolver_handler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/**
* Pipeline resolver request handler
*/
export const request = () => {
export const request = (ctx: Record<string, Record<string, string>>) => {
ctx.stash.awsAppsyncApiId = '${amplifyApiId}';
ctx.stash.amplifyApiEnvironmentName = '${amplifyApiEnvironmentName}';
return {};
};
/**
Expand Down
88 changes: 84 additions & 4 deletions packages/backend-data/src/convert_js_resolvers.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
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';
import {
AmplifyData,
AmplifyDataDefinition,
} from '@aws-amplify/data-construct';
import { resolve } from 'path';
import { fileURLToPath } from 'url';
import { convertJsResolverDefinition } from './convert_js_resolvers.js';
import { join, resolve } from 'path';
import { tmpdir } from 'os';
import { fileURLToPath, pathToFileURL } from 'url';
import {
convertJsResolverDefinition,
defaultJsResolverCode,
} from './convert_js_resolvers.js';
import { a } from '@aws-amplify/data-schema';
import { writeFileSync } from 'node:fs';

// stub schema for the AmplifyApi construct
// not relevant to this test suite
Expand All @@ -28,6 +33,33 @@ const createStackAndSetContext = (): Stack => {
return stack;
};

void describe('defaultJsResolverCode', () => {
void it('returns the default JS resolver code with api id and env name in valid JS', async () => {
const code = defaultJsResolverCode('testApiId', 'testEnvName');
assert(code.includes("ctx.stash.awsAppsyncApiId = 'testApiId';"));
assert(
code.includes("ctx.stash.amplifyApiEnvironmentName = 'testEnvName';")
);

const tempDir = tmpdir();
const filename = join(tempDir, 'js_resolver_handler.js');
writeFileSync(filename, code);

// windows requires dynamic imports to use file urls
const fileUrl = pathToFileURL(filename).href;
const resolver = await import(fileUrl);
const context = { stash: {}, prev: { result: 'result' } };
assert.deepEqual(resolver.request(context), {});

// assert api id and env name are added to the context stash
assert.deepEqual(context.stash, {
awsAppsyncApiId: 'testApiId',
amplifyApiEnvironmentName: 'testEnvName',
});
assert.equal(resolver.response(context), 'result');
});
});

void describe('convertJsResolverDefinition', () => {
let stack: Stack;
let amplifyApi: AmplifyData;
Expand Down Expand Up @@ -158,4 +190,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 * Pipeline resolver request handler\n */\nexport const request = (ctx) => {\n ctx.stash.awsAppsyncApiId = '",
{
'Fn::GetAtt': [
Match.stringLikeRegexp('amplifyDataGraphQLAPI.*'),
'ApiId',
],
},
"';\n ctx.stash.amplifyApiEnvironmentName = 'NONE';\n return {};\n};\n/**\n * Pipeline resolver response handler\n */\nexport const response = (ctx) => {\n return ctx.prev.result;\n};\n",
],
],
},
});
});
});
24 changes: 17 additions & 7 deletions packages/backend-data/src/convert_js_resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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 { readFileSync } from 'fs';
import { Asset } from 'aws-cdk-lib/aws-s3-assets';
import { resolveEntryPath } from './resolve_entry_path.js';

Expand All @@ -18,17 +19,25 @@ const JS_PIPELINE_RESOLVER_HANDLER = './assets/js_resolver_handler.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`
*
* Add Amplify API ID and environment name to the context stash for use in the customer-provided handlers.
*/
const defaultJsResolverAsset = (scope: Construct): Asset => {
export const defaultJsResolverCode = (
amplifyApiId: string,
amplifyApiEnvironmentName: string
): string => {
const resolvedTemplatePath = resolve(
fileURLToPath(import.meta.url),
'../../lib',
JS_PIPELINE_RESOLVER_HANDLER
);

return new Asset(scope, 'default_js_resolver_handler_asset', {
path: resolveEntryPath(resolvedTemplatePath),
});
return readFileSync(resolvedTemplatePath, 'utf-8')
.replace(new RegExp(/\$\{amplifyApiId\}/, 'g'), amplifyApiId)
.replace(
new RegExp(/\$\{amplifyApiEnvironmentName\}/, 'g'),
amplifyApiEnvironmentName
);
};

/**
Expand All @@ -44,8 +53,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 +78,15 @@ export const convertJsResolverDefinition = (

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

const amplifyApiEnvironmentName =
scope.node.tryGetContext('amplifyEnvironmentName') ?? 'NONE';
new CfnResolver(scope, resolverName, {
apiId: amplifyApi.apiId,
fieldName: resolver.fieldName,
typeName: resolver.typeName,
kind: APPSYNC_PIPELINE_RESOLVER,
codeS3Location: jsResolverTemplateAsset.s3ObjectUrl,
// Uses synth-time inline code to avoid circular dependency when adding the API ID as an environment variable.
code: defaultJsResolverCode(amplifyApi.apiId, amplifyApiEnvironmentName),
runtime: {
name: APPSYNC_JS_RUNTIME_NAME,
runtimeVersion: APPSYNC_JS_RUNTIME_VERSION,
Expand Down

0 comments on commit fbf209e

Please sign in to comment.