Skip to content

Commit c39e3fe

Browse files
authored
add custom domain support (#697)
1 parent 3ef34d8 commit c39e3fe

File tree

3 files changed

+216
-32
lines changed

3 files changed

+216
-32
lines changed

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,16 @@ import {
129129

130130
import appSyncConfig from "./aws-exports";
131131

132+
/* The HTTPS endpoint of the AWS AppSync API
133+
(e.g. *https://aaaaaaaaaaaaaaaaaaaaaaaaaa.appsync-api.us-east-1.amazonaws.com/graphql*).
134+
[Custom domain names](https://docs.aws.amazon.com/appsync/latest/devguide/custom-domain-name.html) can also be supplied here (e.g. *https://api.yourdomain.com/graphql*).
135+
Custom domain names can have any format, but must end with `/graphql`
136+
(see https://graphql.org/learn/serving-over-http/#uris-routes). */
132137
const url = appSyncConfig.aws_appsync_graphqlEndpoint;
138+
139+
133140
const region = appSyncConfig.aws_appsync_region;
141+
134142
const auth = {
135143
type: appSyncConfig.aws_appsync_authenticationType,
136144
apiKey: appSyncConfig.aws_appsync_apiKey,
@@ -258,10 +266,14 @@ import AWSAppSyncClient from "aws-appsync";
258266
import AppSyncConfig from "./aws-exports";
259267
import { ApolloProvider } from "react-apollo";
260268
import { Rehydrated } from "aws-appsync-react"; // this needs to also be installed when working with React
261-
262269
import App from "./App";
263270

264271
const client = new AWSAppSyncClient({
272+
/* The HTTPS endpoint of the AWS AppSync API
273+
(e.g. *https://aaaaaaaaaaaaaaaaaaaaaaaaaa.appsync-api.us-east-1.amazonaws.com/graphql*).
274+
[Custom domain names](https://docs.aws.amazon.com/appsync/latest/devguide/custom-domain-name.html) can also be supplied here (e.g. *https://api.yourdomain.com/graphql*).
275+
Custom domain names can have any format, but must end with `/graphql`
276+
(see https://graphql.org/learn/serving-over-http/#uris-routes). */
265277
url: AppSyncConfig.aws_appsync_graphqlEndpoint,
266278
region: AppSyncConfig.aws_appsync_region,
267279
auth: {

packages/aws-appsync-subscription-link/__tests__/link/realtime-subscription-handshake-link-test.ts

Lines changed: 178 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import { AppSyncRealTimeSubscriptionHandshakeLink } from '../../src/realtime-sub
66
const query = gql`subscription { someSubscription { aField } }`
77

88
class myWebSocket implements WebSocket {
9-
binaryType: BinaryType; bufferedAmount: number;
9+
binaryType: BinaryType;
10+
bufferedAmount: number;
1011
extensions: string;
1112
onclose: (this: WebSocket, ev: CloseEvent) => any;
1213
onerror: (this: WebSocket, ev: Event) => any;
@@ -49,8 +50,22 @@ describe("RealTime subscription link", () => {
4950
type: AUTH_TYPE.API_KEY,
5051
apiKey: 'xxxxx'
5152
},
52-
region: 'us-east-1',
53-
url: 'https://xxxxx.appsync-api.amazonaws.com/graphql'
53+
region: 'us-west-2',
54+
url: 'https://firsttesturl12345678901234.appsync-api.us-west-2.amazonaws.com/graphql'
55+
});
56+
57+
expect(link).toBeInstanceOf(AppSyncRealTimeSubscriptionHandshakeLink);
58+
});
59+
60+
test("Can instantiate link with custom domain", () => {
61+
expect.assertions(1);
62+
const link = new AppSyncRealTimeSubscriptionHandshakeLink({
63+
auth: {
64+
type: AUTH_TYPE.API_KEY,
65+
apiKey: 'xxxxx'
66+
},
67+
region: 'us-west-2',
68+
url: 'https://test1.testcustomdomain.com/graphql'
5469
});
5570

5671
expect(link).toBeInstanceOf(AppSyncRealTimeSubscriptionHandshakeLink);
@@ -62,7 +77,7 @@ describe("RealTime subscription link", () => {
6277
return "2019-11-13T18:47:04.733Z";
6378
}));
6479
AppSyncRealTimeSubscriptionHandshakeLink.createWebSocket = jest.fn((url, protocol) => {
65-
expect(url).toBe('wss://xxxxx.appsync-realtime-api.amazonaws.com/graphql?header=eyJob3N0IjoieHh4eHguYXBwc3luYy1hcGkuYW1hem9uYXdzLmNvbSIsIngtYW16LWRhdGUiOiIyMDE5MTExM1QxODQ3MDRaIiwieC1hcGkta2V5IjoieHh4eHgifQ==&payload=e30=');
80+
expect(url).toBe('wss://apikeytesturl1234567890123.appsync-realtime-api.us-west-2.amazonaws.com/graphql?header=eyJob3N0IjoiYXBpa2V5dGVzdHVybDEyMzQ1Njc4OTAxMjMuYXBwc3luYy1hcGkudXMtd2VzdC0yLmFtYXpvbmF3cy5jb20iLCJ4LWFtei1kYXRlIjoiMjAxOTExMTNUMTg0NzA0WiIsIngtYXBpLWtleSI6Inh4eHh4In0=&payload=e30=');
6681
expect(protocol).toBe('graphql-ws');
6782
done();
6883
return new myWebSocket();
@@ -72,13 +87,50 @@ describe("RealTime subscription link", () => {
7287
type: AUTH_TYPE.API_KEY,
7388
apiKey: 'xxxxx'
7489
},
75-
region: 'us-east-1',
76-
url: 'https://xxxxx.appsync-api.amazonaws.com/graphql'
90+
region: 'us-west-2',
91+
url: 'https://apikeytesturl1234567890123.appsync-api.us-west-2.amazonaws.com/graphql'
7792
});
7893

7994
execute(link, { query }).subscribe({
8095
error: (err) => {
81-
console.log({ err });
96+
console.log(JSON.stringify(err));
97+
fail;
98+
},
99+
next: (data) => {
100+
console.log({ data });
101+
done();
102+
},
103+
complete: () => {
104+
console.log('done with this');
105+
done();
106+
}
107+
108+
});
109+
});
110+
111+
test("Initialize WebSocket correctly for API KEY with custom domain", (done) => {
112+
expect.assertions(2);
113+
jest.spyOn(Date.prototype, 'toISOString').mockImplementation(jest.fn(() => {
114+
return "2019-11-13T18:47:04.733Z";
115+
}));
116+
AppSyncRealTimeSubscriptionHandshakeLink.createWebSocket = jest.fn((url, protocol) => {
117+
expect(url).toBe('wss://apikeytest.testcustomdomain.com/graphql/realtime?header=eyJob3N0IjoiYXBpa2V5dGVzdC50ZXN0Y3VzdG9tZG9tYWluLmNvbSIsIngtYW16LWRhdGUiOiIyMDE5MTExM1QxODQ3MDRaIiwieC1hcGkta2V5IjoieHh4eHgifQ==&payload=e30=');
118+
expect(protocol).toBe('graphql-ws');
119+
done();
120+
return new myWebSocket();
121+
});
122+
const link = new AppSyncRealTimeSubscriptionHandshakeLink({
123+
auth: {
124+
type: AUTH_TYPE.API_KEY,
125+
apiKey: 'xxxxx'
126+
},
127+
region: 'us-west-2',
128+
url: 'https://apikeytest.testcustomdomain.com/graphql'
129+
});
130+
131+
execute(link, { query }).subscribe({
132+
error: (err) => {
133+
console.log(JSON.stringify(err));
82134
fail;
83135
},
84136
next: (data) => {
@@ -99,7 +151,7 @@ describe("RealTime subscription link", () => {
99151
return "2019-11-13T18:47:04.733Z";
100152
}));
101153
AppSyncRealTimeSubscriptionHandshakeLink.createWebSocket = jest.fn((url, protocol) => {
102-
expect(url).toBe('wss://xxxxx.appsync-realtime-api.amazonaws.com/graphql?header=eyJBdXRob3JpemF0aW9uIjoidG9rZW4iLCJob3N0IjoieHh4eHguYXBwc3luYy1hcGkuYW1hem9uYXdzLmNvbSJ9&payload=e30=');
154+
expect(url).toBe('wss://cognitouserpooltesturl1234.appsync-realtime-api.us-west-2.amazonaws.com/graphql?header=eyJBdXRob3JpemF0aW9uIjoidG9rZW4iLCJob3N0IjoiY29nbml0b3VzZXJwb29sdGVzdHVybDEyMzQuYXBwc3luYy1hcGkudXMtd2VzdC0yLmFtYXpvbmF3cy5jb20ifQ==&payload=e30=');
103155
expect(protocol).toBe('graphql-ws');
104156
done();
105157
return new myWebSocket();
@@ -109,13 +161,50 @@ describe("RealTime subscription link", () => {
109161
type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS,
110162
jwtToken: 'token'
111163
},
112-
region: 'us-east-1',
113-
url: 'https://xxxxx.appsync-api.amazonaws.com/graphql'
164+
region: 'us-west-2',
165+
url: 'https://cognitouserpooltesturl1234.appsync-api.us-west-2.amazonaws.com/graphql'
114166
});
115167

116168
execute(link, { query }).subscribe({
117169
error: (err) => {
118-
console.log({ err });
170+
console.log(JSON.stringify(err));
171+
fail;
172+
},
173+
next: (data) => {
174+
console.log({ data });
175+
done();
176+
},
177+
complete: () => {
178+
console.log('done with this');
179+
done();
180+
}
181+
182+
});
183+
});
184+
185+
test("Initialize WebSocket correctly for COGNITO USER POOLS with custom domain", (done) => {
186+
expect.assertions(2);
187+
jest.spyOn(Date.prototype, 'toISOString').mockImplementation(jest.fn(() => {
188+
return "2019-11-13T18:47:04.733Z";
189+
}));
190+
AppSyncRealTimeSubscriptionHandshakeLink.createWebSocket = jest.fn((url, protocol) => {
191+
expect(url).toBe('wss://cognitouserpools.testcustomdomain.com/graphql/realtime?header=eyJBdXRob3JpemF0aW9uIjoidG9rZW4iLCJob3N0IjoiY29nbml0b3VzZXJwb29scy50ZXN0Y3VzdG9tZG9tYWluLmNvbSJ9&payload=e30=');
192+
expect(protocol).toBe('graphql-ws');
193+
done();
194+
return new myWebSocket();
195+
});
196+
const link = new AppSyncRealTimeSubscriptionHandshakeLink({
197+
auth: {
198+
type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS,
199+
jwtToken: 'token'
200+
},
201+
region: 'us-west-2',
202+
url: 'https://cognitouserpools.testcustomdomain.com/graphql'
203+
});
204+
205+
execute(link, { query }).subscribe({
206+
error: (err) => {
207+
console.log(JSON.stringify(err));
119208
fail;
120209
},
121210
next: (data) => {
@@ -136,7 +225,44 @@ describe("RealTime subscription link", () => {
136225
return "2019-11-13T18:47:04.733Z";
137226
}));
138227
AppSyncRealTimeSubscriptionHandshakeLink.createWebSocket = jest.fn((url, protocol) => {
139-
expect(url).toBe('wss://xxxxx.appsync-realtime-api.amazonaws.com/graphql?header=eyJBdXRob3JpemF0aW9uIjoidG9rZW4iLCJob3N0IjoieHh4eHguYXBwc3luYy1hcGkuYW1hem9uYXdzLmNvbSJ9&payload=e30=');
228+
expect(url).toBe('wss://openidconnecttesturl123456.appsync-realtime-api.us-west-2.amazonaws.com/graphql?header=eyJBdXRob3JpemF0aW9uIjoidG9rZW4iLCJob3N0Ijoib3BlbmlkY29ubmVjdHRlc3R1cmwxMjM0NTYuYXBwc3luYy1hcGkudXMtd2VzdC0yLmFtYXpvbmF3cy5jb20ifQ==&payload=e30=');
229+
expect(protocol).toBe('graphql-ws');
230+
done();
231+
return new myWebSocket();
232+
});
233+
const link = new AppSyncRealTimeSubscriptionHandshakeLink({
234+
auth: {
235+
type: AUTH_TYPE.OPENID_CONNECT,
236+
jwtToken: 'token'
237+
},
238+
region: 'us-west-2',
239+
url: 'https://openidconnecttesturl123456.appsync-api.us-west-2.amazonaws.com/graphql'
240+
});
241+
242+
execute(link, { query }).subscribe({
243+
error: (err) => {
244+
console.log(JSON.stringify(err));
245+
fail;
246+
},
247+
next: (data) => {
248+
console.log({ data });
249+
done();
250+
},
251+
complete: () => {
252+
console.log('done with this');
253+
done();
254+
}
255+
256+
});
257+
});
258+
259+
test("Initialize WebSocket correctly for OPENID_CONNECT with custom domain", (done) => {
260+
expect.assertions(2);
261+
jest.spyOn(Date.prototype, 'toISOString').mockImplementation(jest.fn(() => {
262+
return "2019-11-13T18:47:04.733Z";
263+
}));
264+
AppSyncRealTimeSubscriptionHandshakeLink.createWebSocket = jest.fn((url, protocol) => {
265+
expect(url).toBe('wss://openidconnecttesturl.testcustomdomain.com/graphql/realtime?header=eyJBdXRob3JpemF0aW9uIjoidG9rZW4iLCJob3N0Ijoib3BlbmlkY29ubmVjdHRlc3R1cmwudGVzdGN1c3RvbWRvbWFpbi5jb20ifQ==&payload=e30=');
140266
expect(protocol).toBe('graphql-ws');
141267
done();
142268
return new myWebSocket();
@@ -146,13 +272,13 @@ describe("RealTime subscription link", () => {
146272
type: AUTH_TYPE.OPENID_CONNECT,
147273
jwtToken: 'token'
148274
},
149-
region: 'us-east-1',
150-
url: 'https://xxxxx.appsync-api.amazonaws.com/graphql'
275+
region: 'us-west-2',
276+
url: 'https://openidconnecttesturl.testcustomdomain.com/graphql'
151277
});
152278

153279
execute(link, { query }).subscribe({
154280
error: (err) => {
155-
console.log({ err });
281+
console.log(JSON.stringify(err));
156282
fail;
157283
},
158284
next: (data) => {
@@ -173,7 +299,41 @@ describe("RealTime subscription link", () => {
173299
return "2019-11-13T18:47:04.733Z";
174300
}));
175301
AppSyncRealTimeSubscriptionHandshakeLink.createWebSocket = jest.fn((url, protocol) => {
176-
expect(url).toBe('wss://xxxxx.appsync-realtime-api.amazonaws.com/graphql?header=eyJBdXRob3JpemF0aW9uIjoidG9rZW4iLCJob3N0IjoieHh4eHguYXBwc3luYy1hcGkuYW1hem9uYXdzLmNvbSJ9&payload=e30=');
302+
expect(url).toBe('wss://awslambdatesturl1234567890.appsync-realtime-api.us-west-2.amazonaws.com/graphql?header=eyJBdXRob3JpemF0aW9uIjoidG9rZW4iLCJob3N0IjoiYXdzbGFtYmRhdGVzdHVybDEyMzQ1Njc4OTAuYXBwc3luYy1hcGkudXMtd2VzdC0yLmFtYXpvbmF3cy5jb20ifQ==&payload=e30=');
303+
expect(protocol).toBe('graphql-ws');
304+
done();
305+
return new myWebSocket();
306+
});
307+
const link = new AppSyncRealTimeSubscriptionHandshakeLink({
308+
auth: {
309+
type: AUTH_TYPE.AWS_LAMBDA,
310+
token: 'token'
311+
},
312+
region: 'us-west-2',
313+
url: 'https://awslambdatesturl1234567890.appsync-api.us-west-2.amazonaws.com/graphql'
314+
});
315+
316+
execute(link, { query }).subscribe({
317+
error: (err) => {
318+
fail;
319+
},
320+
next: (data) => {
321+
done();
322+
},
323+
complete: () => {
324+
done();
325+
}
326+
327+
});
328+
})
329+
330+
test('Initialize WebSocket correctly for AWS_LAMBDA with custom domain', (done) => {
331+
expect.assertions(2);
332+
jest.spyOn(Date.prototype, 'toISOString').mockImplementation(jest.fn(() => {
333+
return "2019-11-13T18:47:04.733Z";
334+
}));
335+
AppSyncRealTimeSubscriptionHandshakeLink.createWebSocket = jest.fn((url, protocol) => {
336+
expect(url).toBe('wss://awslambdatesturl.testcustomdomain.com/graphql/realtime?header=eyJBdXRob3JpemF0aW9uIjoidG9rZW4iLCJob3N0IjoiYXdzbGFtYmRhdGVzdHVybC50ZXN0Y3VzdG9tZG9tYWluLmNvbSJ9&payload=e30=');
177337
expect(protocol).toBe('graphql-ws');
178338
done();
179339
return new myWebSocket();
@@ -183,8 +343,8 @@ describe("RealTime subscription link", () => {
183343
type: AUTH_TYPE.AWS_LAMBDA,
184344
token: 'token'
185345
},
186-
region: 'us-east-1',
187-
url: 'https://xxxxx.appsync-api.amazonaws.com/graphql'
346+
region: 'us-west-2',
347+
url: 'https://awslambdatesturl.testcustomdomain.com/graphql'
188348
});
189349

190350
execute(link, { query }).subscribe({

packages/aws-appsync-subscription-link/src/realtime-subscription-handshake-link.ts

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*!
2-
* Copyright 2017-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
* Copyright 2017-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55
import { ApolloLink, Observable, Operation, FetchResult } from "@apollo/client/core";
@@ -55,6 +55,10 @@ const START_ACK_TIMEOUT = 15000;
5555
*/
5656
const DEFAULT_KEEP_ALIVE_TIMEOUT = 5 * 60 * 1000;
5757

58+
const standardDomainPattern = /^https:\/\/\w{26}\.appsync\-api\.\w{2}(?:(?:\-\w{2,})+)\-\d\.amazonaws.com\/graphql$/i;
59+
60+
const customDomainPath = '/realtime';
61+
5862
export class AppSyncRealTimeSubscriptionHandshakeLink extends ApolloLink {
5963
private url: string;
6064
private region: string;
@@ -73,6 +77,11 @@ export class AppSyncRealTimeSubscriptionHandshakeLink extends ApolloLink {
7377
this.auth = theAuth;
7478
}
7579

80+
// Check if url matches standard domain pattern
81+
private isCustomDomain(url: string): boolean {
82+
return url.match(standardDomainPattern) === null;
83+
}
84+
7685
request(operation: Operation) {
7786
const { query, variables } = operation;
7887
const {
@@ -363,10 +372,6 @@ export class AppSyncRealTimeSubscriptionHandshakeLink extends ApolloLink {
363372
if (this.socketStatus === SOCKET_STATUS.CLOSED) {
364373
try {
365374
this.socketStatus = SOCKET_STATUS.CONNECTING;
366-
// Creating websocket url with required query strings
367-
const discoverableEndpoint = AppSyncRealTimeSubscriptionHandshakeLink._discoverAppSyncRealTimeEndpoint(
368-
this.url
369-
);
370375

371376
const payloadString = "{}";
372377
const headerString = JSON.stringify(
@@ -385,6 +390,21 @@ export class AppSyncRealTimeSubscriptionHandshakeLink extends ApolloLink {
385390
const headerQs = Buffer.from(headerString).toString("base64");
386391

387392
const payloadQs = Buffer.from(payloadString).toString("base64");
393+
394+
let discoverableEndpoint = appSyncGraphqlEndpoint;
395+
396+
if (this.isCustomDomain(discoverableEndpoint)) {
397+
discoverableEndpoint = discoverableEndpoint.concat(
398+
customDomainPath
399+
);
400+
} else {
401+
discoverableEndpoint = discoverableEndpoint.replace('appsync-api', 'appsync-realtime-api').replace('gogi-beta', 'grt-beta');
402+
}
403+
404+
discoverableEndpoint = discoverableEndpoint
405+
.replace("https://", "wss://")
406+
.replace('http://', 'ws://')
407+
388408
const awsRealTimeUrl = `${discoverableEndpoint}?header=${headerQs}&payload=${payloadQs}`;
389409

390410
await this._initializeRetryableHandshake({ awsRealTimeUrl });
@@ -790,12 +810,4 @@ export class AppSyncRealTimeSubscriptionHandshakeLink extends ApolloLink {
790810
static createWebSocket(awsRealTimeUrl: string, protocol: string): WebSocket {
791811
return new WebSocket(awsRealTimeUrl, protocol);
792812
}
793-
794-
private static _discoverAppSyncRealTimeEndpoint(url: string): string {
795-
return url
796-
.replace("https://", "wss://")
797-
.replace('http://', 'ws://')
798-
.replace("appsync-api", "appsync-realtime-api")
799-
.replace("gogi-beta", "grt-beta");
800-
}
801813
}

0 commit comments

Comments
 (0)