Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api-graphql): custom subscriptions #13154

Merged
merged 4 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"editor.detectIndentation": false,
"editor.insertSpaces": false,
"editor.tabSize": 4,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workspace settings defined in this file override local VS Code settings, so this effectively forces everyone to use tabSize: 4. Removing it so contributors can use their tab size of choice.

"prettier.requireConfig": true,
"typescript.tsdk": "node_modules/typescript/lib",
"formattingToggle.affects": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1580,6 +1580,32 @@ const amplifyConfig = {
},
},
},
subscriptions: {
onPostLiked: {
name: 'onPostLiked',
isArray: false,
type: {
model: 'Post',
},
isRequired: false,
},
onPostUpdated: {
name: 'onPostUpdated',
isArray: false,
type: {
model: 'Post',
},
isRequired: false,
arguments: {
postId: {
name: 'postId',
isArray: false,
type: 'String',
isRequired: false,
},
},
},
},
},
};
export default amplifyConfig;
30 changes: 21 additions & 9 deletions packages/api-graphql/__tests__/fixtures/modeled/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ const schema = a.schema({
viewCount: a.integer(),
status: a.enum(['draft', 'pending', 'published']),
})
.secondaryIndexes([
a.index('title'),
a.index('description').sortKeys(['viewCount']),
.secondaryIndexes(index => [
index('title'),
index('description').sortKeys(['viewCount']),
Comment on lines +86 to +88
Copy link
Member Author

@iartemiev iartemiev Mar 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lines up the DX with aws-amplify/amplify-data#132

]),
Product: a
.model({
Expand Down Expand Up @@ -120,7 +120,7 @@ const schema = a.schema({
argumentContent: a.string().required(),
})
.returns(a.ref('EchoResult'))
.function('echoFunction')
.handler(a.handler.function('echoFunction'))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Migrating these to the new DX as well. We still have tests in data-schema that ensure the now-deprecated top-level .function functionality is intact.

.authorization([a.allow.public()]),

// custom query returning a primitive type
Expand All @@ -130,23 +130,23 @@ const schema = a.schema({
inputString: a.string().required(),
})
.returns(a.string())
.function('echoFunction')
.handler(a.handler.function('echoFunction'))
.authorization([a.allow.public()]),
echoNestedCustomTypes: a
.query()
.arguments({
input: a.string().required(),
})
.returns(a.ref('ProductTrackingMeta'))
.function('echoFunction')
.handler(a.handler.function('echoFunction'))
.authorization([a.allow.public()]),
echoModelHasNestedTypes: a
.query()
.arguments({
input: a.string().required(),
})
.returns(a.ref('Product'))
.function('echoFunction')
.handler(a.handler.function('echoFunction'))
.authorization([a.allow.public()]),
// custom mutation returning a non-model type
PostLikeResult: a.customType({
Expand All @@ -158,7 +158,7 @@ const schema = a.schema({
postId: a.id().required(),
})
.returns(a.ref('PostLikeResult'))
.function('echoFunction')
.handler(a.handler.function('echoFunction'))
.authorization([a.allow.private()]),

// custom mutation returning a model type
Expand All @@ -182,9 +182,21 @@ const schema = a.schema({
postId: a.id().required(),
})
.returns(a.ref('Post'))
.function('echoFunction')
.handler(a.handler.function('echoFunction'))
.authorization([a.allow.private()]),

onPostLiked: a
.subscription()
.for(a.ref('likePostReturnPost'))
.returns(a.ref('Post'))
.handler(a.handler.custom({ entry: './jsResolver_base.js' })),

onPostUpdated: a
.subscription()
.for(a.ref('Post').mutations(['update']))
.arguments({ postId: a.string() })
.returns(a.ref('Post'))
.handler(a.handler.custom({ entry: './jsResolver_base.js' })),
//#endregion
});

Expand Down
122 changes: 120 additions & 2 deletions packages/api-graphql/__tests__/internals/generateClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
expectSubWithlibraryConfigHeaders,
} from '../utils/expects';
import { Observable, from } from 'rxjs';
import * as internals from '../../src/internals';

const serverManagedFields = {
id: 'some-id',
Expand Down Expand Up @@ -1996,7 +1995,9 @@ describe('generateClient', () => {
const spy = jest.fn(() => from([graphqlMessage]));
(raw.GraphQLAPI as any).appSyncRealTime = { subscribe: spy };

client.models.Note.onCreate().subscribe({
const onC = client.models.Note.onCreate();

onC.subscribe({
next(value) {
expect(spy).toHaveBeenCalledWith(
expect.objectContaining({
Expand Down Expand Up @@ -5181,6 +5182,8 @@ describe('generateClient', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.resetAllMocks();
jest.restoreAllMocks();
iartemiev marked this conversation as resolved.
Show resolved Hide resolved

Amplify.configure(configFixture as any);

jest
Expand Down Expand Up @@ -5392,6 +5395,121 @@ describe('generateClient', () => {
expect(comments[0]).toEqual(expect.objectContaining(listCommentItem));
});

test('can subscribe to custom subscription', done => {
const postToSend = {
__typename: 'Post',
...serverManagedFields,
content: 'a lovely post',
};

const graphqlMessage = {
data: {
onPostLiked: postToSend,
},
};

const client = generateClient<Schema>({ amplify: Amplify });

const spy = jest.fn(() => from([graphqlMessage]));
(raw.GraphQLAPI as any).appSyncRealTime = { subscribe: spy };

const spyGql = jest.spyOn(raw.GraphQLAPI as any, 'graphql');

const expectedOperation = 'subscription';
const expectedFieldAndSelectionSet =
'onPostLiked {id content owner createdAt updatedAt}';

const sub = client.subscriptions.onPostLiked().subscribe({
next(value) {
expect(value).toEqual(expect.objectContaining(postToSend));

expect(spyGql).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
query: expect.stringContaining(expectedOperation),
variables: {},
}),
expect.anything(),
);
expect(spyGql).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
query: expect.stringContaining(expectedFieldAndSelectionSet),
variables: {},
}),
expect.anything(),
);
done();
},
error(error) {
expect(error).toBeUndefined();
done('bad news!');
},
});

sub.unsubscribe();
});
iartemiev marked this conversation as resolved.
Show resolved Hide resolved

test('can subscribe to custom subscription with args', done => {
const postToSend = {
__typename: 'Post',
...serverManagedFields,
postId: 'abc123',
content: 'a lovely post',
};

const graphqlMessage = {
data: {
onPostLiked: postToSend,
},
};

const client = generateClient<Schema>({ amplify: Amplify });

const spy = jest.fn(() => from([graphqlMessage]));
(raw.GraphQLAPI as any).appSyncRealTime = { subscribe: spy };

const spyGql = jest.spyOn(raw.GraphQLAPI as any, 'graphql');

const expectedOperation = 'subscription($postId: String)';
const expectedFieldAndSelectionSet =
'onPostUpdated(postId: $postId) {id content owner createdAt updatedAt}';

const sub = client.subscriptions
.onPostUpdated({ postId: 'abc123' })
.subscribe({
next(value) {
expect(value).toEqual(expect.objectContaining(postToSend));

expect(spyGql).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
query: expect.stringContaining(expectedOperation),

variables: { postId: 'abc123' },
}),
expect.anything(),
);
expect(spyGql).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
query: expect.stringContaining(expectedFieldAndSelectionSet),

variables: { postId: 'abc123' },
}),
expect.anything(),
);
done();
},
error(error) {
expect(error).toBeUndefined();
done('bad news!');
},
});

sub.unsubscribe();
});
iartemiev marked this conversation as resolved.
Show resolved Hide resolved

test('includes client level headers', async () => {
const spy = mockApiResponse({
data: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ describe('server generateClient', () => {
return result;
});

const mockContextSpec = {};
const mockContextSpec = { token: { value: Symbol('test') } };

const result = await client.queries.echo(mockContextSpec, {
argumentContent: 'echo argumentContent value',
Expand Down
4 changes: 2 additions & 2 deletions packages/api-graphql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
},
"homepage": "https://aws-amplify.github.io/",
"devDependencies": {
"@aws-amplify/data-schema": "^0.13.9",
"@aws-amplify/data-schema": "^0.14.1",
"@rollup/plugin-typescript": "11.1.5",
"rollup": "^4.9.6",
"typescript": "5.0.2"
Expand All @@ -87,7 +87,7 @@
"dependencies": {
"@aws-amplify/api-rest": "4.0.21",
"@aws-amplify/core": "6.0.21",
"@aws-amplify/data-schema-types": "^0.7.6",
"@aws-amplify/data-schema-types": "^0.7.11",
"@aws-sdk/types": "3.387.0",
"graphql": "15.8.0",
"rxjs": "^7.8.1",
Expand Down
Loading
Loading