Skip to content

Commit fbf209e

Browse files
author
Dane Pilcher
authored
feat: add api id and amplify environment name to stash (#2273)
1 parent 107600b commit fbf209e

File tree

5 files changed

+111
-12
lines changed

5 files changed

+111
-12
lines changed

.changeset/strong-toes-sniff.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@aws-amplify/backend-data': minor
3+
'@aws-amplify/backend': minor
4+
---
5+
6+
Add GraphQL API ID and Amplify environment name to custom JS resolver stash

.eslint_dictionary.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"hotswappable",
7979
"hotswapped",
8080
"hotswapping",
81+
"href",
8182
"iamv2",
8283
"identitypool",
8384
"idps",

packages/backend-data/src/assets/js_resolver_handler.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
/**
22
* Pipeline resolver request handler
33
*/
4-
export const request = () => {
4+
export const request = (ctx: Record<string, Record<string, string>>) => {
5+
ctx.stash.awsAppsyncApiId = '${amplifyApiId}';
6+
ctx.stash.amplifyApiEnvironmentName = '${amplifyApiEnvironmentName}';
57
return {};
68
};
79
/**

packages/backend-data/src/convert_js_resolvers.test.ts

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1-
import { Template } from 'aws-cdk-lib/assertions';
1+
import { Match, Template } from 'aws-cdk-lib/assertions';
22
import assert from 'node:assert';
33
import { beforeEach, describe, it } from 'node:test';
44
import { App, Duration, Stack } from 'aws-cdk-lib';
55
import {
66
AmplifyData,
77
AmplifyDataDefinition,
88
} from '@aws-amplify/data-construct';
9-
import { resolve } from 'path';
10-
import { fileURLToPath } from 'url';
11-
import { convertJsResolverDefinition } from './convert_js_resolvers.js';
9+
import { join, resolve } from 'path';
10+
import { tmpdir } from 'os';
11+
import { fileURLToPath, pathToFileURL } from 'url';
12+
import {
13+
convertJsResolverDefinition,
14+
defaultJsResolverCode,
15+
} from './convert_js_resolvers.js';
1216
import { a } from '@aws-amplify/data-schema';
17+
import { writeFileSync } from 'node:fs';
1318

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

36+
void describe('defaultJsResolverCode', () => {
37+
void it('returns the default JS resolver code with api id and env name in valid JS', async () => {
38+
const code = defaultJsResolverCode('testApiId', 'testEnvName');
39+
assert(code.includes("ctx.stash.awsAppsyncApiId = 'testApiId';"));
40+
assert(
41+
code.includes("ctx.stash.amplifyApiEnvironmentName = 'testEnvName';")
42+
);
43+
44+
const tempDir = tmpdir();
45+
const filename = join(tempDir, 'js_resolver_handler.js');
46+
writeFileSync(filename, code);
47+
48+
// windows requires dynamic imports to use file urls
49+
const fileUrl = pathToFileURL(filename).href;
50+
const resolver = await import(fileUrl);
51+
const context = { stash: {}, prev: { result: 'result' } };
52+
assert.deepEqual(resolver.request(context), {});
53+
54+
// assert api id and env name are added to the context stash
55+
assert.deepEqual(context.stash, {
56+
awsAppsyncApiId: 'testApiId',
57+
amplifyApiEnvironmentName: 'testEnvName',
58+
});
59+
assert.equal(resolver.response(context), 'result');
60+
});
61+
});
62+
3163
void describe('convertJsResolverDefinition', () => {
3264
let stack: Stack;
3365
let amplifyApi: AmplifyData;
@@ -158,4 +190,52 @@ void describe('convertJsResolverDefinition', () => {
158190

159191
template.resourceCountIs('AWS::AppSync::Resolver', 1);
160192
});
193+
194+
void it('adds api id and environment name to stash', () => {
195+
const absolutePath = resolve(
196+
fileURLToPath(import.meta.url),
197+
'../../lib/assets',
198+
'js_resolver_handler.js'
199+
);
200+
201+
const schema = a.schema({
202+
customQuery: a
203+
.query()
204+
.authorization((allow) => allow.publicApiKey())
205+
.returns(a.string())
206+
.handler(
207+
a.handler.custom({
208+
entry: absolutePath,
209+
})
210+
),
211+
});
212+
const { jsFunctions } = schema.transform();
213+
convertJsResolverDefinition(stack, amplifyApi, jsFunctions);
214+
215+
const template = Template.fromStack(stack);
216+
template.hasResourceProperties('AWS::AppSync::Resolver', {
217+
Runtime: {
218+
Name: 'APPSYNC_JS',
219+
RuntimeVersion: '1.0.0',
220+
},
221+
Kind: 'PIPELINE',
222+
TypeName: 'Query',
223+
FieldName: 'customQuery',
224+
Code: {
225+
'Fn::Join': [
226+
'',
227+
[
228+
"/**\n * Pipeline resolver request handler\n */\nexport const request = (ctx) => {\n ctx.stash.awsAppsyncApiId = '",
229+
{
230+
'Fn::GetAtt': [
231+
Match.stringLikeRegexp('amplifyDataGraphQLAPI.*'),
232+
'ApiId',
233+
],
234+
},
235+
"';\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",
236+
],
237+
],
238+
},
239+
});
240+
});
161241
});

