Skip to content

Commit 9ae1d34

Browse files
kwwendtonlybakamGavinZZmergify[bot]
authored
feat(appsync): add L2 constructs for AWS AppSync Events (#32505)
### Issue # (if applicable) Closes #32004 ### Reason for this change This is in support of AWS AppSync Events. ### Description of changes - New constructs for `EventApi` and `ChannelNamespace` to support AWS AppSync Events. - Create common file for authorization config across `EventApi` and `GraphqlApi` constructs. - Create common file for common resources across `EventApi` and `GraphqlApi` constructs. ### Description of how you validated changes Added both unit and integration tests for AWS AppSync Event API changes. ### Contributors @mazyu36 @onlybakam @kwwendt ### Checklist - [X] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --------- Co-authored-by: onlybakam <[email protected]> Co-authored-by: GZ <[email protected]> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent b3975c5 commit 9ae1d34

File tree

79 files changed

+64424
-257
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+64424
-257
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function onPublish(ctx) {
2+
return ctx.events.filter((event) => event.payload.odds > 0)
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// Reference: https://github.com/onlybakam/appsync-events-client-tutorial/blob/main/app/signer-smithy.mjs
2+
3+
import { HttpRequest } from '@smithy/protocol-http'
4+
import { SignatureV4 } from '@smithy/signature-v4'
5+
import { fromNodeProviderChain } from '@aws-sdk/credential-providers'
6+
import { Sha256 } from '@aws-crypto/sha256-js'
7+
8+
// The default headers to to sign the request
9+
const DEFAULT_HEADERS = {
10+
accept: 'application/json, text/javascript',
11+
'content-encoding': 'amz-1.0',
12+
'content-type': 'application/json; charset=UTF-8',
13+
}
14+
15+
const AWS_APPSYNC_EVENTS_SUBPROTOCOL = 'aws-appsync-event-ws';
16+
const realtimeUrl = process.env.EVENT_API_REALTIME_URL;
17+
const httpUrl = process.env.EVENT_API_HTTP_URL;
18+
const region = process.env.AWS_REGION;
19+
20+
/**
21+
* Returns a signed authorization object
22+
*
23+
* @param {string} httpDomain the AppSync Event API HTTP domain
24+
* @param {string} region the AWS region of your API
25+
* @param {string} [body] the body of the request
26+
* @returns {Object}
27+
*/
28+
async function signWithAWSV4(httpDomain, region, body) {
29+
const signer = new SignatureV4({
30+
credentials: fromNodeProviderChain(),
31+
service: 'appsync',
32+
region,
33+
sha256: Sha256,
34+
})
35+
36+
const url = new URL(`https://${httpDomain}/event`)
37+
const request = new HttpRequest({
38+
method: 'POST',
39+
headers: {
40+
...DEFAULT_HEADERS,
41+
host: url.hostname,
42+
},
43+
body: body ?? '{}',
44+
hostname: url.hostname,
45+
path: url.pathname,
46+
})
47+
48+
const signedHttpRequest = await signer.sign(request)
49+
50+
return {
51+
host: signedHttpRequest.hostname,
52+
...signedHttpRequest.headers,
53+
}
54+
}
55+
56+
/**
57+
* Returns a header value for the SubProtocol header
58+
* @param {string} httpDomain the AppSync Event API HTTP domain
59+
* @param {string} region the AWS region of your API
60+
* @returns string a header string
61+
*/
62+
async function getAuthProtocolForIAM(httpDomain, region) {
63+
const signed = await signWithAWSV4(httpDomain, region)
64+
const based64UrlHeader = btoa(JSON.stringify(signed))
65+
.replace(/\+/g, '-') // Convert '+' to '-'
66+
.replace(/\//g, '_') // Convert '/' to '_'
67+
.replace(/=+$/, '') // Remove padding `=`
68+
return `header-${based64UrlHeader}`
69+
}
70+
71+
/**
72+
* Returns a Promise after a delay
73+
*
74+
* @param {int} ms milliseconds to delay
75+
* @returns {Promise}
76+
*/
77+
function sleep(ms) {
78+
return new Promise(resolve => setTimeout(resolve, ms));
79+
}
80+
81+
/**
82+
* Initiates a subscription to a channel and returns the response
83+
*
84+
* @param {string} channel the channel to subscribe to
85+
* @param {boolean} triggerPub whether to also publish in the method
86+
* @returns {Object}
87+
*/
88+
async function subscribe(channel, triggerPub=false) {
89+
const response = {};
90+
const auth = await getAuthProtocolForIAM(httpUrl, region)
91+
const socket = await new Promise((resolve, reject) => {
92+
const socket = new WebSocket(
93+
`wss://${realtimeUrl}/event/realtime`,
94+
[AWS_APPSYNC_EVENTS_SUBPROTOCOL, auth],
95+
{ headers: { ...DEFAULT_HEADERS } },
96+
)
97+
98+
socket.onopen = () => {
99+
socket.send(JSON.stringify({ type: 'connection_init' }))
100+
console.log("Initialize connection");
101+
resolve(socket)
102+
}
103+
104+
socket.onclose = (evt) => reject(new Error(evt.reason))
105+
socket.onmessage = (event) => {
106+
const payload = JSON.parse(event.data);
107+
console.log('=>', payload);
108+
if (payload.type === 'subscribe_success') {
109+
console.log('Connection established')
110+
response.statusCode = 200;
111+
response.msg = 'subscribe_success';
112+
} else if (payload.type === 'data') {
113+
console.log('Data received');
114+
response.pubStatusCode = 200;
115+
response.pubMsg = JSON.parse(payload.event).message;
116+
} else if (payload.type === "subscribe_error") {
117+
console.log(payload);
118+
if (payload.errors.some((error) => error.errorType === "UnauthorizedException")) {
119+
console.log("Error received");
120+
response.statusCode = 401;
121+
response.msg = "UnauthorizedException";
122+
} else if (payload.errors.some(error => error.errorType === 'AccessDeniedException')) {
123+
console.log('Error received');
124+
response.statusCode = 403;
125+
response.msg = 'Forbidden';
126+
} else {
127+
console.log("Error received");
128+
response.statusCode = 400;
129+
response.msg = payload.errors[0].errorType;
130+
}
131+
}
132+
}
133+
socket.onerror = (event) => console.log(event)
134+
});
135+
136+
const subChannel = `/${channel}/*`;
137+
socket.send(JSON.stringify({
138+
type: 'subscribe',
139+
id: crypto.randomUUID(),
140+
channel: subChannel,
141+
authorization: await signWithAWSV4(httpUrl, region, JSON.stringify({ channel: subChannel })),
142+
}));
143+
144+
if (triggerPub) {
145+
await sleep(1000);
146+
await publish(channel);
147+
}
148+
await sleep(3000);
149+
return response;
150+
}
151+
152+
/**
153+
* Publishes to a channel and returns the response
154+
*
155+
* @param {string} channel the channel to publish to
156+
* @returns {Object}
157+
*/
158+
async function publish(channel) {
159+
const event = {
160+
"channel": `/${channel}/test`,
161+
"events": [
162+
JSON.stringify({message:'Hello World!'})
163+
]
164+
}
165+
166+
const response = await fetch(`https://${httpUrl}/event`, {
167+
method: 'POST',
168+
headers: await signWithAWSV4(httpUrl, region, JSON.stringify(event)),
169+
body: JSON.stringify(event)
170+
});
171+
172+
if (!response.ok) {
173+
return {
174+
statusCode: response.status,
175+
msg: response.statusText
176+
}
177+
}
178+
const output = await response.json();
179+
return {
180+
statusCode: 200,
181+
msg: output.successful.length == 1 ? 'publish_success' : 'publish_fail',
182+
}
183+
}
184+
185+
/**
186+
*
187+
* @param {Object} event json object that contains the action and channel
188+
* @returns {Object}
189+
*/
190+
exports.handler = async function(event) {
191+
const pubSubAction = event.action;
192+
const channel = event.channel;
193+
194+
if (pubSubAction === 'publish') {
195+
const res = await publish(channel);
196+
console.log(res);
197+
return res;
198+
} else if (pubSubAction === 'subscribe') {
199+
const res = await subscribe(channel, false);
200+
console.log(res);
201+
return res;
202+
} else if (pubSubAction === 'pubSub') {
203+
const res = await subscribe(channel, true);
204+
console.log(res);
205+
return res;
206+
}
207+
};

packages/@aws-cdk-testing/framework-integ/test/aws-appsync/test/integ.appsync-channel-namespace.js.snapshot/EventApiChannelNamespaceStack.assets.json

+45
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)