Skip to content

Commit 2318384

Browse files
feat(appsync): js resolver support (#23551)
Adds support for resolvers using the APPSYNC_JS runtime. Adds new props for specifying `Code` and `Runtime` on both `Resolver` and `Function` constructs. Adds `Code` class with asset support for local files and inline code. Adds integ test covering basic JS function/resolver usage. Fix: #22921
1 parent 19dbc34 commit 2318384

File tree

26 files changed

+4112
-0
lines changed

26 files changed

+4112
-0
lines changed

packages/@aws-cdk/aws-appsync/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ junit.xml
2323
!**/*.snapshot/**/asset.*/*.d.ts
2424

2525
!**/*.snapshot/**/asset.*/**
26+
!test/integ-assets/**/*.js

packages/@aws-cdk/aws-appsync/README.md

+36
Original file line numberDiff line numberDiff line change
@@ -550,4 +550,40 @@ const pipelineResolver = new appsync.Resolver(this, 'pipeline', {
550550
});
551551
```
552552

553+
### JS Functions and Resolvers
554+
555+
JS Functions and resolvers are also supported. You can use a `.js` file within your CDK project, or specify your function code inline.
556+
557+
```ts
558+
declare const api: appsync.GraphqlApi;
559+
560+
const myJsFunction = new appsync.AppsyncFunction(this, 'function', {
561+
name: 'my_js_function',
562+
api,
563+
dataSource: api.addNoneDataSource('none'),
564+
code: appsync.Code.fromAsset('directory/function_code.js'),
565+
runtime: appsync.FunctionRuntime.JS_1_0_0,
566+
});
567+
568+
new appsync.Resolver(this, 'PipelineResolver', {
569+
api,
570+
typeName: 'typeName',
571+
fieldName: 'fieldName',
572+
code: appsync.Code.fromInline(`
573+
// The before step
574+
export function request(...args) {
575+
console.log(args);
576+
return {}
577+
}
578+
579+
// The after step
580+
export function response(ctx) {
581+
return ctx.prev.result
582+
}
583+
`),
584+
runtime: appsync.FunctionRuntime.JS_1_0_0,
585+
pipelineConfig: [myJsFunction],
586+
});
587+
```
588+
553589
Learn more about Pipeline Resolvers and AppSync Functions [here](https://docs.aws.amazon.com/appsync/latest/devguide/pipeline-resolvers.html).

packages/@aws-cdk/aws-appsync/lib/appsync-function.ts

+28
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { Resource, IResource, Lazy, Fn } from '@aws-cdk/core';
22
import { Construct } from 'constructs';
33
import { CfnFunctionConfiguration } from './appsync.generated';
4+
import { Code } from './code';
45
import { BaseDataSource } from './data-source';
56
import { IGraphqlApi } from './graphqlapi-base';
67
import { MappingTemplate } from './mapping-template';
8+
import { FunctionRuntime } from './runtime';
79

810
/**
911
* the base properties for AppSync Functions
@@ -31,6 +33,18 @@ export interface BaseAppsyncFunctionProps {
3133
* @default - no response mapping template
3234
*/
3335
readonly responseMappingTemplate?: MappingTemplate;
36+
/**
37+
* The functions runtime
38+
*
39+
* @default - no function runtime, VTL mapping templates used
40+
*/
41+
readonly runtime?: FunctionRuntime;
42+
/**
43+
* The function code
44+
*
45+
* @default - no code is used
46+
*/
47+
readonly code?: Code;
3448
}
3549

3650
/**
@@ -128,11 +142,25 @@ export class AppsyncFunction extends Resource implements IAppsyncFunction {
128142

129143
public constructor(scope: Construct, id: string, props: AppsyncFunctionProps) {
130144
super(scope, id);
145+
146+
// If runtime is specified, code must also be
147+
if (props.runtime && !props.code) {
148+
throw new Error('Code is required when specifying a runtime');
149+
}
150+
151+
if (props.code && (props.requestMappingTemplate || props.responseMappingTemplate)) {
152+
throw new Error('Mapping templates cannot be used alongside code');
153+
}
154+
155+
const code = props.code?.bind(this);
131156
this.function = new CfnFunctionConfiguration(this, 'Resource', {
132157
name: props.name,
133158
description: props.description,
134159
apiId: props.api.apiId,
135160
dataSourceName: props.dataSource.name,
161+
runtime: props.runtime?.toProperties(),
162+
codeS3Location: code?.s3Location,
163+
code: code?.inlineCode,
136164
functionVersion: '2018-05-29',
137165
requestMappingTemplate: props.requestMappingTemplate?.renderTemplate(),
138166
responseMappingTemplate: props.responseMappingTemplate?.renderTemplate(),
+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import * as s3_assets from '@aws-cdk/aws-s3-assets';
2+
import * as cdk from '@aws-cdk/core';
3+
import { Construct } from 'constructs';
4+
5+
/**
6+
* Result of binding `Code` into a `Function`.
7+
*/
8+
export interface CodeConfig {
9+
/**
10+
* The location of the code in S3 (mutually exclusive with `inlineCode`.
11+
* @default - code is not an s3 location
12+
*/
13+
readonly s3Location?: string;
14+
15+
/**
16+
* Inline code (mutually exclusive with `s3Location`).
17+
* @default - code is not inline code
18+
*/
19+
readonly inlineCode?: string;
20+
}
21+
22+
/**
23+
* Represents source code for an AppSync Function or Resolver.
24+
*/
25+
export abstract class Code {
26+
/**
27+
* Loads the function code from a local disk path.
28+
*
29+
* @param path The path to the source code file.
30+
*/
31+
public static fromAsset(path: string, options?: s3_assets.AssetOptions): AssetCode {
32+
return new AssetCode(path, options);
33+
}
34+
35+
/**
36+
* Inline code for AppSync function
37+
* @returns `InlineCode` with inline code.
38+
* @param code The actual handler code (limited to 4KiB)
39+
*/
40+
public static fromInline(code: string): InlineCode {
41+
return new InlineCode(code);
42+
}
43+
44+
/**
45+
* Bind source code to an AppSync Function or resolver.
46+
*/
47+
public abstract bind(scope: Construct): CodeConfig;
48+
}
49+
50+
/**
51+
* Represents a local file with source code used for an AppSync Function or Resolver.
52+
*/
53+
export class AssetCode extends Code {
54+
private asset?: s3_assets.Asset;
55+
56+
/**
57+
* @param path The path to the asset file.
58+
*/
59+
constructor(public readonly path: string, private readonly options: s3_assets.AssetOptions = { }) {
60+
super();
61+
}
62+
63+
public bind(scope: Construct): CodeConfig {
64+
// If the same AssetCode is used multiple times, retain only the first instantiation.
65+
if (!this.asset) {
66+
this.asset = new s3_assets.Asset(scope, 'Code', {
67+
path: this.path,
68+
...this.options,
69+
});
70+
} else if (cdk.Stack.of(this.asset) !== cdk.Stack.of(scope)) {
71+
throw new Error(`Asset is already associated with another stack '${cdk.Stack.of(this.asset).stackName}'. ` +
72+
'Create a new Code instance for every stack.');
73+
}
74+
75+
return {
76+
s3Location: this.asset.s3ObjectUrl,
77+
};
78+
}
79+
}
80+
81+
/**
82+
* AppSync function code from an inline string.
83+
*/
84+
export class InlineCode extends Code {
85+
constructor(private code: string) {
86+
super();
87+
88+
if (code.length === 0) {
89+
throw new Error('AppSync Inline code cannot be empty');
90+
}
91+
}
92+
93+
public bind(_scope: Construct): CodeConfig {
94+
return {
95+
inlineCode: this.code,
96+
};
97+
}
98+
}

packages/@aws-cdk/aws-appsync/lib/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ export * from './resolver';
1010
export * from './schema';
1111
export * from './graphqlapi';
1212
export * from './graphqlapi-base';
13+
export * from './code';
14+
export * from './runtime';

packages/@aws-cdk/aws-appsync/lib/resolver.ts

+28
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import { IAppsyncFunction } from './appsync-function';
44
import { CfnResolver } from './appsync.generated';
55
import { CachingConfig } from './caching-config';
66
import { BASE_CACHING_KEYS } from './caching-key';
7+
import { Code } from './code';
78
import { BaseDataSource } from './data-source';
89
import { IGraphqlApi } from './graphqlapi-base';
910
import { MappingTemplate } from './mapping-template';
11+
import { FunctionRuntime } from './runtime';
1012

1113
/**
1214
* Basic properties for an AppSync resolver
@@ -51,6 +53,19 @@ export interface BaseResolverProps {
5153
* @default - No max batch size
5254
*/
5355
readonly maxBatchSize?: number;
56+
57+
/**
58+
* The functions runtime
59+
*
60+
* @default - no function runtime, VTL mapping templates used
61+
*/
62+
readonly runtime?: FunctionRuntime;
63+
/**
64+
* The function code
65+
*
66+
* @default - no code is used
67+
*/
68+
readonly code?: Code;
5469
}
5570

5671
/**
@@ -93,6 +108,15 @@ export class Resolver extends Construct {
93108
{ functions: props.pipelineConfig.map((func) => func.functionId) }
94109
: undefined;
95110

111+
// If runtime is specified, code must also be
112+
if (props.runtime && !props.code) {
113+
throw new Error('Code is required when specifying a runtime');
114+
}
115+
116+
if (props.code && (props.requestMappingTemplate || props.responseMappingTemplate)) {
117+
throw new Error('Mapping templates cannot be used alongside code');
118+
}
119+
96120
if (pipelineConfig && props.dataSource) {
97121
throw new Error(`Pipeline Resolver cannot have data source. Received: ${props.dataSource.name}`);
98122
}
@@ -108,12 +132,16 @@ export class Resolver extends Construct {
108132
}
109133
}
110134

135+
const code = props.code?.bind(this);
111136
this.resolver = new CfnResolver(this, 'Resource', {
112137
apiId: props.api.apiId,
113138
typeName: props.typeName,
114139
fieldName: props.fieldName,
115140
dataSourceName: props.dataSource ? props.dataSource.name : undefined,
116141
kind: pipelineConfig ? 'PIPELINE' : 'UNIT',
142+
runtime: props.runtime?.toProperties(),
143+
codeS3Location: code?.s3Location,
144+
code: code?.inlineCode,
117145
pipelineConfig: pipelineConfig,
118146
requestMappingTemplate: props.requestMappingTemplate ? props.requestMappingTemplate.renderTemplate() : undefined,
119147
responseMappingTemplate: props.responseMappingTemplate ? props.responseMappingTemplate.renderTemplate() : undefined,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* Config for binding runtime to a function or resolver
3+
*/
4+
export interface RuntimeConfig {
5+
/**
6+
* The name of the runtime
7+
*/
8+
readonly name: string;
9+
10+
/**
11+
* The version string of the runtime
12+
*/
13+
readonly runtimeVersion: string;
14+
}
15+
16+
/**
17+
* Appsync supported runtimes. Only JavaScript as of now
18+
*/
19+
export enum FunctionRuntimeFamily {
20+
/**
21+
* AppSync JavaScript runtime
22+
*/
23+
JS = 'APPSYNC_JS',
24+
}
25+
26+
/**
27+
* Utility class for specifying specific appsync runtime versions
28+
*/
29+
export class FunctionRuntime {
30+
/**
31+
* APPSYNC_JS v1.0.0 runtime
32+
*/
33+
public static readonly JS_1_0_0 = new FunctionRuntime(FunctionRuntimeFamily.JS, '1.0.0');
34+
35+
/**
36+
* The name of the runtime
37+
*/
38+
public readonly name: string;
39+
40+
/**
41+
* The runtime version
42+
*/
43+
public readonly version: string;
44+
45+
public constructor(family: FunctionRuntimeFamily, version: string) {
46+
this.name = family;
47+
this.version = version;
48+
}
49+
50+
/**
51+
* Convert to Cfn runtime configuration property format
52+
*/
53+
public toProperties(): RuntimeConfig {
54+
return {
55+
name: this.name,
56+
runtimeVersion: this.version,
57+
};
58+
}
59+
}
60+

packages/@aws-cdk/aws-appsync/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
"@aws-cdk/aws-logs": "0.0.0",
119119
"@aws-cdk/aws-opensearchservice": "0.0.0",
120120
"@aws-cdk/aws-rds": "0.0.0",
121+
"@aws-cdk/assets": "0.0.0",
121122
"@aws-cdk/aws-s3-assets": "0.0.0",
122123
"@aws-cdk/aws-secretsmanager": "0.0.0",
123124
"@aws-cdk/core": "0.0.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
type Test {
2+
id: String!
3+
name: String!
4+
}
5+
type Query {
6+
getTests: [Test]!
7+
}
8+
type Mutation {
9+
addTest(name: String!): Test
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// The before step
2+
export function request(...args) {
3+
return {}
4+
}
5+
6+
// The after step
7+
export function response(ctx) {
8+
return ctx.prev.result
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { util } from '@aws-appsync/utils'
2+
3+
export function request(ctx) {
4+
const id = util.autoId()
5+
const name = ctx.args.name;
6+
7+
ctx.args.input = {
8+
id,
9+
name,
10+
}
11+
12+
return {
13+
version: '2018-05-29',
14+
operation: 'PutItem',
15+
key: { id: util.dynamodb.toDynamoDB(ctx.args.input.id) },
16+
attributeValues: util.dynamodb.toMapValues(ctx.args.input),
17+
};
18+
}
19+
20+
export function response(ctx) {
21+
return ctx.result;
22+
}

0 commit comments

Comments
 (0)