packages/backend-data/src/convert_js_resolvers.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { CfnFunctionConfiguration, CfnResolver } from 'aws-cdk-lib/aws-appsync';
44
import { JsResolver } from '@aws-amplify/data-schema-types';
55
import { resolve } from 'path';
66
import { fileURLToPath } from 'node:url';
7+
import { readFileSync } from 'fs';
78
import { Asset } from 'aws-cdk-lib/aws-s3-assets';
89
import { resolveEntryPath } from './resolve_entry_path.js';
910

@@ -18,17 +19,25 @@ const JS_PIPELINE_RESOLVER_HANDLER = './assets/js_resolver_handler.js';
1819
* 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.
1920
*
2021
* Customer-provided handlers are added as a Functions list in `pipelineConfig.functions`
22+
*
23+
* Add Amplify API ID and environment name to the context stash for use in the customer-provided handlers.
2124
*/
22-
const defaultJsResolverAsset = (scope: Construct): Asset => {
25+
export const defaultJsResolverCode = (
26+
amplifyApiId: string,
27+
amplifyApiEnvironmentName: string
28+
): string => {
2329
const resolvedTemplatePath = resolve(
2430
fileURLToPath(import.meta.url),
2531
'../../lib',
2632
JS_PIPELINE_RESOLVER_HANDLER
2733
);
2834

29-
return new Asset(scope, 'default_js_resolver_handler_asset', {
30-
path: resolveEntryPath(resolvedTemplatePath),
31-
});
35+
return readFileSync(resolvedTemplatePath, 'utf-8')
36+
.replace(new RegExp(/\$\{amplifyApiId\}/, 'g'), amplifyApiId)
37+
.replace(
38+
new RegExp(/\$\{amplifyApiEnvironmentName\}/, 'g'),
39+
amplifyApiEnvironmentName
40+
);
3241
};
3342

3443
/**
@@ -44,8 +53,6 @@ export const convertJsResolverDefinition = (
4453
return;
4554
}
4655

47-
const jsResolverTemplateAsset = defaultJsResolverAsset(scope);
48-
4956
for (const resolver of jsResolvers) {
5057
const functions: string[] = resolver.handlers.map((handler, idx) => {
5158
const fnName = `Fn_${resolver.typeName}_${resolver.fieldName}_${idx + 1}`;
@@ -71,12 +78,15 @@ export const convertJsResolverDefinition = (
7178

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

81+
const amplifyApiEnvironmentName =
82+
scope.node.tryGetContext('amplifyEnvironmentName') ?? 'NONE';
7483
new CfnResolver(scope, resolverName, {
7584
apiId: amplifyApi.apiId,
7685
fieldName: resolver.fieldName,
7786
typeName: resolver.typeName,
7887
kind: APPSYNC_PIPELINE_RESOLVER,
79-
codeS3Location: jsResolverTemplateAsset.s3ObjectUrl,
88+
// Uses synth-time inline code to avoid circular dependency when adding the API ID as an environment variable.
89+
code: defaultJsResolverCode(amplifyApi.apiId, amplifyApiEnvironmentName),
8090
runtime: {
8191
name: APPSYNC_JS_RUNTIME_NAME,
8292
runtimeVersion: APPSYNC_JS_RUNTIME_VERSION,

0 commit comments

Comments
 (0)