From d18ee988485cd772fae23a9e0d579d40f62976b2 Mon Sep 17 00:00:00 2001 From: David McAfee Date: Wed, 27 Mar 2024 11:31:18 -0700 Subject: [PATCH 1/7] feat(data): add 'sortDirection' support for list and index queries (#13164) --- .vscode/launch.json | 4 +- .../__snapshots__/generateClient.test.ts.snap | 456 ++++++++++++++++-- .../internals/generateClient.test.ts | 150 ++++++ ...rateClientWithAmplifyInstance.test.ts.snap | 263 +++++++++- .../generateClientWithAmplifyInstance.test.ts | 262 +++++----- packages/api-graphql/package.json | 2 +- .../api-graphql/src/internals/APIClient.ts | 19 + packages/api-graphql/src/types/index.ts | 2 + yarn.lock | 45 +- 9 files changed, 1009 insertions(+), 194 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index d0ec7daf54d..f82f2062146 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,12 +10,12 @@ "type": "node", "request": "launch", // The debugger will only run tests for the package specified here: - "cwd": "${workspaceFolder}/packages/datastore", + "cwd": "${workspaceFolder}/packages/api-graphql", "runtimeArgs": [ "--inspect-brk", "${workspaceRoot}/node_modules/.bin/jest", // Optionally specify a single test file to run/debug: - "connectivityHandling.test.ts", + "generateClientWithAmplifyInstance.test.ts", "--runInBand", "--testTimeout", "600000", // 10 min timeout so jest doesn't error while we're stepping through code diff --git a/packages/api-graphql/__tests__/internals/__snapshots__/generateClient.test.ts.snap b/packages/api-graphql/__tests__/internals/__snapshots__/generateClient.test.ts.snap index c18e432675b..b468f595825 100644 --- a/packages/api-graphql/__tests__/internals/__snapshots__/generateClient.test.ts.snap +++ b/packages/api-graphql/__tests__/internals/__snapshots__/generateClient.test.ts.snap @@ -252,8 +252,14 @@ exports[`generateClient basic model operations - authMode: CuP override at the t "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelNoteFilterInput, $limit: Int, $nextToken: String) { - listNotes(filter: $filter, limit: $limit, nextToken: $nextToken) { + "query": "query ($filter: ModelNoteFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listNotes( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { items { id body @@ -488,8 +494,14 @@ exports[`generateClient basic model operations - authMode: CuP override at the t "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelNoteFilterInput, $limit: Int, $nextToken: String) { - listNotes(filter: $filter, limit: $limit, nextToken: $nextToken) { + "query": "query ($filter: ModelNoteFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listNotes( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { items { id body @@ -608,8 +620,14 @@ exports[`generateClient basic model operations - authMode: CuP override at the t "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { - listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { + "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listTodos( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { items { id name @@ -947,8 +965,14 @@ exports[`generateClient basic model operations - authMode: CuP override in the c "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelNoteFilterInput, $limit: Int, $nextToken: String) { - listNotes(filter: $filter, limit: $limit, nextToken: $nextToken) { + "query": "query ($filter: ModelNoteFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listNotes( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { items { id body @@ -1183,8 +1207,14 @@ exports[`generateClient basic model operations - authMode: CuP override in the c "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelNoteFilterInput, $limit: Int, $nextToken: String) { - listNotes(filter: $filter, limit: $limit, nextToken: $nextToken) { + "query": "query ($filter: ModelNoteFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listNotes( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { items { id body @@ -1303,8 +1333,14 @@ exports[`generateClient basic model operations - authMode: CuP override in the c "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { - listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { + "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listTodos( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { items { id name @@ -1642,8 +1678,14 @@ exports[`generateClient basic model operations - authMode: lambda override at th "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelNoteFilterInput, $limit: Int, $nextToken: String) { - listNotes(filter: $filter, limit: $limit, nextToken: $nextToken) { + "query": "query ($filter: ModelNoteFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listNotes( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { items { id body @@ -1881,8 +1923,14 @@ exports[`generateClient basic model operations - authMode: lambda override at th "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelNoteFilterInput, $limit: Int, $nextToken: String) { - listNotes(filter: $filter, limit: $limit, nextToken: $nextToken) { + "query": "query ($filter: ModelNoteFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listNotes( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { items { id body @@ -2007,8 +2055,14 @@ exports[`generateClient basic model operations - authMode: lambda override at th "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { - listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { + "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listTodos( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { items { id name @@ -2346,8 +2400,14 @@ exports[`generateClient basic model operations - authMode: lambda override in th "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelNoteFilterInput, $limit: Int, $nextToken: String) { - listNotes(filter: $filter, limit: $limit, nextToken: $nextToken) { + "query": "query ($filter: ModelNoteFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listNotes( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { items { id body @@ -2585,8 +2645,14 @@ exports[`generateClient basic model operations - authMode: lambda override in th "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelNoteFilterInput, $limit: Int, $nextToken: String) { - listNotes(filter: $filter, limit: $limit, nextToken: $nextToken) { + "query": "query ($filter: ModelNoteFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listNotes( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { items { id body @@ -2711,8 +2777,14 @@ exports[`generateClient basic model operations - authMode: lambda override in th "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { - listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { + "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listTodos( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { items { id name @@ -3258,8 +3330,14 @@ exports[`generateClient basic model operations - custom client and request heade "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { - listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { + "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listTodos( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { items { id name @@ -3306,8 +3384,14 @@ exports[`generateClient basic model operations - custom client and request heade "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { - listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { + "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listTodos( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { items { id name @@ -3666,8 +3750,14 @@ exports[`generateClient basic model operations can lazy load @hasMany 1`] = ` "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelNoteFilterInput, $limit: Int, $nextToken: String) { - listNotes(filter: $filter, limit: $limit, nextToken: $nextToken) { + "query": "query ($filter: ModelNoteFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listNotes( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { items { id body @@ -3748,8 +3838,14 @@ exports[`generateClient basic model operations can lazy load @hasMany with limit "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelNoteFilterInput, $limit: Int, $nextToken: String) { - listNotes(filter: $filter, limit: $limit, nextToken: $nextToken) { + "query": "query ($filter: ModelNoteFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listNotes( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { items { id body @@ -3831,8 +3927,14 @@ exports[`generateClient basic model operations can lazy load @hasMany with nextT "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelNoteFilterInput, $limit: Int, $nextToken: String) { - listNotes(filter: $filter, limit: $limit, nextToken: $nextToken) { + "query": "query ($filter: ModelNoteFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listNotes( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { items { id body @@ -3949,8 +4051,14 @@ exports[`generateClient basic model operations can list() 1`] = ` "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { - listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { + "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listTodos( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { items { id name @@ -3996,8 +4104,14 @@ exports[`generateClient basic model operations can list() with limit 1`] = ` "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { - listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { + "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listTodos( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { items { id name @@ -4044,8 +4158,14 @@ exports[`generateClient basic model operations can list() with nextToken 1`] = ` "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { - listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { + "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listTodos( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { items { id name @@ -4084,6 +4204,100 @@ exports[`generateClient basic model operations can list() with nextToken 1`] = ` ] `; +exports[`generateClient basic model operations can list() with sortDirection (ASC) 1`] = ` +[ + [ + "AmplifyClassV6", + { + "abortController": AbortController {}, + "options": { + "body": { + "query": "query ($filter: ModelThingWithCustomPkFilterInput, $sortDirection: ModelSortDirection, $cpk_cluster_key: String, $cpk_sort_key: String, $limit: Int, $nextToken: String) { + listThingWithCustomPks( + filter: $filter + sortDirection: $sortDirection + cpk_cluster_key: $cpk_cluster_key + cpk_sort_key: $cpk_sort_key + limit: $limit + nextToken: $nextToken + ) { + items { + cpk_cluster_key + cpk_sort_key + otherField + createdAt + updatedAt + } + nextToken + __typename + } +} +", + "variables": { + "cpk_cluster_key": "1", + "sortDirection": "ASC", + }, + }, + "headers": { + "X-Api-Key": "FAKE-KEY", + "x-amz-user-agent": "aws-amplify/latest api/latest framework/latest", + }, + "signingServiceInfo": undefined, + "withCredentials": undefined, + }, + "url": "https://localhost/graphql", + }, + ], +] +`; + +exports[`generateClient basic model operations can list() with sortDirection (DESC) 1`] = ` +[ + [ + "AmplifyClassV6", + { + "abortController": AbortController {}, + "options": { + "body": { + "query": "query ($filter: ModelThingWithCustomPkFilterInput, $sortDirection: ModelSortDirection, $cpk_cluster_key: String, $cpk_sort_key: String, $limit: Int, $nextToken: String) { + listThingWithCustomPks( + filter: $filter + sortDirection: $sortDirection + cpk_cluster_key: $cpk_cluster_key + cpk_sort_key: $cpk_sort_key + limit: $limit + nextToken: $nextToken + ) { + items { + cpk_cluster_key + cpk_sort_key + otherField + createdAt + updatedAt + } + nextToken + __typename + } +} +", + "variables": { + "cpk_cluster_key": "1", + "sortDirection": "DESC", + }, + }, + "headers": { + "X-Api-Key": "FAKE-KEY", + "x-amz-user-agent": "aws-amplify/latest api/latest framework/latest", + }, + "signingServiceInfo": undefined, + "withCredentials": undefined, + }, + "url": "https://localhost/graphql", + }, + ], +] +`; + exports[`generateClient basic model operations can update() 1`] = ` [ [ @@ -4434,8 +4648,14 @@ exports[`generateClient basic model operations with Amplify configuration option "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { - listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { + "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listTodos( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { items { id name @@ -4483,8 +4703,14 @@ exports[`generateClient basic model operations with Amplify configuration option "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { - listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { + "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listTodos( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { items { id name @@ -4868,8 +5094,14 @@ exports[`generateClient custom operations can return model (Post) that with lazy "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelCommentFilterInput, $limit: Int, $nextToken: String) { - listComments(filter: $filter, limit: $limit, nextToken: $nextToken) { + "query": "query ($filter: ModelCommentFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listComments( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { items { id content @@ -5197,11 +5429,12 @@ exports[`generateClient index queries PK and SK index query 1`] = ` "abortController": AbortController {}, "options": { "body": { - "query": "query ($description: String!, $viewCount: ModelIntKeyConditionInput, $filter: ModelSecondaryIndexModelFilterInput, $limit: Int, $nextToken: String) { + "query": "query ($description: String!, $viewCount: ModelIntKeyConditionInput, $filter: ModelSecondaryIndexModelFilterInput, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) { listByDescriptionAndViewCount( description: $description viewCount: $viewCount filter: $filter + sortDirection: $sortDirection limit: $limit nextToken: $nextToken ) { @@ -5240,6 +5473,112 @@ exports[`generateClient index queries PK and SK index query 1`] = ` ] `; +exports[`generateClient index queries PK and SK index query, with sort direction (ascending) 1`] = ` +[ + [ + "AmplifyClassV6", + { + "abortController": AbortController {}, + "options": { + "body": { + "query": "query ($description: String!, $viewCount: ModelIntKeyConditionInput, $filter: ModelSecondaryIndexModelFilterInput, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) { + listByDescriptionAndViewCount( + description: $description + viewCount: $viewCount + filter: $filter + sortDirection: $sortDirection + limit: $limit + nextToken: $nextToken + ) { + items { + id + title + description + viewCount + status + createdAt + updatedAt + } + nextToken + __typename + } +} +", + "variables": { + "description": "match", + "sortDirection": "ASC", + "viewCount": { + "lt": 4, + }, + }, + }, + "headers": { + "Authorization": "amplify-config-auth-token", + "X-Api-Key": "FAKE-KEY", + "x-amz-user-agent": "aws-amplify/latest api/latest framework/latest", + }, + "signingServiceInfo": undefined, + "withCredentials": undefined, + }, + "url": "https://localhost/graphql", + }, + ], +] +`; + +exports[`generateClient index queries PK and SK index query, with sort direction (descending) 1`] = ` +[ + [ + "AmplifyClassV6", + { + "abortController": AbortController {}, + "options": { + "body": { + "query": "query ($description: String!, $viewCount: ModelIntKeyConditionInput, $filter: ModelSecondaryIndexModelFilterInput, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) { + listByDescriptionAndViewCount( + description: $description + viewCount: $viewCount + filter: $filter + sortDirection: $sortDirection + limit: $limit + nextToken: $nextToken + ) { + items { + id + title + description + viewCount + status + createdAt + updatedAt + } + nextToken + __typename + } +} +", + "variables": { + "description": "match", + "sortDirection": "DESC", + "viewCount": { + "lt": 4, + }, + }, + }, + "headers": { + "Authorization": "amplify-config-auth-token", + "X-Api-Key": "FAKE-KEY", + "x-amz-user-agent": "aws-amplify/latest api/latest framework/latest", + }, + "signingServiceInfo": undefined, + "withCredentials": undefined, + }, + "url": "https://localhost/graphql", + }, + ], +] +`; + exports[`generateClient index queries PK-only index query 1`] = ` [ [ @@ -5248,10 +5587,11 @@ exports[`generateClient index queries PK-only index query 1`] = ` "abortController": AbortController {}, "options": { "body": { - "query": "query ($title: String!, $filter: ModelSecondaryIndexModelFilterInput, $limit: Int, $nextToken: String) { + "query": "query ($title: String!, $filter: ModelSecondaryIndexModelFilterInput, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) { listByTitle( title: $title filter: $filter + sortDirection: $sortDirection limit: $limit nextToken: $nextToken ) { @@ -5295,8 +5635,14 @@ exports[`generateClient observeQuery can paginate through initial results 1`] = "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { - listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { + "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listTodos( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { items { id name @@ -5332,8 +5678,14 @@ exports[`generateClient observeQuery can paginate through initial results 1`] = "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { - listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { + "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listTodos( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { items { id name diff --git a/packages/api-graphql/__tests__/internals/generateClient.test.ts b/packages/api-graphql/__tests__/internals/generateClient.test.ts index 233bddb64f2..428600cdb4e 100644 --- a/packages/api-graphql/__tests__/internals/generateClient.test.ts +++ b/packages/api-graphql/__tests__/internals/generateClient.test.ts @@ -382,6 +382,58 @@ describe('generateClient', () => { expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); }); + test('can list() with sortDirection (ASC)', async () => { + const spy = mockApiResponse({ + data: { + listThingWithCustomPks: { + items: [ + { + __typename: 'ThingWithCustomPk', + ...serverManagedFields, + cpk_cluster_key: '1', + cpk_sort_key: 'a', + }, + ], + }, + }, + }); + + const client = generateClient({ amplify: Amplify }); + + const { data } = await client.models.ThingWithCustomPk.list({ + cpk_cluster_key: '1', + sortDirection: 'ASC', + }); + + expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); + }); + + test('can list() with sortDirection (DESC)', async () => { + const spy = mockApiResponse({ + data: { + listThingWithCustomPks: { + items: [ + { + __typename: 'ThingWithCustomPk', + ...serverManagedFields, + cpk_cluster_key: '1', + cpk_sort_key: 'c', + }, + ], + }, + }, + }); + + const client = generateClient({ amplify: Amplify }); + + const { data } = await client.models.ThingWithCustomPk.list({ + cpk_cluster_key: '1', + sortDirection: 'DESC', + }); + + expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); + }); + test('can update()', async () => { const spy = mockApiResponse({ data: { @@ -5098,6 +5150,104 @@ describe('generateClient', () => { }), ); }); + + test('PK and SK index query, with sort direction (ascending)', async () => { + const spy = mockApiResponse({ + data: { + listByDescriptionAndViewCount: { + items: [ + { + __typename: 'SecondaryIndexModel', + ...serverManagedFields, + title: 'first', + description: 'match', + viewCount: 1, + }, + { + __typename: 'SecondaryIndexModel', + ...serverManagedFields, + title: 'second', + description: 'match', + viewCount: 2, + }, + { + __typename: 'SecondaryIndexModel', + ...serverManagedFields, + title: 'third', + description: 'match', + viewCount: 3, + }, + ], + }, + }, + }); + + const client = generateClient({ amplify: Amplify }); + + const { data } = + await client.models.SecondaryIndexModel.listByDescriptionAndViewCount( + { + description: 'match', + viewCount: { lt: 4 }, + }, + { + sortDirection: 'ASC', + }, + ); + + expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); + + expect(data.length).toBe(3); + }); + + test('PK and SK index query, with sort direction (descending)', async () => { + const spy = mockApiResponse({ + data: { + listByDescriptionAndViewCount: { + items: [ + { + __typename: 'SecondaryIndexModel', + ...serverManagedFields, + title: 'third', + description: 'match', + viewCount: 3, + }, + { + __typename: 'SecondaryIndexModel', + ...serverManagedFields, + title: 'second', + description: 'match', + viewCount: 2, + }, + { + __typename: 'SecondaryIndexModel', + ...serverManagedFields, + title: 'first', + description: 'match', + viewCount: 1, + }, + ], + }, + }, + }); + + const client = generateClient({ amplify: Amplify }); + + const { data } = + await client.models.SecondaryIndexModel.listByDescriptionAndViewCount( + { + description: 'match', + viewCount: { lt: 4 }, + }, + { + sortDirection: 'DESC', + }, + ); + + expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); + + expect(data.length).toBe(3); + }); }); describe('custom operations', () => { diff --git a/packages/api-graphql/__tests__/internals/server/__snapshots__/generateClientWithAmplifyInstance.test.ts.snap b/packages/api-graphql/__tests__/internals/server/__snapshots__/generateClientWithAmplifyInstance.test.ts.snap index 68565aa84e9..d35a077077c 100644 --- a/packages/api-graphql/__tests__/internals/server/__snapshots__/generateClientWithAmplifyInstance.test.ts.snap +++ b/packages/api-graphql/__tests__/internals/server/__snapshots__/generateClientWithAmplifyInstance.test.ts.snap @@ -31,7 +31,268 @@ exports[`server generateClient with cookies can custom query 1`] = ` ] `; -exports[`server generateClient with request can custom query 1`] = ` +exports[`server generateClient with cookies can list 1`] = ` +[ + [ + "AmplifyClassV6", + { + "abortController": AbortController {}, + "options": { + "body": { + "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listTodos( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { + items { + id + name + description + status + tags + createdAt + updatedAt + todoMetaId + owner + } + nextToken + __typename + } +} +", + "variables": { + "filter": { + "name": { + "contains": "name", + }, + }, + }, + }, + "headers": { + "X-Api-Key": "FAKE-KEY", + "x-amz-user-agent": "aws-amplify/latest api/latest framework/latest", + }, + "signingServiceInfo": undefined, + "withCredentials": undefined, + }, + "url": "https://localhost/graphql", + }, + ], +] +`; + +exports[`server generateClient with cookies can list with limit 1`] = ` +[ + [ + "AmplifyClassV6", + { + "abortController": AbortController {}, + "options": { + "body": { + "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listTodos( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { + items { + id + name + description + status + tags + createdAt + updatedAt + todoMetaId + owner + } + nextToken + __typename + } +} +", + "variables": { + "filter": { + "name": { + "contains": "name", + }, + }, + "limit": 5, + }, + }, + "headers": { + "X-Api-Key": "FAKE-KEY", + "x-amz-user-agent": "aws-amplify/latest api/latest framework/latest", + }, + "signingServiceInfo": undefined, + "withCredentials": undefined, + }, + "url": "https://localhost/graphql", + }, + ], +] +`; + +exports[`server generateClient with cookies can list with nextToken 1`] = ` +[ + [ + "AmplifyClassV6", + { + "abortController": AbortController {}, + "options": { + "body": { + "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listTodos( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { + items { + id + name + description + status + tags + createdAt + updatedAt + todoMetaId + owner + } + nextToken + __typename + } +} +", + "variables": { + "filter": { + "name": { + "contains": "name", + }, + }, + "nextToken": "some-token", + }, + }, + "headers": { + "X-Api-Key": "FAKE-KEY", + "x-amz-user-agent": "aws-amplify/latest api/latest framework/latest", + }, + "signingServiceInfo": undefined, + "withCredentials": undefined, + }, + "url": "https://localhost/graphql", + }, + ], +] +`; + +exports[`server generateClient with cookies can list with sort direction (ascending) 1`] = ` +[ + [ + "AmplifyClassV6", + { + "abortController": AbortController {}, + "options": { + "body": { + "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listTodos( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { + items { + id + name + description + status + tags + createdAt + updatedAt + todoMetaId + owner + } + nextToken + __typename + } +} +", + "variables": { + "id": "some-id", + "sortDirection": "ASC", + }, + }, + "headers": { + "X-Api-Key": "FAKE-KEY", + "x-amz-user-agent": "aws-amplify/latest api/latest framework/latest", + }, + "signingServiceInfo": undefined, + "withCredentials": undefined, + }, + "url": "https://localhost/graphql", + }, + ], +] +`; + +exports[`server generateClient with cookies can list with sort direction (descending) 1`] = ` +[ + [ + "AmplifyClassV6", + { + "abortController": AbortController {}, + "options": { + "body": { + "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { + listTodos( + filter: $filter + sortDirection: $sortDirection + id: $id + limit: $limit + nextToken: $nextToken + ) { + items { + id + name + description + status + tags + createdAt + updatedAt + todoMetaId + owner + } + nextToken + __typename + } +} +", + "variables": { + "id": "some-id", + "sortDirection": "DESC", + }, + }, + "headers": { + "X-Api-Key": "FAKE-KEY", + "x-amz-user-agent": "aws-amplify/latest api/latest framework/latest", + }, + "signingServiceInfo": undefined, + "withCredentials": undefined, + }, + "url": "https://localhost/graphql", + }, + ], +] +`; + +exports[`server generateClient with cookies with request can custom query 1`] = ` [ [ "AmplifyClassV6", diff --git a/packages/api-graphql/__tests__/internals/server/generateClientWithAmplifyInstance.test.ts b/packages/api-graphql/__tests__/internals/server/generateClientWithAmplifyInstance.test.ts index 1bf1ea76af9..b1b23b86b9e 100644 --- a/packages/api-graphql/__tests__/internals/server/generateClientWithAmplifyInstance.test.ts +++ b/packages/api-graphql/__tests__/internals/server/generateClientWithAmplifyInstance.test.ts @@ -81,40 +81,7 @@ describe('server generateClient', () => { filter: { name: { contains: 'name' } }, }); - expect(spy).toHaveBeenCalledWith( - expect.any(AmplifyClassV6), - expect.objectContaining({ - options: expect.objectContaining({ - headers: expect.objectContaining({ - 'X-Api-Key': 'FAKE-KEY', - }), - body: { - query: expect.stringContaining( - 'listTodos(filter: $filter, limit: $limit, nextToken: $nextToken)', - ), - variables: { - filter: { - name: { - contains: 'name', - }, - }, - }, - }, - }), - }), - ); - - expect(spy).toHaveBeenCalledWith( - expect.any(AmplifyClassV6), - expect.objectContaining({ - options: expect.objectContaining({ - body: expect.objectContaining({ - // match nextToken in selection set - query: expect.stringMatching(/^\s*nextToken\s*$/m), - }), - }), - }), - ); + expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); expect(data.length).toBe(1); expect(data[0]).toEqual( @@ -162,29 +129,7 @@ describe('server generateClient', () => { nextToken: 'some-token', }); - expect(spy).toHaveBeenCalledWith( - expect.any(AmplifyClassV6), - expect.objectContaining({ - options: expect.objectContaining({ - headers: expect.objectContaining({ - 'X-Api-Key': 'FAKE-KEY', - }), - body: { - query: expect.stringContaining( - 'listTodos(filter: $filter, limit: $limit, nextToken: $nextToken)', - ), - variables: { - filter: { - name: { - contains: 'name', - }, - }, - nextToken: 'some-token', - }, - }, - }), - }), - ); + expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); expect(spy).toHaveBeenCalledWith( expect.any(AmplifyClassV6), @@ -233,29 +178,7 @@ describe('server generateClient', () => { limit: 5, }); - expect(spy).toHaveBeenCalledWith( - expect.any(AmplifyClassV6), - expect.objectContaining({ - options: expect.objectContaining({ - headers: expect.objectContaining({ - 'X-Api-Key': 'FAKE-KEY', - }), - body: { - query: expect.stringContaining( - 'listTodos(filter: $filter, limit: $limit, nextToken: $nextToken)', - ), - variables: { - filter: { - name: { - contains: 'name', - }, - }, - limit: 5, - }, - }, - }), - }), - ); + expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); expect(spy).toHaveBeenCalledWith( expect.any(AmplifyClassV6), @@ -270,14 +193,21 @@ describe('server generateClient', () => { ); }); - test('can custom query', async () => { + test('can list with sort direction (ascending)', async () => { Amplify.configure(configFixture as any); const config = Amplify.getConfig(); const spy = mockApiResponse({ data: { - echo: { - resultContent: 'echo result content', + listTodos: { + items: [ + { + __typename: 'Todo', + ...serverManagedFields, + name: 'some name', + description: 'something something', + }, + ], }, }, }); @@ -292,86 +222,156 @@ describe('server generateClient', () => { config: config, }); - const result = await client.queries.echo({ - argumentContent: 'echo argumentContent value', + const { data } = await client.models.Todo.list({ + id: 'some-id', + sortDirection: 'ASC', }); expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); - expect(result?.data).toEqual({ - resultContent: 'echo result content', - }); - }); - }); - describe('with request', () => { - test('subscriptions are disabled', () => { - const client = generateClientWithAmplifyInstance< - Schema, - V6ClientSSRRequest - >({ - amplify: null, - config: config, - }); - - expect(() => { - // @ts-expect-error - client.models.Note.onCreate().subscribe(); - }).toThrow(); }); - - test('contextSpec param gets passed through to client.graphql', async () => { + test('can list with sort direction (descending)', async () => { Amplify.configure(configFixture as any); const config = Amplify.getConfig(); + const spy = mockApiResponse({ + data: { + listTodos: { + items: [ + { + __typename: 'Todo', + ...serverManagedFields, + name: 'some name', + description: 'something something', + }, + ], + }, + }, + }); + + const getAmplify = async (fn: any) => await fn(Amplify); + const client = generateClientWithAmplifyInstance< Schema, - V6ClientSSRRequest + V6ClientSSRCookies >({ - amplify: null, + amplify: getAmplify, config: config, }); - const mockContextSpec = {}; - - const spy = jest.spyOn(client, 'graphql').mockImplementation(async () => { - const result: any = {}; - return result; + const { data } = await client.models.Todo.list({ + id: 'some-id', + sortDirection: 'DESC', }); - await client.models.Note.list(mockContextSpec); - - expect(spy).toHaveBeenCalledWith( - mockContextSpec, - expect.objectContaining({ - query: expect.stringContaining('listNotes'), - }), - {}, - ); + expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); }); test('can custom query', async () => { Amplify.configure(configFixture as any); const config = Amplify.getConfig(); + const spy = mockApiResponse({ + data: { + echo: { + resultContent: 'echo result content', + }, + }, + }); + + const getAmplify = async (fn: any) => await fn(Amplify); + const client = generateClientWithAmplifyInstance< Schema, - V6ClientSSRRequest + V6ClientSSRCookies >({ - amplify: null, + amplify: getAmplify, config: config, }); - const spy = jest.spyOn(client, 'graphql').mockImplementation(async () => { - const result: any = {}; - return result; + const result = await client.queries.echo({ + argumentContent: 'echo argumentContent value', }); - const mockContextSpec = { token: { value: Symbol('test') } }; + expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); + expect(result?.data).toEqual({ + resultContent: 'echo result content', + }); + }); + describe('with request', () => { + test('subscriptions are disabled', () => { + const client = generateClientWithAmplifyInstance< + Schema, + V6ClientSSRRequest + >({ + amplify: null, + config: config, + }); + + expect(() => { + // @ts-expect-error + client.models.Note.onCreate().subscribe(); + }).toThrow(); + }); - const result = await client.queries.echo(mockContextSpec, { - argumentContent: 'echo argumentContent value', + test('contextSpec param gets passed through to client.graphql', async () => { + Amplify.configure(configFixture as any); + const config = Amplify.getConfig(); + + const client = generateClientWithAmplifyInstance< + Schema, + V6ClientSSRRequest + >({ + amplify: null, + config: config, + }); + + const mockContextSpec = {}; + + const spy = jest + .spyOn(client, 'graphql') + .mockImplementation(async () => { + const result: any = {}; + return result; + }); + + await client.models.Note.list(mockContextSpec); + + expect(spy).toHaveBeenCalledWith( + mockContextSpec, + expect.objectContaining({ + query: expect.stringContaining('listNotes'), + }), + {}, + ); }); - expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); + test('can custom query', async () => { + Amplify.configure(configFixture as any); + const config = Amplify.getConfig(); + + const client = generateClientWithAmplifyInstance< + Schema, + V6ClientSSRRequest + >({ + amplify: null, + config: config, + }); + + const spy = jest + .spyOn(client, 'graphql') + .mockImplementation(async () => { + const result: any = {}; + return result; + }); + + const mockContextSpec = { token: { value: Symbol('test') } }; + + const result = await client.queries.echo(mockContextSpec, { + argumentContent: 'echo argumentContent value', + }); + + expect(normalizePostGraphqlCalls(spy)).toMatchSnapshot(); + }); }); }); }); diff --git a/packages/api-graphql/package.json b/packages/api-graphql/package.json index f4d1f7b2e9e..d6ca3a5341f 100644 --- a/packages/api-graphql/package.json +++ b/packages/api-graphql/package.json @@ -87,7 +87,7 @@ "dependencies": { "@aws-amplify/api-rest": "4.0.23", "@aws-amplify/core": "6.0.23", - "@aws-amplify/data-schema-types": "^0.7.11", + "@aws-amplify/data-schema-types": "^0.7.14", "@aws-sdk/types": "3.387.0", "graphql": "15.8.0", "rxjs": "^7.8.1", diff --git a/packages/api-graphql/src/internals/APIClient.ts b/packages/api-graphql/src/internals/APIClient.ts index 1231fb42b2e..d6f39844401 100644 --- a/packages/api-graphql/src/internals/APIClient.ts +++ b/packages/api-graphql/src/internals/APIClient.ts @@ -732,6 +732,15 @@ export function generateGraphQLDocument( graphQLArguments ?? (graphQLArguments = { filter: `Model${name}FilterInput`, + sortDirection: 'ModelSortDirection', + ...[primaryKeyFieldName, ...sortKeyFieldNames].reduce( + (acc: Record, fieldName) => { + acc[fieldName] = `${fields[fieldName].type}`; + + return acc; + }, + [], + ), limit: 'Int', nextToken: 'String', }); @@ -745,6 +754,7 @@ export function generateGraphQLDocument( (graphQLArguments = { ...indexQueryArgs!, filter: `Model${name}FilterInput`, + sortDirection: 'ModelSortDirection', limit: 'Int', nextToken: 'String', }); @@ -858,6 +868,10 @@ export function buildGraphQLVariables( if (arg?.filter) { variables.filter = arg.filter; } + if (arg?.sortDirection) { + variables.sortDirection = arg.sortDirection; + variables[primaryKeyFieldName] = arg[primaryKeyFieldName]; + } if (arg?.nextToken) { variables.nextToken = arg.nextToken; } @@ -877,6 +891,11 @@ export function buildGraphQLVariables( if (arg?.filter) { variables.filter = arg.filter; } + + if (arg?.sortDirection) { + variables.sortDirection = arg.sortDirection; + } + if (arg?.nextToken) { variables.nextToken = arg.nextToken; } diff --git a/packages/api-graphql/src/types/index.ts b/packages/api-graphql/src/types/index.ts index e65dbe44c0e..f2a019e2aae 100644 --- a/packages/api-graphql/src/types/index.ts +++ b/packages/api-graphql/src/types/index.ts @@ -7,6 +7,7 @@ import { CustomQueries, CustomSubscriptions, EnumTypes, + ModelSortDirection, ModelTypes, } from '@aws-amplify/data-schema-types'; import { DocumentNode, GraphQLError, Source } from 'graphql'; @@ -464,6 +465,7 @@ export type QueryArgs = Record; export interface ListArgs extends Record { selectionSet?: string[]; filter?: Record; + sortDirection?: ModelSortDirection; headers?: CustomHeaders; } diff --git a/yarn.lock b/yarn.lock index 5fbce33ef43..fcb4b9d484e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15,11 +15,12 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@aws-amplify/data-schema-types@*", "@aws-amplify/data-schema-types@^0.7.11": - version "0.7.11" - resolved "https://registry.yarnpkg.com/@aws-amplify/data-schema-types/-/data-schema-types-0.7.11.tgz#d6c4b5be551fa0600f6bbf2b512f19e39a20e481" - integrity sha512-K16p1NF1entoNBDQUnH/wK3exmOg+AtF96uFaj3SxQDOBad+wBnzVHZNWaoemlX2ISnuH/fXbuFvB5D7w76zbQ== +"@aws-amplify/data-schema-types@*", "@aws-amplify/data-schema-types@^0.7.14": + version "0.7.14" + resolved "https://registry.npmjs.org/@aws-amplify/data-schema-types/-/data-schema-types-0.7.14.tgz#75504d040d7a85e1d1328e388f35e5ebd02572f4" + integrity sha512-zvo1j6NljHsV62KnEz560rTx9jJ1KfTz0OOqZjW/CP1AtWdlakM2ADBAn9z4AL+bKrt31CrAr55ZOAo6Uk+LTw== dependencies: + "@aws-amplify/plugin-types" "^0.9.0-beta.1" rxjs "^7.8.1" "@aws-amplify/data-schema@^0.14.1": @@ -30,6 +31,11 @@ "@aws-amplify/data-schema-types" "*" "@types/aws-lambda" "^8.10.134" +"@aws-amplify/plugin-types@^0.9.0-beta.1": + version "0.9.0-beta.1" + resolved "https://registry.npmjs.org/@aws-amplify/plugin-types/-/plugin-types-0.9.0-beta.1.tgz#066af0646b379b4c999668253e87754e67fc7427" + integrity sha512-5eJ2SYoXbq4ZSvBjCgStXSejNOKuBkxYVXqOXZP4xP5C9iIpUYG36s9xP+IdhWDm9K/yxklp8OUMr8YGc0g7tw== + "@aws-crypto/crc32@3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@aws-crypto/crc32/-/crc32-3.0.0.tgz#07300eca214409c33e3ff769cd5697b57fdd38fa" @@ -14199,7 +14205,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -14267,7 +14282,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -14281,6 +14296,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -15485,7 +15507,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -15512,6 +15534,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From d4a48f1ace550bf478ebbfa41bca8b83d7dd52e4 Mon Sep 17 00:00:00 2001 From: Hui Zhao <10602282+HuiSF@users.noreply.github.com> Date: Wed, 27 Mar 2024 13:40:37 -0700 Subject: [PATCH 2/7] fix(api-graphql): update error handling (#13177) chore(api-graphql): improve error handling - Use GraphApiError to create errors to be thrown * error message field remained the same as before * added recoverySuggestion field for each error case - Created createGraphQLResultWithError utility for rewrapping error into GraphQLResult format --- .../api-graphql/__tests__/GraphQLAPI.test.ts | 117 +++++++++++++----- .../src/internals/InternalGraphQLAPI.ts | 106 ++++++++-------- .../isGraphQLResponseWithErrors.ts | 15 +++ .../api-graphql/src/utils/errors/constants.ts | 56 +++++++++ .../errors/createGraphQLResultWithError.ts | 15 +++ .../api-graphql/src/utils/errors/index.ts | 1 + .../src/utils/errors/repackageAuthError.ts | 18 ++- packages/api-graphql/tsconfig.json | 1 - .../api-rest/src/apis/common/internalPost.ts | 28 +++++ packages/api-rest/src/internals/index.ts | 21 +--- .../src/utils/createCancellableOperation.ts | 2 + packages/aws-amplify/package.json | 2 +- 12 files changed, 262 insertions(+), 120 deletions(-) create mode 100644 packages/api-graphql/src/internals/utils/runtimeTypeGuards/isGraphQLResponseWithErrors.ts create mode 100644 packages/api-graphql/src/utils/errors/constants.ts create mode 100644 packages/api-graphql/src/utils/errors/createGraphQLResultWithError.ts diff --git a/packages/api-graphql/__tests__/GraphQLAPI.test.ts b/packages/api-graphql/__tests__/GraphQLAPI.test.ts index 29599a9cb81..e3936363270 100644 --- a/packages/api-graphql/__tests__/GraphQLAPI.test.ts +++ b/packages/api-graphql/__tests__/GraphQLAPI.test.ts @@ -14,6 +14,9 @@ import { import { GetThreadQuery } from './fixtures/with-types/API'; import { AWSAppSyncRealTimeProvider } from '../src/Providers/AWSAppSyncRealTimeProvider'; import { Observable, of } from 'rxjs'; +import { GraphQLApiError } from '../src/utils/errors'; +import { NO_ENDPOINT } from '../src/utils/errors/constants'; +import { GraphQLError } from 'graphql'; const serverManagedFields = { id: 'some-id', @@ -61,13 +64,13 @@ const client = { isCancelError, } as V6Client; -afterEach(() => { - jest.restoreAllMocks(); -}); +const mockFetchAuthSession = (Amplify as any).Auth + .fetchAuthSession as jest.Mock; describe('API test', () => { afterEach(() => { jest.clearAllMocks(); + jest.restoreAllMocks(); }); describe('graphql test', () => { @@ -738,32 +741,9 @@ describe('API test', () => { }); test('multi-auth default case api-key, OIDC as auth mode, but no federatedSign', async () => { - Amplify.configure({ - API: { - GraphQL: { - defaultAuthMode: 'apiKey', - apiKey: 'FAKE-KEY', - endpoint: 'https://localhost/graphql', - region: 'local-host-h4x', - }, - }, - }); - - Amplify.configure({ - API: { - GraphQL: { - defaultAuthMode: 'apiKey', - apiKey: 'FAKE-KEY', - endpoint: 'https://localhost/graphql', - region: 'local-host-h4x', - }, - }, - }); - }); - - test('multi-auth default case api-key, OIDC as auth mode, but no federatedSign', async () => { - const prevMockAccessToken = mockAccessToken; - mockAccessToken = null; + mockFetchAuthSession.mockRejectedValueOnce( + new Error('mock failing fetchAuthSession() call here.'), + ); Amplify.configure({ API: { @@ -806,9 +786,6 @@ describe('API test', () => { authMode: 'oidc', }), ).rejects.toThrow('No current user'); - - // Cleanup: - mockAccessToken = prevMockAccessToken; }); test('multi-auth using CUP as auth mode, but no userpool', async () => { @@ -1342,5 +1319,81 @@ describe('API test', () => { }, ); }); + + test('throws a GraphQLResult with NO_ENDPOINT error when endpoint is not configured', () => { + const expectedGraphQLApiError = new GraphQLApiError(NO_ENDPOINT); + + Amplify.configure({ + API: { + GraphQL: { + defaultAuthMode: 'apiKey', + apiKey: 'FAKE-KEY', + region: 'local-host-h4x', + } as any, + }, + }); + + const graphqlVariables = { id: 'some-id' }; + + expect(() => + client.graphql({ + query: typedQueries.getThread, + variables: graphqlVariables, + authMode: 'iam', + }), + ).rejects.toEqual( + expect.objectContaining({ + errors: expect.arrayContaining([ + new GraphQLError( + expectedGraphQLApiError.message, + null, + null, + null, + null, + expectedGraphQLApiError, + ), + ]), + }), + ); + }); + + test('throws a GraphQLResult with NetworkError when the `post()` API throws for network error', () => { + const postAPIThrownError = new Error('Network error'); + jest + .spyOn((raw.GraphQLAPI as any)._api, 'post') + .mockRejectedValueOnce(postAPIThrownError); + + Amplify.configure({ + API: { + GraphQL: { + defaultAuthMode: 'userPool', + endpoint: 'https://localhost/graphql', + region: 'local-host-h4x', + }, + }, + }); + + const graphqlVariables = { id: 'some-id' }; + + expect( + client.graphql({ + query: typedQueries.getThread, + variables: graphqlVariables, + }), + ).rejects.toEqual( + expect.objectContaining({ + errors: expect.arrayContaining([ + new GraphQLError( + postAPIThrownError.message, + null, + null, + null, + null, + postAPIThrownError, + ), + ]), + }), + ); + }); }); }); diff --git a/packages/api-graphql/src/internals/InternalGraphQLAPI.ts b/packages/api-graphql/src/internals/InternalGraphQLAPI.ts index fd3f276b2b6..c0fff702918 100644 --- a/packages/api-graphql/src/internals/InternalGraphQLAPI.ts +++ b/packages/api-graphql/src/internals/InternalGraphQLAPI.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 import { DocumentNode, - GraphQLError, OperationDefinitionNode, OperationTypeNode, parse, @@ -25,14 +24,20 @@ import { import { CustomHeaders, RequestOptions } from '@aws-amplify/data-schema-types'; import { AWSAppSyncRealTimeProvider } from '../Providers/AWSAppSyncRealTimeProvider'; -import { - GraphQLAuthError, - GraphQLOperation, - GraphQLOptions, - GraphQLResult, -} from '../types'; +import { GraphQLOperation, GraphQLOptions, GraphQLResult } from '../types'; import { resolveConfig, resolveLibraryOptions } from '../utils'; -import { repackageUnauthError } from '../utils/errors/repackageAuthError'; +import { repackageUnauthorizedError } from '../utils/errors/repackageAuthError'; +import { + NO_API_KEY, + NO_AUTH_TOKEN_HEADER, + NO_ENDPOINT, + NO_SIGNED_IN_USER, + NO_VALID_AUTH_TOKEN, + NO_VALID_CREDENTIALS, +} from '../utils/errors/constants'; +import { GraphQLApiError, createGraphQLResultWithError } from '../utils/errors'; + +import { isGraphQLResponseWithErrors } from './utils/runtimeTypeGuards/isGraphQLResponseWithErrors'; const USER_AGENT_HEADER = 'x-amz-user-agent'; @@ -76,7 +81,7 @@ export class InternalGraphQLAPIClass { switch (authMode) { case 'apiKey': if (!apiKey) { - throw new Error(GraphQLAuthError.NO_API_KEY); + throw new GraphQLApiError(NO_API_KEY); } headers = { 'X-Api-Key': apiKey, @@ -85,33 +90,44 @@ export class InternalGraphQLAPIClass { case 'iam': { const session = await amplify.Auth.fetchAuthSession(); if (session.credentials === undefined) { - throw new Error(GraphQLAuthError.NO_CREDENTIALS); + throw new GraphQLApiError(NO_VALID_CREDENTIALS); } break; } case 'oidc': - case 'userPool': + case 'userPool': { + let token: string | undefined; + try { - const token = ( + token = ( await amplify.Auth.fetchAuthSession() ).tokens?.accessToken.toString(); - - if (!token) { - throw new Error(GraphQLAuthError.NO_FEDERATED_JWT); - } - headers = { - Authorization: token, - }; } catch (e) { - throw new Error(GraphQLAuthError.NO_CURRENT_USER); + // fetchAuthSession failed + throw new GraphQLApiError({ + ...NO_SIGNED_IN_USER, + underlyingError: e, + }); + } + + // `fetchAuthSession()` succeeded but didn't return `tokens`. + // This may happen when unauthenticated access is enabled and there is + // no user signed in. + if (!token) { + throw new GraphQLApiError(NO_VALID_AUTH_TOKEN); } + + headers = { + Authorization: token, + }; break; + } case 'lambda': if ( typeof additionalHeaders === 'object' && !additionalHeaders.Authorization ) { - throw new Error(GraphQLAuthError.NO_AUTH_TOKEN); + throw new GraphQLApiError(NO_AUTH_TOKEN_HEADER); } headers = { @@ -350,18 +366,15 @@ export class InternalGraphQLAPIClass { const endpoint = customEndpoint || appSyncGraphqlEndpoint; if (!endpoint) { - const error = new GraphQLError('No graphql endpoint provided.'); - // TODO(Eslint): refactor this to throw an Error instead of a plain object - // eslint-disable-next-line no-throw-literal - throw { - data: {}, - errors: [error], - }; + throw createGraphQLResultWithError(new GraphQLApiError(NO_ENDPOINT)); } let response: any; try { + // See the inline doc of the REST `post()` API for possible errors to be thrown. + // As these errors are catastrophic they should be caught and handled by GraphQL + // API consumers. const { body: responseBody } = await this._api.post(amplify, { url: new AmplifyUrl(endpoint), options: { @@ -373,39 +386,20 @@ export class InternalGraphQLAPIClass { abortController, }); - const result = await responseBody.json(); - - response = result; - } catch (err) { - // If the exception is because user intentionally - // cancelled the request, do not modify the exception - // so that clients can identify the exception correctly. - if (this.isCancelError(err)) { - throw err; + response = await responseBody.json(); + } catch (error) { + if (this.isCancelError(error)) { + throw error; } - response = { - data: {}, - errors: [ - new GraphQLError( - (err as any).message, - null, - null, - null, - null, - err as any, - ), - ], - }; + response = createGraphQLResultWithError(error as any); } - const { errors } = response; - - if (errors && errors.length) { - throw repackageUnauthError(response); + if (isGraphQLResponseWithErrors(response)) { + throw repackageUnauthorizedError(response); } - return response; + return response as unknown as GraphQLResult; } /** @@ -463,7 +457,7 @@ export class InternalGraphQLAPIClass { .pipe( catchError(e => { if (e.errors) { - throw repackageUnauthError(e); + throw repackageUnauthorizedError(e); } throw e; }), diff --git a/packages/api-graphql/src/internals/utils/runtimeTypeGuards/isGraphQLResponseWithErrors.ts b/packages/api-graphql/src/internals/utils/runtimeTypeGuards/isGraphQLResponseWithErrors.ts new file mode 100644 index 00000000000..64efa23a2cb --- /dev/null +++ b/packages/api-graphql/src/internals/utils/runtimeTypeGuards/isGraphQLResponseWithErrors.ts @@ -0,0 +1,15 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { GraphQLResult } from '../../../types'; + +export function isGraphQLResponseWithErrors( + response: unknown, +): response is GraphQLResult { + if (!response) { + return false; + } + const r = response as GraphQLResult; + + return Array.isArray(r.errors) && r.errors.length > 0; +} diff --git a/packages/api-graphql/src/utils/errors/constants.ts b/packages/api-graphql/src/utils/errors/constants.ts new file mode 100644 index 00000000000..c621897ff08 --- /dev/null +++ b/packages/api-graphql/src/utils/errors/constants.ts @@ -0,0 +1,56 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AmplifyErrorParams } from '@aws-amplify/core/internals/utils'; + +import { GraphQLAuthError } from '../../types'; + +export const NO_API_KEY: AmplifyErrorParams = { + name: 'NoApiKey', + // ideal: No API key configured. + message: GraphQLAuthError.NO_API_KEY, + recoverySuggestion: + 'The API request was made with `authMode: "apiKey"` but no API Key was passed into `Amplify.configure()`. Review if your API key is passed into the `Amplify.configure()` function.', +}; + +export const NO_VALID_CREDENTIALS: AmplifyErrorParams = { + name: 'NoCredentials', + // ideal: No auth credentials available. + message: GraphQLAuthError.NO_CREDENTIALS, + recoverySuggestion: `The API request was made with \`authMode: "iam"\` but no authentication credentials are available. + +If you intended to make a request using an authenticated role, review if your user is signed in before making the request. + +If you intend to make a request using an unauthenticated role or also known as "guest access", verify if "Auth.Cognito.allowGuestAccess" is set to "true" in the \`Amplify.configure()\` function.`, +}; + +export const NO_VALID_AUTH_TOKEN: AmplifyErrorParams = { + name: 'NoValidAuthTokens', + // ideal: No valid JWT auth token provided to make the API request.. + message: GraphQLAuthError.NO_FEDERATED_JWT, + recoverySuggestion: + 'If you intended to make an authenticated API request, review if the current user is signed in.', +}; + +export const NO_SIGNED_IN_USER: AmplifyErrorParams = { + name: 'NoSignedUser', + // ideal: Couldn't retrieve authentication credentials to make the API request. + message: GraphQLAuthError.NO_CURRENT_USER, + recoverySuggestion: + 'Review the underlying exception field for more details. If you intended to make an authenticated API request, review if the current user is signed in.', +}; + +export const NO_AUTH_TOKEN_HEADER: AmplifyErrorParams = { + name: 'NoAuthorizationHeader', + // ideal: Authorization header not specified. + message: GraphQLAuthError.NO_AUTH_TOKEN, + recoverySuggestion: + 'The API request was made with `authMode: "lambda"` but no `authToken` is set. Review if a valid authToken is passed into the request options or in the `Amplify.configure()` function.', +}; + +export const NO_ENDPOINT: AmplifyErrorParams = { + name: 'NoEndpoint', + message: 'No GraphQL endpoint configured in `Amplify.configure()`.', + recoverySuggestion: + 'Review if the GraphQL API endpoint is set in the `Amplify.configure()` function.', +}; diff --git a/packages/api-graphql/src/utils/errors/createGraphQLResultWithError.ts b/packages/api-graphql/src/utils/errors/createGraphQLResultWithError.ts new file mode 100644 index 00000000000..d53782731d1 --- /dev/null +++ b/packages/api-graphql/src/utils/errors/createGraphQLResultWithError.ts @@ -0,0 +1,15 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { GraphQLError } from 'graphql'; + +import { GraphQLResult } from '../../types'; + +export const createGraphQLResultWithError = ( + error: Error, +): GraphQLResult => { + return { + data: {} as T, + errors: [new GraphQLError(error.message, null, null, null, null, error)], + }; +}; diff --git a/packages/api-graphql/src/utils/errors/index.ts b/packages/api-graphql/src/utils/errors/index.ts index 54c8faa30a8..12d7659575d 100644 --- a/packages/api-graphql/src/utils/errors/index.ts +++ b/packages/api-graphql/src/utils/errors/index.ts @@ -4,3 +4,4 @@ export { GraphQLApiError } from './GraphQLApiError'; export { assertValidationError } from './assertValidationError'; export { APIValidationErrorCode, validationErrorMap } from './validation'; +export { createGraphQLResultWithError } from './createGraphQLResultWithError'; diff --git a/packages/api-graphql/src/utils/errors/repackageAuthError.ts b/packages/api-graphql/src/utils/errors/repackageAuthError.ts index e179c67436d..2a92c1f2f96 100644 --- a/packages/api-graphql/src/utils/errors/repackageAuthError.ts +++ b/packages/api-graphql/src/utils/errors/repackageAuthError.ts @@ -1,23 +1,21 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmplifyErrorParams } from '@aws-amplify/core/internals/utils'; - -interface ErrorObject { - errors: AmplifyErrorParams[]; -} +import { GraphQLResult } from '../../types'; /** * Checks to see if the given response or subscription message contains an - * unauth error. If it does, it changes the error message to include instructions + * Unauthorized error. If it does, it changes the error message to include instructions * for the app developer. */ -export function repackageUnauthError(content: T): T { +export function repackageUnauthorizedError>( + content: T, +): T { if (content.errors && Array.isArray(content.errors)) { content.errors.forEach(e => { - if (isUnauthError(e)) { + if (isUnauthorizedError(e)) { e.message = 'Unauthorized'; - e.recoverySuggestion = + (e as any).recoverySuggestion = `If you're calling an Amplify-generated API, make sure ` + `to set the "authMode" in generateClient({ authMode: '...' }) to the backend authorization ` + `rule's auth provider ('apiKey', 'userPool', 'iam', 'oidc', 'lambda')`; @@ -28,7 +26,7 @@ export function repackageUnauthError(content: T): T { return content; } -function isUnauthError(error: any): boolean { +function isUnauthorizedError(error: any): boolean { // Error pattern corresponding to appsync calls if (error?.originalError?.name?.startsWith('UnauthorizedException')) { return true; diff --git a/packages/api-graphql/tsconfig.json b/packages/api-graphql/tsconfig.json index 11d9d83dd6a..162af6598a0 100644 --- a/packages/api-graphql/tsconfig.json +++ b/packages/api-graphql/tsconfig.json @@ -2,7 +2,6 @@ "extends": "../../tsconfig.json", "compilerOptions": { "strictNullChecks": true, - "baseUrl": ".", "paths": { "@aws-amplify/data-schema-types": [ "../../node_modules/@aws-amplify/data-schema-types" diff --git a/packages/api-rest/src/apis/common/internalPost.ts b/packages/api-rest/src/apis/common/internalPost.ts index 4f7a6ccc416..835c3581d56 100644 --- a/packages/api-rest/src/apis/common/internalPost.ts +++ b/packages/api-rest/src/apis/common/internalPost.ts @@ -5,6 +5,7 @@ import { AmplifyClassV6 } from '@aws-amplify/core'; import { InternalPostInput, RestApiResponse } from '../../types'; import { createCancellableOperation } from '../../utils'; +import { CanceledError } from '../../errors'; import { transferHandler } from './handler'; @@ -23,6 +24,33 @@ const cancelTokenMap = new WeakMap, AbortController>(); /** * @internal + * + * REST POST handler to send GraphQL request to given endpoint. By default, it will use IAM to authorize + * the request. In some auth modes, the IAM auth has to be disabled. Here's how to set up the request auth correctly: + * * If auth mode is 'iam', you MUST NOT set 'authorization' header and 'x-api-key' header, since it would disable IAM + * auth. You MUST also set 'input.options.signingServiceInfo' option. + * * The including 'input.options.signingServiceInfo.service' and 'input.options.signingServiceInfo.region' are + * optional. If omitted, the signing service and region will be inferred from url. + * * If auth mode is 'none', you MUST NOT set 'options.signingServiceInfo' option. + * * If auth mode is 'apiKey', you MUST set 'x-api-key' custom header. + * * If auth mode is 'oidc' or 'lambda' or 'userPool', you MUST set 'authorization' header. + * + * To make the internal post cancellable, you must also call `updateRequestToBeCancellable()` with the promise from + * internal post call and the abort controller supplied to the internal post call. + * + * @param amplify the AmplifyClassV6 instance - it may be the singleton used on Web, or an instance created within + * a context created by `runWithAmplifyServerContext` + * @param postInput an object of {@link InternalPostInput} + * @param postInput.url The URL that the POST request sends to + * @param postInput.options Options of the POST request + * @param postInput.abortController The abort controller used to cancel the POST request + * @returns a {@link RestApiResponse} + * + * @throws an {@link Error} with `Network error` as the `message` when the external resource is unreachable due to one + * of the following reasons: + * 1. no network connection + * 2. CORS error + * @throws a {@link CanceledError} when the ongoing POST request get cancelled */ export const post = ( amplify: AmplifyClassV6, diff --git a/packages/api-rest/src/internals/index.ts b/packages/api-rest/src/internals/index.ts index 3ce95df5371..78ef4399d0f 100644 --- a/packages/api-rest/src/internals/index.ts +++ b/packages/api-rest/src/internals/index.ts @@ -1,26 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { post as internalPost } from '../apis/common/internalPost'; - -/** - * Internal-only REST POST handler to send GraphQL request to given endpoint. By default, it will use IAM to authorize - * the request. In some auth modes, the IAM auth has to be disabled. Here's how to set up the request auth correctly: - * * If auth mode is 'iam', you MUST NOT set 'authorization' header and 'x-api-key' header, since it would disable IAM - * auth. You MUST also set 'input.options.signingServiceInfo' option. - * * The including 'input.options.signingServiceInfo.service' and 'input.options.signingServiceInfo.region' are - * optional. If omitted, the signing service and region will be inferred from url. - * * If auth mode is 'none', you MUST NOT set 'options.signingServiceInfo' option. - * * If auth mode is 'apiKey', you MUST set 'x-api-key' custom header. - * * If auth mode is 'oidc' or 'lambda' or 'userPool', you MUST set 'authorization' header. - * - * To make the internal post cancellable, you must also call `updateRequestToBeCancellable()` with the promise from - * internal post call and the abort controller supplied to the internal post call. - * - * @internal - */ -export const post = internalPost; - +export { post } from '../apis/common/internalPost'; export { cancel, updateRequestToBeCancellable, diff --git a/packages/api-rest/src/utils/createCancellableOperation.ts b/packages/api-rest/src/utils/createCancellableOperation.ts index f75010faabe..f2ec4b5c714 100644 --- a/packages/api-rest/src/utils/createCancellableOperation.ts +++ b/packages/api-rest/src/utils/createCancellableOperation.ts @@ -67,6 +67,8 @@ export function createCancellableOperation( const canceledError = new CanceledError({ ...(message && { message }), underlyingError: error, + recoverySuggestion: + 'The API request was explicitly canceled. If this is not intended, validate if you called the `cancel()` function on the API request erroneously.', }); logger.debug(error); throw canceledError; diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index 43fc02d8cbb..52433bd9d7f 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -335,7 +335,7 @@ "name": "[API] generateClient (AppSync)", "path": "./dist/esm/api/index.mjs", "import": "{ generateClient }", - "limit": "38.00 kB" + "limit": "38.5 kB" }, { "name": "[API] REST API handlers", From 962179bcc3c443e621e9bb7b522b638b0951df08 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Wed, 27 Mar 2024 14:42:11 -0700 Subject: [PATCH 3/7] fix(auth): signUp should always return a userId (#13181) * fix(auth): signup should always return a userId * add unit tests * Update packages/auth/__tests__/providers/cognito/signUp.test.ts Co-authored-by: Chris F <5827964+cshfang@users.noreply.github.com> * address feedback --------- Co-authored-by: Ashwin Kumar Co-authored-by: Chris F <5827964+cshfang@users.noreply.github.com> --- .../providers/cognito/signUp.test.ts | 237 +++++++++++++----- .../auth/src/providers/cognito/apis/signUp.ts | 2 + 2 files changed, 173 insertions(+), 66 deletions(-) diff --git a/packages/auth/__tests__/providers/cognito/signUp.test.ts b/packages/auth/__tests__/providers/cognito/signUp.test.ts index bccf0af0078..afcdfccb580 100644 --- a/packages/auth/__tests__/providers/cognito/signUp.test.ts +++ b/packages/auth/__tests__/providers/cognito/signUp.test.ts @@ -23,6 +23,8 @@ jest.mock( '../../../src/providers/cognito/utils/clients/CognitoIdentityProvider', ); +const userId = '1234567890'; + describe('signUp', () => { const { user1 } = authAPITestParams; // assert mocks @@ -32,81 +34,184 @@ describe('signUp', () => { setUpGetConfig(Amplify); }); - beforeEach(() => { - mockSignUp.mockResolvedValue(authAPITestParams.signUpHttpCallResult); - }); + describe('Happy Path Cases:', () => { + beforeEach(() => { + mockSignUp.mockResolvedValue(authAPITestParams.signUpHttpCallResult); + }); - afterEach(() => { - mockSignUp.mockReset(); - }); + afterEach(() => { + mockSignUp.mockReset(); + }); + + it('should call SignUp service client with correct params', async () => { + await signUp({ + username: user1.username, + password: user1.password, + options: { + userAttributes: { email: user1.email }, + }, + }); + expect(mockSignUp).toHaveBeenCalledWith( + { + region: 'us-west-2', + userAgentValue: expect.any(String), + }, + { + ClientMetadata: undefined, + Password: user1.password, + UserAttributes: [{ Name: 'email', Value: user1.email }], + Username: user1.username, + ValidationData: undefined, + ClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + }, + ); + expect(mockSignUp).toHaveBeenCalledTimes(1); + }); - it('should call signUp and return a result', async () => { - const result = await signUp({ - username: user1.username, - password: user1.password, - options: { - userAttributes: { email: user1.email }, - }, + it('should return `CONFIRM_SIGN_UP` step when user isn`t confirmed yet', async () => { + const result = await signUp({ + username: user1.username, + password: user1.password, + options: { + userAttributes: { email: user1.email }, + }, + }); + expect(result).toEqual({ + isSignUpComplete: false, + nextStep: { + signUpStep: 'CONFIRM_SIGN_UP', + codeDeliveryDetails: { + destination: user1.email, + deliveryMedium: 'EMAIL', + attributeName: 'email', + }, + }, + userId, + }); }); - expect(result).toEqual({ - isSignUpComplete: false, - nextStep: { - signUpStep: 'CONFIRM_SIGN_UP', - codeDeliveryDetails: { - destination: user1.email, - deliveryMedium: 'EMAIL', - attributeName: 'email', + + it('should return `DONE` step when user is confirmed', async () => { + mockSignUp.mockResolvedValue({ + UserConfirmed: true, + UserSub: userId, + }); + const result = await signUp({ + username: user1.username, + password: user1.password, + options: { + userAttributes: { email: user1.email }, + }, + }); + expect(result).toEqual({ + isSignUpComplete: true, + nextStep: { + signUpStep: 'DONE', }, - }, - userId: '1234567890', + userId, + }); }); - expect(mockSignUp).toHaveBeenCalledWith( - { - region: 'us-west-2', - userAgentValue: expect.any(String), - }, - { - ClientMetadata: undefined, - Password: user1.password, - UserAttributes: [{ Name: 'email', Value: user1.email }], - Username: user1.username, - ValidationData: undefined, - ClientId: '111111-aaaaa-42d8-891d-ee81a1549398', - }, - ); - expect(mockSignUp).toHaveBeenCalledTimes(1); - }); - it('should throw an error when username is empty', async () => { - expect.assertions(2); - try { - await signUp({ username: '', password: user1.password }); - } catch (error: any) { - expect(error).toBeInstanceOf(AuthError); - expect(error.name).toBe(AuthValidationErrorCode.EmptySignUpUsername); - } - }); + it('should return `COMPLETE_AUTO_SIGN_IN` step with `isSignUpComplete` false when autoSignIn is enabled and user isn`t confirmed yet', async () => { + // set up signUpVerificationMethod as link in auth config + (Amplify.getConfig as any).mockReturnValue({ + Auth: { + Cognito: { + userPoolClientId: '111111-aaaaa-42d8-891d-ee81a1549398', + userPoolId: 'us-west-2_zzzzz', + identityPoolId: 'us-west-2:xxxxxx', + signUpVerificationMethod: 'link', + }, + }, + }); - it('should throw an error when password is empty', async () => { - expect.assertions(2); - try { - await signUp({ username: user1.username, password: '' }); - } catch (error: any) { - expect(error).toBeInstanceOf(AuthError); - expect(error.name).toBe(AuthValidationErrorCode.EmptySignUpPassword); - } + const result = await signUp({ + username: user1.username, + password: user1.password, + options: { + userAttributes: { email: user1.email }, + autoSignIn: true, + }, + }); + + expect(result).toEqual({ + isSignUpComplete: false, + nextStep: { + signUpStep: 'COMPLETE_AUTO_SIGN_IN', + codeDeliveryDetails: { + destination: user1.email, + deliveryMedium: 'EMAIL', + attributeName: 'email', + }, + }, + userId, + }); + }); + + it('should return `COMPLETE_AUTO_SIGN_IN` step with `isSignUpComplete` true when autoSignIn is enabled and user is confirmed', async () => { + mockSignUp.mockResolvedValue({ + UserConfirmed: true, + UserSub: userId, + }); + + const result = await signUp({ + username: user1.username, + password: user1.password, + options: { + userAttributes: { email: user1.email }, + autoSignIn: true, + }, + }); + + expect(result).toEqual({ + isSignUpComplete: true, + nextStep: { + signUpStep: 'COMPLETE_AUTO_SIGN_IN', + }, + userId, + }); + }); }); - it('should throw an error when service returns an error response', async () => { - expect.assertions(2); - mockSignUp.mockImplementation(() => { - throw getMockError(SignUpException.InvalidParameterException); + describe('Error Path Cases:', () => { + beforeEach(() => { + mockSignUp.mockResolvedValue(authAPITestParams.signUpHttpCallResult); + }); + + afterEach(() => { + mockSignUp.mockReset(); + }); + + it('should throw an error when username is empty', async () => { + expect.assertions(2); + try { + await signUp({ username: '', password: user1.password }); + } catch (error: any) { + expect(error).toBeInstanceOf(AuthError); + expect(error.name).toBe(AuthValidationErrorCode.EmptySignUpUsername); + } + }); + + it('should throw an error when password is empty', async () => { + expect.assertions(2); + try { + await signUp({ username: user1.username, password: '' }); + } catch (error: any) { + expect(error).toBeInstanceOf(AuthError); + expect(error.name).toBe(AuthValidationErrorCode.EmptySignUpPassword); + } + }); + + it('should throw an error when service returns an error response', async () => { + expect.assertions(2); + mockSignUp.mockImplementation(() => { + throw getMockError(SignUpException.InvalidParameterException); + }); + try { + await signUp({ username: user1.username, password: user1.password }); + } catch (error: any) { + expect(error).toBeInstanceOf(AuthError); + expect(error.name).toBe(SignUpException.InvalidParameterException); + } }); - try { - await signUp({ username: user1.username, password: user1.password }); - } catch (error: any) { - expect(error).toBeInstanceOf(AuthError); - expect(error.name).toBe(SignUpException.InvalidParameterException); - } }); }); diff --git a/packages/auth/src/providers/cognito/apis/signUp.ts b/packages/auth/src/providers/cognito/apis/signUp.ts index aff6ac83028..69aca1b528a 100644 --- a/packages/auth/src/providers/cognito/apis/signUp.ts +++ b/packages/auth/src/providers/cognito/apis/signUp.ts @@ -96,6 +96,7 @@ export async function signUp(input: SignUpInput): Promise { nextStep: { signUpStep: 'COMPLETE_AUTO_SIGN_IN', }, + userId: UserSub, }; } else if (isSignUpComplete(clientOutput) && !isAutoSignInStarted()) { return { @@ -103,6 +104,7 @@ export async function signUp(input: SignUpInput): Promise { nextStep: { signUpStep: 'DONE', }, + userId: UserSub, }; } else if ( !isSignUpComplete(clientOutput) && From c4f1432c4cc5ebeca7da9bb37c45fda5ea6bd5c5 Mon Sep 17 00:00:00 2001 From: Hui Zhao <10602282+HuiSF@users.noreply.github.com> Date: Thu, 28 Mar 2024 10:58:49 -0700 Subject: [PATCH 4/7] chore: enable eslint for __tests__ for adapter-nextjs aws-amplify and core (#13108) * chore(repo): setup the base for lint __tests__ codebase * chore(adapter-nextjs): run yarn lint:fix * chore(adapter-nextjs): manual fix linter reported erros * chore(aws-amplify): run yarn lint:fix Please enter the commit message for your changes. Lines starting * chore(aws-amplify): add camelcase var name exception: phone_number * chore(core): run yarn lint:fix * chore(core): manual fix linter reported errors under __tests__ * apply suggestions * turn off node callback function convention check * do not mock console functions globaly but noopify them * apply suggestions * Revert "do not mock console functions globaly but noopify them" This reverts commit 913b11e2959e20014b093910f8661da1dc7a2b2e. * refactor a no-op function * remove // eslint-disable-next-line no-new --- .eslintrc.js | 26 +++- .../api/generateServerClient.test.ts | 7 +- .../__tests__/createServerRunner.test.ts | 9 +- .../adapter-nextjs/__tests__/mocks/headers.ts | 5 +- ...torageAdapterFromNextServerContext.test.ts | 17 ++- ...WSCredentialsAndIdentityIdProvider.test.ts | 7 +- .../createUserPoolsTokenProvider.test.ts | 2 +- .../runWithAmplifyServerContext.test.ts | 3 +- .../__tests__/initSingleton.test.ts | 4 +- .../BackgroundProcessManager.test.ts | 86 ++++++----- .../core/__tests__/Cache/StorageCache.test.ts | 4 +- .../Cache/StorageCacheCommon.test.ts | 46 +++--- .../__tests__/Cache/utils/CacheList.test.ts | 4 +- .../__tests__/Cache/utils/cacheUtils.test.ts | 2 +- packages/core/__tests__/ConsoleLogger.test.ts | 24 +-- packages/core/__tests__/DateUtils.test.ts | 21 ++- packages/core/__tests__/Hub.test.ts | 10 +- .../core/__tests__/JS-browser-runtime.test.ts | 3 +- packages/core/__tests__/Mutex.test.ts | 60 +++++--- .../Platform/customUserAgent.test.ts | 20 ++- .../core/__tests__/Platform/userAgent.test.ts | 13 +- packages/core/__tests__/Retry.test.ts | 10 +- packages/core/__tests__/ServiceWorker.test.ts | 20 +-- packages/core/__tests__/Signer.test.ts | 35 +++-- packages/core/__tests__/StringUtils.test.ts | 4 +- .../adapterCore/serverContext.test.ts | 2 +- .../getCredentialsForIdentity.test.ts | 2 +- .../awsClients/cognitoIdentity/getId.test.ts | 2 +- .../pinpoint/getInAppMessages.test.ts | 2 +- .../awsClients/pinpoint/putEvents.test.ts | 2 +- .../pinpoint/updateEndpoint.test.ts | 2 +- .../__tests__/awsClients/testUtils/data.ts | 1 + .../clients/composeApiHandler.test.ts | 22 ++- .../clients/composeTransferHandler.test.ts | 22 ++- packages/core/__tests__/clients/fetch.test.ts | 8 +- .../middleware/retry/middleware.test.ts | 19 ++- .../middleware/signing/middleware.test.ts | 3 +- .../signer/signatureV4/presignUrl.test.ts | 6 +- .../signer/signatureV4/signRequest.test.ts | 6 +- .../signatureV4/testUtils/signingTestTable.ts | 1 + .../signatureV4/utils/getSignature.test.ts | 2 +- .../core/__tests__/parseAWSExports.test.ts | 2 +- .../pinpoint/apis/flushEvents.test.ts | 2 +- .../providers/pinpoint/apis/record.test.ts | 5 +- .../testUtils/getExpectedPutEventsInput.ts | 2 +- .../pinpoint/apis/updateEndpoint.test.ts | 2 + .../__tests__/singleton/Singleton.test.ts | 36 ++--- .../__tests__/storage/CookieStorage.test.ts | 18 +-- .../__tests__/storage/DefaultStorage.test.ts | 9 +- .../__tests__/storage/SessionStorage.test.ts | 1 + packages/core/__tests__/utils.test.ts | 4 +- .../utils/generateRandomString.test.ts | 1 + .../globalHelpers.native.test.ts | 8 +- .../queuedStorage.native.test.ts | 37 +++-- .../utils/queuedStorage/queuedStorage.test.ts | 139 ++++++++++++++++-- 55 files changed, 508 insertions(+), 302 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index eb968e3fb61..73687a1364c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -35,8 +35,25 @@ module.exports = { 'setupTests.ts', 'jest.setup.*', 'jest.config.*', - // temporarily disable lint on __tests__ - '__tests__', + // 'packages/adapter-nextjs/__tests__', + 'packages/analytics/__tests__', + 'packages/api/__tests__', + 'packages/api-graphql/__tests__', + 'packages/api-rest/__tests__', + 'packages/auth/__tests__', + // 'packages/aws-amplify/__tests__', + // 'packages/core/__tests__', + 'packages/datastore/__tests__', + 'packages/datastore-storage-adapter/__tests__', + 'packages/geo/__tests__', + 'packages/interactions/__tests__', + 'packages/notifications/__tests__', + 'packages/predictions/__tests__', + 'packages/pubsub/__tests__', + 'packages/react-native/__tests__', + 'packages/rtn-push-notification/__tests__', + 'packages/rtn-web-browser/__tests__', + 'packages/storage/__tests__', // will enable lint by packages // 'adapter-nextjs', // 'packages/analytics', @@ -63,6 +80,10 @@ module.exports = { 'error', { allow: [ + // exceptions for core package + 'phone_number', + 'search_indices', + // exceptions for api packages 'graphql_headers', // exceptions for the legacy config /^(aws_|amazon_)/, @@ -105,6 +126,7 @@ module.exports = { 'no-useless-constructor': 'off', 'no-trailing-spaces': 'error', 'no-return-await': 'error', + 'n/no-callback-literal': 'off', 'object-shorthand': 'error', 'prefer-destructuring': 'off', 'promise/catch-or-return': [ diff --git a/packages/adapter-nextjs/__tests__/api/generateServerClient.test.ts b/packages/adapter-nextjs/__tests__/api/generateServerClient.test.ts index dfc038de44a..31e28946721 100644 --- a/packages/adapter-nextjs/__tests__/api/generateServerClient.test.ts +++ b/packages/adapter-nextjs/__tests__/api/generateServerClient.test.ts @@ -1,11 +1,12 @@ import { ResourcesConfig } from '@aws-amplify/core'; + import { generateServerClientUsingCookies, generateServerClientUsingReqRes, } from '../../src/api'; import { - getAmplifyConfig, createRunWithAmplifyServerContext, + getAmplifyConfig, } from '../../src/utils'; import { NextApiRequestMock, NextApiResponseMock } from '../mocks/headers'; import { createServerRunnerForAPI } from '../../src/api/createServerRunnerForAPI'; @@ -59,7 +60,7 @@ describe('generateServerClientUsingCookies', () => { }); it('should call createRunWithAmplifyServerContext to create runWithAmplifyServerContext function', async () => { - const cookies = (await headers).cookies; + const { cookies } = await headers; generateServerClientUsingCookies({ config: mockAmplifyConfig, cookies }); expect(mockCreateRunWithAmplifyServerContext).toHaveBeenCalledWith({ @@ -94,7 +95,7 @@ describe('generateServerClient', () => { })); jest.mock('@aws-amplify/core/internals/adapter-core', () => ({ - getAmplifyServerContext: () => {}, + getAmplifyServerContext: jest.fn(), })); const client = generateServerClientUsingReqRes({ diff --git a/packages/adapter-nextjs/__tests__/createServerRunner.test.ts b/packages/adapter-nextjs/__tests__/createServerRunner.test.ts index 17ff383720f..678d1fbf42c 100644 --- a/packages/adapter-nextjs/__tests__/createServerRunner.test.ts +++ b/packages/adapter-nextjs/__tests__/createServerRunner.test.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { ResourcesConfig, sharedInMemoryStorage } from '@aws-amplify/core'; + import { NextServer } from '../src/types'; const mockAmplifyConfig: ResourcesConfig = { @@ -50,7 +51,7 @@ describe('createServerRunner', () => { parseAWSExports: mockParseAWSExports, })); - createServerRunner = require('../src').createServerRunner; + ({ createServerRunner } = require('../src')); }); afterEach(() => { @@ -76,7 +77,7 @@ describe('createServerRunner', () => { describe('runWithAmplifyServerContext', () => { describe('when amplifyConfig.Auth is not defined', () => { it('should call runWithAmplifyServerContextCore without Auth library options', () => { - const mockAmplifyConfig: ResourcesConfig = { + const mockAmplifyConfigWithoutAuth: ResourcesConfig = { Analytics: { Pinpoint: { appId: 'app-id', @@ -85,12 +86,12 @@ describe('createServerRunner', () => { }, }; const { runWithAmplifyServerContext } = createServerRunner({ - config: mockAmplifyConfig, + config: mockAmplifyConfigWithoutAuth, }); const operation = jest.fn(); runWithAmplifyServerContext({ operation, nextServerContext: null }); expect(mockRunWithAmplifyServerContextCore).toHaveBeenCalledWith( - mockAmplifyConfig, + mockAmplifyConfigWithoutAuth, {}, operation, ); diff --git a/packages/adapter-nextjs/__tests__/mocks/headers.ts b/packages/adapter-nextjs/__tests__/mocks/headers.ts index 9023b16b0af..f8b5b27be78 100644 --- a/packages/adapter-nextjs/__tests__/mocks/headers.ts +++ b/packages/adapter-nextjs/__tests__/mocks/headers.ts @@ -1,10 +1,11 @@ +import { Socket } from 'net'; +import { IncomingMessage } from 'http'; + import { NextApiRequest, NextApiResponse } from 'next/index.js'; import { NextApiRequestCookies, NextApiRequestQuery, } from 'next/dist/server/api-utils/index.js'; -import { Socket } from 'net'; -import { IncomingMessage } from 'http'; export type NextApiRequestOptions = Partial; export class NextApiRequestMock diff --git a/packages/adapter-nextjs/__tests__/utils/createCookieStorageAdapterFromNextServerContext.test.ts b/packages/adapter-nextjs/__tests__/utils/createCookieStorageAdapterFromNextServerContext.test.ts index 857c0369514..0023fb7aa80 100644 --- a/packages/adapter-nextjs/__tests__/utils/createCookieStorageAdapterFromNextServerContext.test.ts +++ b/packages/adapter-nextjs/__tests__/utils/createCookieStorageAdapterFromNextServerContext.test.ts @@ -1,18 +1,21 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { IncomingMessage, ServerResponse } from 'http'; +import { Socket } from 'net'; + import { enableFetchMocks } from 'jest-fetch-mock'; +import { NextRequest, NextResponse } from 'next/server.js'; +import { cookies } from 'next/headers.js'; + +import { + DATE_IN_THE_PAST, + createCookieStorageAdapterFromNextServerContext, +} from '../../src/utils/createCookieStorageAdapterFromNextServerContext'; // Make global Request available during test enableFetchMocks(); -import { NextRequest, NextResponse } from 'next/server.js'; -import { cookies } from 'next/headers.js'; -import { createCookieStorageAdapterFromNextServerContext } from '../../src/utils/createCookieStorageAdapterFromNextServerContext'; -import { DATE_IN_THE_PAST } from '../../src/utils/createCookieStorageAdapterFromNextServerContext'; -import { IncomingMessage, ServerResponse } from 'http'; -import { Socket } from 'net'; - jest.mock('next/headers', () => ({ cookies: jest.fn(), })); diff --git a/packages/aws-amplify/__tests__/adapterCore/authProvidersFactories/cognito/createAWSCredentialsAndIdentityIdProvider.test.ts b/packages/aws-amplify/__tests__/adapterCore/authProvidersFactories/cognito/createAWSCredentialsAndIdentityIdProvider.test.ts index 92f291e5bd4..9df53b6f2b8 100644 --- a/packages/aws-amplify/__tests__/adapterCore/authProvidersFactories/cognito/createAWSCredentialsAndIdentityIdProvider.test.ts +++ b/packages/aws-amplify/__tests__/adapterCore/authProvidersFactories/cognito/createAWSCredentialsAndIdentityIdProvider.test.ts @@ -5,11 +5,8 @@ import { CognitoAWSCredentialsAndIdentityIdProvider, DefaultIdentityIdStore, } from '@aws-amplify/auth/cognito'; -import { - CredentialsAndIdentityIdProvider, - AuthConfig, - KeyValueStorageInterface, -} from '@aws-amplify/core'; +import { AuthConfig, KeyValueStorageInterface } from '@aws-amplify/core'; + import { createAWSCredentialsAndIdentityIdProvider } from '../../../../src/adapter-core'; jest.mock('@aws-amplify/auth/cognito'); diff --git a/packages/aws-amplify/__tests__/adapterCore/authProvidersFactories/cognito/createUserPoolsTokenProvider.test.ts b/packages/aws-amplify/__tests__/adapterCore/authProvidersFactories/cognito/createUserPoolsTokenProvider.test.ts index 2fec2bc5f06..0bea27d44fb 100644 --- a/packages/aws-amplify/__tests__/adapterCore/authProvidersFactories/cognito/createUserPoolsTokenProvider.test.ts +++ b/packages/aws-amplify/__tests__/adapterCore/authProvidersFactories/cognito/createUserPoolsTokenProvider.test.ts @@ -6,8 +6,8 @@ import { TokenOrchestrator, refreshAuthTokens, } from '@aws-amplify/auth/cognito'; - import { AuthConfig, KeyValueStorageInterface } from '@aws-amplify/core'; + import { createUserPoolsTokenProvider } from '../../../../src/adapter-core'; jest.mock('@aws-amplify/auth/cognito'); diff --git a/packages/aws-amplify/__tests__/adapterCore/runWithAmplifyServerContext.test.ts b/packages/aws-amplify/__tests__/adapterCore/runWithAmplifyServerContext.test.ts index 8299cce6224..9e2655bd2d5 100644 --- a/packages/aws-amplify/__tests__/adapterCore/runWithAmplifyServerContext.test.ts +++ b/packages/aws-amplify/__tests__/adapterCore/runWithAmplifyServerContext.test.ts @@ -1,12 +1,13 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { runWithAmplifyServerContext } from '../../src/adapter-core'; import { createAmplifyServerContext, destroyAmplifyServerContext, } from '@aws-amplify/core/internals/adapter-core'; +import { runWithAmplifyServerContext } from '../../src/adapter-core'; + // mock serverContext jest.mock('@aws-amplify/core/internals/adapter-core'); const mockCreateAmplifyServerContext = createAmplifyServerContext as jest.Mock; diff --git a/packages/aws-amplify/__tests__/initSingleton.test.ts b/packages/aws-amplify/__tests__/initSingleton.test.ts index dec181ca74a..2aa93209569 100644 --- a/packages/aws-amplify/__tests__/initSingleton.test.ts +++ b/packages/aws-amplify/__tests__/initSingleton.test.ts @@ -7,11 +7,11 @@ import { ResourcesConfig, defaultStorage, } from '@aws-amplify/core'; + import { - cognitoUserPoolsTokenProvider, cognitoCredentialsProvider, + cognitoUserPoolsTokenProvider, } from '../src/auth/cognito'; - import { Amplify } from '../src'; jest.mock('@aws-amplify/core'); diff --git a/packages/core/__tests__/BackgroundProcessManager.test.ts b/packages/core/__tests__/BackgroundProcessManager.test.ts index c06cccc6d46..b2e5f95ea08 100644 --- a/packages/core/__tests__/BackgroundProcessManager.test.ts +++ b/packages/core/__tests__/BackgroundProcessManager.test.ts @@ -1,4 +1,5 @@ import { Observable } from 'rxjs'; + import { BackgroundProcessManager } from '../src/BackgroundProcessManager'; import { BackgroundProcessManagerState } from '../src/BackgroundProcessManager/types'; @@ -95,9 +96,9 @@ describe('BackgroundProcessManager', () => { const manager = new BackgroundProcessManager(); const resultPromise = manager.add(async () => { - return new Promise((resolve, raise) => { + return new Promise((resolve, reject) => { setTimeout(() => { - raise(new Error('a fuss')); + reject(new Error('a fuss')); }, 50); }); }); @@ -145,7 +146,7 @@ describe('BackgroundProcessManager', () => { const manager = new BackgroundProcessManager(); // add a job that will not have completed by the time we open() again - manager.add(async () => new Promise(unsleep => setTimeout(unsleep, 10))); + manager.add(async () => new Promise(resolve => setTimeout(resolve, 10))); // close, but don't want, because we want to prove that open() will wait // internally for close to resolve before re-opening. @@ -175,8 +176,13 @@ describe('BackgroundProcessManager', () => { expect(manager.isClosing).toBe(false); expect(manager.isClosed).toBe(false); - let unblock; - manager.add(async () => new Promise(_unblock => (unblock = _unblock))); + let unblock: () => void = () => undefined; + manager.add( + async () => + new Promise(resolve => { + unblock = resolve; + }), + ); expect(manager.state).toEqual(BackgroundProcessManagerState.Open); expect(manager.isOpen).toBe(true); @@ -194,7 +200,7 @@ describe('BackgroundProcessManager', () => { // promise layers handling by awaiting another promise before the // manager can register completion. - unblock(); + unblock?.(); await new Promise(process.nextTick); expect(manager.state).toEqual(BackgroundProcessManagerState.Closed); @@ -251,8 +257,8 @@ describe('BackgroundProcessManager', () => { let completed = false; const manager = new BackgroundProcessManager(); - const resultPromise = manager.add(async onTerminate => { - return new Promise((resolve, reject) => { + const _ = manager.add(async (onTerminate: Promise) => { + return new Promise((resolve, _reject) => { const timer = setTimeout(() => { // this is the happy path that we plan not to reach in // this test. @@ -287,10 +293,11 @@ describe('BackgroundProcessManager', () => { test('can send termination signals to jobs that support termination, with reject', async () => { let completed = false; - let thrown = undefined; + let thrown; const manager = new BackgroundProcessManager(); + const expectedError = new Error('badness happened'); - const resultPromise = manager.add(async onTerminate => { + const resultPromise = manager.add(async (onTerminate: Promise) => { return new Promise((resolve, reject) => { const timer = setTimeout(() => { // this is the happy path that we plan not to reach in @@ -304,7 +311,7 @@ describe('BackgroundProcessManager', () => { // or reject. onTerminate.then(() => { clearTimeout(timer); - reject('badness happened'); + reject(expectedError); }); }); }); @@ -328,7 +335,7 @@ describe('BackgroundProcessManager', () => { // then making sure the job really really didn't fire. expect(completed).toBe(false); - expect(thrown).toEqual('badness happened'); + expect(thrown).toEqual(expectedError); }); test('attempts to terminate all, but patiently waits for persistent jobs', async () => { @@ -340,7 +347,7 @@ describe('BackgroundProcessManager', () => { for (let i = 0; i < 10; i++) { const _i = i; results.push(false); - manager.add(async onTerminate => { + manager.add(async (onTerminate: Promise) => { return new Promise((resolve, reject) => { const timer = setTimeout(() => { results[_i] = true; @@ -356,7 +363,7 @@ describe('BackgroundProcessManager', () => { // remember, if a job *does* terminate, it still // needs resolve/reject to unblock `close()`. - _i > 5 ? resolve() : reject(); + _i > 5 ? resolve() : reject(new Error()); } }); }); @@ -370,7 +377,9 @@ describe('BackgroundProcessManager', () => { expect(results.length).toEqual(10); // sanity check expect(results.filter(v => v === true).length).toBe(5); expect(terminationAttemptCount).toEqual(10); - expect(resolutions.filter(r => r.status === 'rejected').length).toEqual(3); + expect( + resolutions.filter((r: any) => r.status === 'rejected').length, + ).toEqual(3); }); test('can be used to terminate other types of bg jobs, like zen subscriptions', async () => { @@ -383,7 +392,9 @@ describe('BackgroundProcessManager', () => { // that the observable constructor can manage it like a hook. new Observable(observer => { const { resolve, onTerminate } = manager.add(); - const interval = setInterval(() => observer.next({}), 10); + const interval = setInterval(() => { + observer.next({}); + }, 10); const unsubscribe = () => { resolve(); // always remember to resolve/reject! @@ -392,6 +403,7 @@ describe('BackgroundProcessManager', () => { }; onTerminate.then(unsubscribe); + return unsubscribe; }).subscribe(() => count++); @@ -414,8 +426,10 @@ describe('BackgroundProcessManager', () => { const manager = new BackgroundProcessManager(); let count = 0; - const subscription = new Observable(observer => { - const interval = setInterval(() => observer.next({}), 10); + new Observable(observer => { + const interval = setInterval(() => { + observer.next({}); + }, 10); // LOOK: here's the magic. (tada!) return manager.addCleaner(async () => { @@ -443,7 +457,9 @@ describe('BackgroundProcessManager', () => { let count = 0; const subscription = new Observable(observer => { - const interval = setInterval(() => observer.next({}), 10); + const interval = setInterval(() => { + observer.next({}); + }, 10); // LOOK: here's the magic. (tada!) return manager.addCleaner(async () => { @@ -494,7 +510,7 @@ describe('BackgroundProcessManager', () => { // accumulate a bunch of close promises, only the first of which should // send the close signal, but all of which should await resolution. - const closes = [0, 1, 2, 3, 4, 5].map(i => manager.close()); + const closes = [0, 1, 2, 3, 4, 5].map(() => manager.close()); // ensure everything has settled const resolved = await Promise.allSettled(closes); @@ -556,7 +572,7 @@ describe('BackgroundProcessManager', () => { const manager = new BackgroundProcessManager(); manager.add( - async () => new Promise(unsleep => setTimeout(unsleep, 1)), + async () => new Promise(resolve => setTimeout(resolve, 1)), 'async function', ); @@ -571,9 +587,9 @@ describe('BackgroundProcessManager', () => { const manager = new BackgroundProcessManager(); manager.add( - async onTerminate => - new Promise(finishJob => { - onTerminate.then(finishJob); + async (onTerminate: Promise) => + new Promise(resolve => { + onTerminate.then(resolve); }), 'cancelable async function', ); @@ -613,9 +629,7 @@ describe('BackgroundProcessManager', () => { test('cleaners can be named', async () => { const manager = new BackgroundProcessManager(); - manager.addCleaner(async () => { - // no op - }, 'cleaner name'); + manager.addCleaner(() => Promise.resolve(), 'cleaner name'); expect(manager.pending.length).toBe(1); expect(manager.pending[0]).toEqual('cleaner name'); @@ -627,27 +641,27 @@ describe('BackgroundProcessManager', () => { const manager = new BackgroundProcessManager(); await manager.close(); - await expect(manager.add(async () => {}, 'some job')).rejects.toThrow( - 'some job', - ); + await expect( + manager.add(() => Promise.resolve(), 'some job'), + ).rejects.toThrow('some job'); }); test('manager closed error shows names of pending items in error', async () => { const manager = new BackgroundProcessManager(); - let unblock; + let unblock: any; manager.add( - () => new Promise(_unblock => (unblock = _unblock)), + () => new Promise(resolve => (unblock = resolve)), 'blocking job', ); const close = manager.close(); - await expect(manager.add(async () => {}, 'some job')).rejects.toThrow( - 'blocking job', - ); + await expect( + manager.add(() => Promise.resolve(), 'some job'), + ).rejects.toThrow('blocking job'); - unblock(); + unblock?.(); await close; }); }); diff --git a/packages/core/__tests__/Cache/StorageCache.test.ts b/packages/core/__tests__/Cache/StorageCache.test.ts index c30474f5374..f6615f7a38b 100644 --- a/packages/core/__tests__/Cache/StorageCache.test.ts +++ b/packages/core/__tests__/Cache/StorageCache.test.ts @@ -46,8 +46,8 @@ describe('StorageCache', () => { } } // create test helpers - const getStorageCache = (config?: CacheConfig) => - new StorageCacheTest(config); + const getStorageCache = (storageCacheConfig?: CacheConfig) => + new StorageCacheTest(storageCacheConfig); beforeAll(() => { mockGetCurrentSizeKey.mockReturnValue(currentSizeKey); diff --git a/packages/core/__tests__/Cache/StorageCacheCommon.test.ts b/packages/core/__tests__/Cache/StorageCacheCommon.test.ts index ea997dac41b..a1488ef71e9 100644 --- a/packages/core/__tests__/Cache/StorageCacheCommon.test.ts +++ b/packages/core/__tests__/Cache/StorageCacheCommon.test.ts @@ -26,7 +26,7 @@ describe('StorageCacheCommon', () => { warningThreshold: 0.8, }; // create spies - const loggerSpy = { + const loggerSpy: Record = { debug: jest.spyOn(ConsoleLogger.prototype, 'debug'), error: jest.spyOn(ConsoleLogger.prototype, 'error'), warn: jest.spyOn(ConsoleLogger.prototype, 'warn'), @@ -57,9 +57,9 @@ describe('StorageCacheCommon', () => { } } // create test helpers - const getStorageCache = (config?: CacheConfig) => + const getStorageCache = (storageCacheConfig?: CacheConfig) => new StorageCacheCommonTest({ - config, + config: storageCacheConfig, keyValueStorage: mockKeyValueStorage, }); @@ -464,8 +464,8 @@ describe('StorageCacheCommon', () => { describe('getItem()', () => { const value = 'value'; const cache = getStorageCache(config); - const key = 'key'; - const prefixedKey = `${keyPrefix}${key}`; + const testKey = 'key'; + const testPrefixedKey = `${keyPrefix}${testKey}`; beforeEach(() => { mockKeyValueStorageGetItem.mockReturnValue(null); @@ -476,11 +476,11 @@ describe('StorageCacheCommon', () => { JSON.stringify({ data: value }), ); - expect(await cache.getItem(key)).toBe(value); + expect(await cache.getItem(testKey)).toBe(value); expect(loggerSpy.debug).toHaveBeenCalledWith( - expect.stringContaining(`Get item: key is ${key}`), + expect.stringContaining(`Get item: key is ${testKey}`), ); - expect(mockKeyValueStorageGetItem).toHaveBeenCalledWith(prefixedKey); + expect(mockKeyValueStorageGetItem).toHaveBeenCalledWith(testPrefixedKey); }); it('aborts on empty key', async () => { @@ -508,22 +508,24 @@ describe('StorageCacheCommon', () => { }), ); - expect(await cache.getItem(key)).toBeNull(); - expect(mockKeyValueStorageRemoveItem).toHaveBeenCalledWith(prefixedKey); + expect(await cache.getItem(testKey)).toBeNull(); + expect(mockKeyValueStorageRemoveItem).toHaveBeenCalledWith( + testPrefixedKey, + ); }); it('returns null if not in cache', async () => { - expect(await cache.getItem(key)).toBeNull(); + expect(await cache.getItem(testKey)).toBeNull(); }); it('updates item visitedTime when fetched from cache', async () => { const item = { data: value }; mockKeyValueStorageGetItem.mockReturnValue(JSON.stringify(item)); - expect(await cache.getItem(key)).toBe(value); - expect(mockKeyValueStorageGetItem).toHaveBeenCalledWith(prefixedKey); + expect(await cache.getItem(testKey)).toBe(value); + expect(mockKeyValueStorageGetItem).toHaveBeenCalledWith(testPrefixedKey); expect(mockKeyValueStorageSetItem).toHaveBeenCalledWith( - prefixedKey, + testPrefixedKey, JSON.stringify({ ...item, visitedTime: currentTime }), ); }); @@ -531,7 +533,7 @@ describe('StorageCacheCommon', () => { it('execute a callback if specified when key not found in cache', async () => { mockGetByteLength.mockReturnValue(20); const callback = jest.fn(() => value); - expect(await cache.getItem(key, { callback })).toBe(value); + expect(await cache.getItem(testKey, { callback })).toBe(value); expect(callback).toHaveBeenCalled(); expect(mockKeyValueStorageSetItem).toHaveBeenCalled(); }); @@ -539,8 +541,8 @@ describe('StorageCacheCommon', () => { describe('removeItem()', () => { const cache = getStorageCache(config); - const key = 'key'; - const prefixedKey = `${keyPrefix}${key}`; + const testKey = 'key'; + const testPrefixedKey = `${keyPrefix}${testKey}`; beforeEach(() => { mockKeyValueStorageGetItem.mockReturnValue( @@ -549,11 +551,13 @@ describe('StorageCacheCommon', () => { }); it('removes an item', async () => { - await cache.removeItem(key); + await cache.removeItem(testKey); expect(loggerSpy.debug).toHaveBeenCalledWith( - expect.stringContaining(`Remove item: key is ${key}`), + expect.stringContaining(`Remove item: key is ${testKey}`), + ); + expect(mockKeyValueStorageRemoveItem).toHaveBeenCalledWith( + testPrefixedKey, ); - expect(mockKeyValueStorageRemoveItem).toHaveBeenCalledWith(prefixedKey); }); it('aborts on empty key', async () => { @@ -574,7 +578,7 @@ describe('StorageCacheCommon', () => { it('does nothing if item not found', async () => { mockKeyValueStorageGetItem.mockReturnValue(null); - await cache.removeItem(key); + await cache.removeItem(testKey); expect(mockKeyValueStorageRemoveItem).not.toHaveBeenCalled(); }); }); diff --git a/packages/core/__tests__/Cache/utils/CacheList.test.ts b/packages/core/__tests__/Cache/utils/CacheList.test.ts index a8016a56c09..4bbede75fab 100644 --- a/packages/core/__tests__/Cache/utils/CacheList.test.ts +++ b/packages/core/__tests__/Cache/utils/CacheList.test.ts @@ -125,8 +125,8 @@ describe('CacheList', () => { test('get all keys in the list', () => { const list: CacheList = new CacheList(); const keys: string[] = ['0', '1', '2']; - for (let i = 0; i < keys.length; i++) { - list.insertItem(keys[i]); + for (const key of keys) { + list.insertItem(key); } const listKeys: string[] = list.getKeys(); diff --git a/packages/core/__tests__/Cache/utils/cacheUtils.test.ts b/packages/core/__tests__/Cache/utils/cacheUtils.test.ts index 4c99645a352..ad46e477d52 100644 --- a/packages/core/__tests__/Cache/utils/cacheUtils.test.ts +++ b/packages/core/__tests__/Cache/utils/cacheUtils.test.ts @@ -3,7 +3,7 @@ import { getByteLength } from '../../../src/Cache/utils/cacheHelpers'; describe('cacheHelpers', () => { describe('getByteLength()', () => { test('happy case', () => { - const str: string = 'abc'; + const str = 'abc'; expect(getByteLength(str)).toBe(3); const str2: string = String.fromCharCode(0x80); diff --git a/packages/core/__tests__/ConsoleLogger.test.ts b/packages/core/__tests__/ConsoleLogger.test.ts index 069fed59ca1..02bf526b7e1 100644 --- a/packages/core/__tests__/ConsoleLogger.test.ts +++ b/packages/core/__tests__/ConsoleLogger.test.ts @@ -1,22 +1,22 @@ import { ConsoleLogger } from '../src'; -import { LoggingProvider, LogType } from '../src/Logger/types'; +import { LogType, LoggingProvider } from '../src/Logger/types'; type LogEvent = 'verbose' | 'debug' | 'info' | 'warn' | 'error'; describe('ConsoleLogger', () => { beforeAll(() => { - jest.spyOn(console, 'log').mockImplementation(() => {}); - jest.spyOn(console, 'error').mockImplementation(() => {}); - jest.spyOn(console, 'warn').mockImplementation(() => {}); - jest.spyOn(console, 'info').mockImplementation(() => {}); - jest.spyOn(console, 'debug').mockImplementation(() => {}); + jest.spyOn(console, 'log'); + jest.spyOn(console, 'error'); + jest.spyOn(console, 'warn'); + jest.spyOn(console, 'info'); + jest.spyOn(console, 'debug'); }); afterEach(() => { jest.clearAllMocks(); }); describe('pluggables', () => { - /*it('should store pluggables correctly when addPluggable is called', () => { + /* it('should store pluggables correctly when addPluggable is called', () => { const provider = new AWSCloudWatchProvider(); const logger = new Logger('name'); logger.addPluggable(provider); @@ -26,7 +26,7 @@ describe('ConsoleLogger', () => { expect(pluggables[0].getProviderName()).toEqual( AWS_CLOUDWATCH_PROVIDER_NAME ); - });*/ + }); */ it('should do nothing when no plugin is provided to addPluggable', () => { const logger = new ConsoleLogger('name'); @@ -38,16 +38,16 @@ describe('ConsoleLogger', () => { it('should do nothing when a non-logging category plugin is provided to addPluggable', () => { const provider = { - getCategoryName: function () { + getCategoryName: () => { return 'non-logging'; }, - getProviderName: function () { + getProviderName: () => { return 'lol'; }, - configure: function () { + configure: () => { return {}; }, - pushLogs: () => {}, + pushLogs: jest.fn(), } as LoggingProvider; const logger = new ConsoleLogger('name'); diff --git a/packages/core/__tests__/DateUtils.test.ts b/packages/core/__tests__/DateUtils.test.ts index d0ed93fd7fe..d42ee18f92a 100644 --- a/packages/core/__tests__/DateUtils.test.ts +++ b/packages/core/__tests__/DateUtils.test.ts @@ -1,16 +1,15 @@ import { DateUtils } from '../src/Signer/DateUtils'; -// Mock Date (https://github.com/facebook/jest/issues/2234#issuecomment-308121037) -const OriginalDate = Date; -// @ts-ignore Type 'typeof Date' is not assignable to type 'DateConstructor'. -Date = class extends Date { - // @ts-ignore Constructors for derived classes must contain a 'super' call.ts(2377) - constructor() { - return new OriginalDate('2020-01-01'); - } -}; - describe('DateUtils', () => { + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2020-01-01')); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + describe('getDateWithClockOffset()', () => { it('should return a new Date()', () => { expect(DateUtils.getDateWithClockOffset()).toEqual(new Date()); @@ -63,7 +62,7 @@ describe('DateUtils', () => { it('should be true when over 5 minutes', () => { const serverDate = new Date(); - serverDate.setMinutes(5); + serverDate.setMinutes(5, 0, 1000); expect(DateUtils.isClockSkewed(serverDate)).toBe(true); }); diff --git a/packages/core/__tests__/Hub.test.ts b/packages/core/__tests__/Hub.test.ts index 141ebbed16f..e7cd42a8f4f 100644 --- a/packages/core/__tests__/Hub.test.ts +++ b/packages/core/__tests__/Hub.test.ts @@ -1,4 +1,4 @@ -import { Hub, AMPLIFY_SYMBOL } from '../src/Hub'; +import { Hub } from '../src/Hub'; import { ConsoleLogger } from '../src'; describe('Hub', () => { @@ -9,7 +9,7 @@ describe('Hub', () => { }); test('happy case', () => { - const listener = jest.fn(() => {}); + const listener = jest.fn(); Hub.listen('auth', listener); @@ -28,7 +28,7 @@ describe('Hub', () => { }); test('Protected channel', () => { - const listener = jest.fn(() => {}); + const listener = jest.fn(); Hub.listen('auth', listener); @@ -50,7 +50,7 @@ describe('Hub', () => { }); test('Protected channel - ui', () => { - const listener = jest.fn(() => {}); + const listener = jest.fn(); Hub.listen('ui', listener); @@ -67,7 +67,7 @@ describe('Hub', () => { ); }); test('Remove listener', () => { - const listener = jest.fn(() => {}); + const listener = jest.fn(); const unsubscribe = Hub.listen('auth', listener); diff --git a/packages/core/__tests__/JS-browser-runtime.test.ts b/packages/core/__tests__/JS-browser-runtime.test.ts index 7050481bb5c..a82954d9733 100644 --- a/packages/core/__tests__/JS-browser-runtime.test.ts +++ b/packages/core/__tests__/JS-browser-runtime.test.ts @@ -9,12 +9,11 @@ describe('isBrowser build test', () => { // testing the Node.js process. const originalVersions = process.versions; beforeEach(() => { - //@ts-ignore + // @ts-expect-error test overrides delete global.process.versions; }); afterEach(() => { - //@ts-ignore global.process.versions = originalVersions; }); diff --git a/packages/core/__tests__/Mutex.test.ts b/packages/core/__tests__/Mutex.test.ts index e7335f292e5..b27615b4310 100644 --- a/packages/core/__tests__/Mutex.test.ts +++ b/packages/core/__tests__/Mutex.test.ts @@ -24,12 +24,12 @@ import { Mutex } from '../src/Mutex'; -describe('Mutex', function () { +describe('Mutex', () => { let mutex: Mutex; beforeEach(() => (mutex = new Mutex())); - test('ownership is exclusive', function () { + test('ownership is exclusive', () => { let flag = false; mutex.acquire().then(release => @@ -46,39 +46,51 @@ describe('Mutex', function () { }); }); - test('runExclusive passes result (immediate)', function () { + test('runExclusive passes result (immediate)', () => { return mutex .runExclusive(() => 10) - .then(value => expect(value).toBe(10)); + .then(value => { + expect(value).toBe(10); + }); }); - test('runExclusive passes result (promise)', function () { + test('runExclusive passes result (promise)', () => { return mutex .runExclusive(() => Promise.resolve(10)) - .then(value => expect(value).toBe(10)); + .then(value => { + expect(value).toBe(10); + }); }); - test('runExclusive passes rejection', function () { + test('runExclusive passes rejection', () => { + const expectedError = new Error('foo'); + return mutex - .runExclusive(() => Promise.reject('foo')) + .runExclusive(() => Promise.reject(expectedError)) .then( - () => Promise.reject('should have been rejected'), - value => expect(value).toBe('foo'), + () => Promise.reject(new Error('should have been rejected')), + value => { + expect(value).toBe(expectedError); + }, ); }); - test('runExclusive passes exception', function () { + test('runExclusive passes exception', () => { + const expectedError = new Error('foo'); + return mutex .runExclusive(() => { - throw 'foo'; + throw expectedError; }) .then( - () => Promise.reject('should have been rejected'), - value => expect(value).toBe('foo'), + () => Promise.reject(new Error('should have been rejected')), + value => { + expect(value).toBe(expectedError); + }, ); }); - test('runExclusive is exclusive', function () { + test('runExclusive is exclusive', () => { let flag = false; mutex.runExclusive( @@ -91,10 +103,12 @@ describe('Mutex', function () { ), ); - return mutex.runExclusive(() => expect(flag).toBe(true)); + return mutex.runExclusive(() => { + expect(flag).toBe(true); + }); }); - test('exceptions during runExclusive do not leave mutex locked', function () { + test('exceptions during runExclusive do not leave mutex locked', () => { let flag = false; mutex @@ -107,16 +121,18 @@ describe('Mutex', function () { () => undefined, ); - return mutex.runExclusive(() => expect(flag).toBe(true)); + return mutex.runExclusive(() => { + expect(flag).toBe(true); + }); }); - test('new mutex is unlocked', function () { + test('new mutex is unlocked', () => { expect(!mutex.isLocked()).toBe(true); }); - test('isLocked reflects the mutex state', async function () { - const lock1 = mutex.acquire(), - lock2 = mutex.acquire(); + test('isLocked reflects the mutex state', async () => { + const lock1 = mutex.acquire(); + const lock2 = mutex.acquire(); expect(mutex.isLocked()).toBe(true); diff --git a/packages/core/__tests__/Platform/customUserAgent.test.ts b/packages/core/__tests__/Platform/customUserAgent.test.ts index 6ce8a3a2684..dfe8e1f2098 100644 --- a/packages/core/__tests__/Platform/customUserAgent.test.ts +++ b/packages/core/__tests__/Platform/customUserAgent.test.ts @@ -1,8 +1,9 @@ import { - Category, + AdditionalDetails, AuthAction, - StorageAction, + Category, SetCustomUserAgentInput, + StorageAction, } from '../../src/Platform/types'; const MOCK_AUTH_UA_STATE: SetCustomUserAgentInput = { @@ -18,15 +19,18 @@ const MOCK_STORAGE_UA_STATE: SetCustomUserAgentInput = { }; describe('Custom user agent utilities', () => { - let getCustomUserAgent; - let setCustomUserAgent; + let getCustomUserAgent: ( + category: string, + api: string, + ) => AdditionalDetails | undefined; + let setCustomUserAgent: (input: SetCustomUserAgentInput) => () => void; beforeEach(() => { jest.resetModules(); - getCustomUserAgent = - require('../../src/Platform/customUserAgent').getCustomUserAgent; - setCustomUserAgent = - require('../../src/Platform/customUserAgent').setCustomUserAgent; + ({ + getCustomUserAgent, + setCustomUserAgent, + } = require('../../src/Platform/customUserAgent')); }); it('sets custom user agent state for multiple categories and APIs', () => { diff --git a/packages/core/__tests__/Platform/userAgent.test.ts b/packages/core/__tests__/Platform/userAgent.test.ts index 88174e3d75f..383b92486f0 100644 --- a/packages/core/__tests__/Platform/userAgent.test.ts +++ b/packages/core/__tests__/Platform/userAgent.test.ts @@ -1,18 +1,13 @@ import { - getAmplifyUserAgentObject, - getAmplifyUserAgent, Platform, + getAmplifyUserAgent, + getAmplifyUserAgentObject, } from '../../src/Platform'; import { version } from '../../src/Platform/version'; +import { AuthAction, Category, Framework } from '../../src/Platform/types'; import { - ApiAction, - AuthAction, - Category, - Framework, -} from '../../src/Platform/types'; -import { - detectFramework, clearCache, + detectFramework, } from '../../src/Platform/detectFramework'; import * as detection from '../../src/Platform/detection'; import { getCustomUserAgent } from '../../src/Platform/customUserAgent'; diff --git a/packages/core/__tests__/Retry.test.ts b/packages/core/__tests__/Retry.test.ts index a7389237985..59a5ceca741 100644 --- a/packages/core/__tests__/Retry.test.ts +++ b/packages/core/__tests__/Retry.test.ts @@ -1,7 +1,7 @@ import { - retry, - jitteredExponentialRetry, NonRetryableError, + jitteredExponentialRetry, + retry, } from '../src/utils/retry'; import { BackgroundProcessManager } from '../src/BackgroundProcessManager'; @@ -60,9 +60,10 @@ describe('retry', () => { } } - function delayFunction(attempt, args) { - receivedAttempt = attempt; + function delayFunction(attemptForDelayed: number, args: any[] | undefined) { + receivedAttempt = attemptForDelayed; receivedArgs = args; + return 1; } @@ -83,6 +84,7 @@ describe('retry', () => { return false; } count++; + return 1; } diff --git a/packages/core/__tests__/ServiceWorker.test.ts b/packages/core/__tests__/ServiceWorker.test.ts index f63e07aeed5..6f2c6232aeb 100644 --- a/packages/core/__tests__/ServiceWorker.test.ts +++ b/packages/core/__tests__/ServiceWorker.test.ts @@ -17,11 +17,11 @@ describe('ServiceWorker test', () => { serviceWorker.enablePush('publicKey'); }; - return expect(enablePush).toThrow(AmplifyError); + expect(enablePush).toThrow(AmplifyError); }); test('fails when registering', async () => { (global as any).navigator.serviceWorker = { - register: () => Promise.reject('an error'), + register: () => Promise.reject(new Error('an error')), }; const serviceWorker = new ServiceWorker(); @@ -38,7 +38,11 @@ describe('ServiceWorker test', () => { const statuses = ['installing', 'waiting', 'active']; statuses.forEach(status => { test(`can register (${status})`, () => { - const bla = { [status]: { addEventListener: () => {} } }; + const bla = { + [status]: { + addEventListener: jest.fn(), + }, + }; (global as any).navigator.serviceWorker = { register: () => Promise.resolve(bla), }; @@ -62,7 +66,7 @@ describe('ServiceWorker test', () => { const serviceWorker = new ServiceWorker(); await serviceWorker.register(); - return expect(bla[status].addEventListener).toHaveBeenCalledTimes(2); + expect(bla[status].addEventListener).toHaveBeenCalledTimes(2); }); }); }); @@ -80,7 +84,7 @@ describe('ServiceWorker test', () => { serviceWorker.send('A message'); - return expect(bla.installing.postMessage).toHaveBeenCalledTimes(0); + expect(bla.installing.postMessage).toHaveBeenCalledTimes(0); }); test('can send string message after registration', async () => { const bla = { @@ -96,9 +100,7 @@ describe('ServiceWorker test', () => { serviceWorker.send('A message'); - return expect(bla.installing.postMessage).toHaveBeenCalledWith( - 'A message', - ); + expect(bla.installing.postMessage).toHaveBeenCalledWith('A message'); }); test('can send object message after registration', async () => { const bla = { @@ -114,7 +116,7 @@ describe('ServiceWorker test', () => { serviceWorker.send({ property: 'value' }); - return expect(bla.installing.postMessage).toHaveBeenCalledWith( + expect(bla.installing.postMessage).toHaveBeenCalledWith( JSON.stringify({ property: 'value' }), ); }); diff --git a/packages/core/__tests__/Signer.test.ts b/packages/core/__tests__/Signer.test.ts index 4e15f008282..9625b972783 100644 --- a/packages/core/__tests__/Signer.test.ts +++ b/packages/core/__tests__/Signer.test.ts @@ -5,6 +5,7 @@ import { SignRequestOptions } from '../src/clients/middleware/signing/signer/sig import { Signer } from '../src/Signer'; import { DateUtils } from '../src/Signer/DateUtils'; import * as getSignatureModule from '../src/clients/middleware/signing/signer/signatureV4/utils/getSignature'; + import { credentials, credentialsWithToken, @@ -39,6 +40,7 @@ describe('Signer.sign', () => { ...signingOptions, ...options, }; + return [name, updatedRequest, updatedOptions, expectedAuthorization]; }, ), @@ -46,22 +48,26 @@ describe('Signer.sign', () => { 'signs request with %s', ( _, - { url, ...request }, - { credentials, signingRegion, signingService }, + { url: testUrl, ...request }, + { + credentials: testCredentials, + signingRegion: testSigningRegion, + signingService: testSigningService, + }, expected, ) => { - const { accessKeyId, secretAccessKey, sessionToken } = credentials; + const { accessKeyId, secretAccessKey, sessionToken } = testCredentials; const accessInfo = { access_key: accessKeyId, secret_key: secretAccessKey, session_token: sessionToken, }; const serviceInfo = { - region: signingRegion, - service: signingService, + region: testSigningRegion, + service: testSigningService, }; const signedRequest = Signer.sign( - { ...request, url: url.toString() }, + { ...request, url: testUrl.toString() }, accessInfo as any, serviceInfo as any, ); @@ -146,6 +152,7 @@ describe('Signer.signUrl', () => { ...signingOptions, ...options, }; + return [name, updatedRequest, updatedOptions, expectedUrl]; }, ), @@ -153,22 +160,26 @@ describe('Signer.signUrl', () => { 'signs url with %s', ( _, - { url, ...request }, - { credentials, signingRegion, signingService }, + { url: testUrl, ...request }, + { + credentials: testCredentials, + signingRegion: testSigningRegion, + signingService: testSigningService, + }, expected, ) => { - const { accessKeyId, secretAccessKey, sessionToken } = credentials; + const { accessKeyId, secretAccessKey, sessionToken } = testCredentials; const accessInfo = { access_key: accessKeyId, secret_key: secretAccessKey, session_token: sessionToken, }; const serviceInfo = { - region: signingRegion, - service: signingService, + region: testSigningRegion, + service: testSigningService, }; const signedUrl = Signer.signUrl( - { ...request, url: url.toString() }, + { ...request, url: testUrl.toString() }, accessInfo, serviceInfo as any, ); diff --git a/packages/core/__tests__/StringUtils.test.ts b/packages/core/__tests__/StringUtils.test.ts index 8fa74b7236d..e8bfaabff98 100644 --- a/packages/core/__tests__/StringUtils.test.ts +++ b/packages/core/__tests__/StringUtils.test.ts @@ -1,5 +1,7 @@ -import { urlSafeEncode, urlSafeDecode } from '../src/utils'; import { TextDecoder, TextEncoder } from 'util'; + +import { urlSafeDecode, urlSafeEncode } from '../src/utils'; + (global as any).TextEncoder = TextEncoder; (global as any).TextDecoder = TextDecoder; diff --git a/packages/core/__tests__/adapterCore/serverContext.test.ts b/packages/core/__tests__/adapterCore/serverContext.test.ts index ea6e2a55f1b..3f401e38aa2 100644 --- a/packages/core/__tests__/adapterCore/serverContext.test.ts +++ b/packages/core/__tests__/adapterCore/serverContext.test.ts @@ -3,8 +3,8 @@ import { createAmplifyServerContext, - getAmplifyServerContext, destroyAmplifyServerContext, + getAmplifyServerContext, } from '../../src/adapterCore'; const mockConfigure = jest.fn(); diff --git a/packages/core/__tests__/awsClients/cognitoIdentity/getCredentialsForIdentity.test.ts b/packages/core/__tests__/awsClients/cognitoIdentity/getCredentialsForIdentity.test.ts index 8125a212b7d..e7f820549e2 100644 --- a/packages/core/__tests__/awsClients/cognitoIdentity/getCredentialsForIdentity.test.ts +++ b/packages/core/__tests__/awsClients/cognitoIdentity/getCredentialsForIdentity.test.ts @@ -1,8 +1,8 @@ import { fetchTransferHandler } from '../../../src/clients/handlers/fetch'; import { - getCredentialsForIdentity, GetCredentialsForIdentityInput, GetCredentialsForIdentityOutput, + getCredentialsForIdentity, } from '../../../src/awsClients/cognitoIdentity'; import { cognitoIdentityHandlerOptions, diff --git a/packages/core/__tests__/awsClients/cognitoIdentity/getId.test.ts b/packages/core/__tests__/awsClients/cognitoIdentity/getId.test.ts index 4268cb73fe3..fceb0508ad7 100644 --- a/packages/core/__tests__/awsClients/cognitoIdentity/getId.test.ts +++ b/packages/core/__tests__/awsClients/cognitoIdentity/getId.test.ts @@ -1,8 +1,8 @@ import { fetchTransferHandler } from '../../../src/clients/handlers/fetch'; import { - getId, GetIdInput, GetIdOutput, + getId, } from '../../../src/awsClients/cognitoIdentity'; import { cognitoIdentityHandlerOptions, diff --git a/packages/core/__tests__/awsClients/pinpoint/getInAppMessages.test.ts b/packages/core/__tests__/awsClients/pinpoint/getInAppMessages.test.ts index 826254af149..9bebbf61c03 100644 --- a/packages/core/__tests__/awsClients/pinpoint/getInAppMessages.test.ts +++ b/packages/core/__tests__/awsClients/pinpoint/getInAppMessages.test.ts @@ -3,9 +3,9 @@ import { fetchTransferHandler } from '../../../src/clients/handlers/fetch'; import { - getInAppMessages, GetInAppMessagesInput, GetInAppMessagesOutput, + getInAppMessages, } from '../../../src/awsClients/pinpoint'; import { mockApplicationId, diff --git a/packages/core/__tests__/awsClients/pinpoint/putEvents.test.ts b/packages/core/__tests__/awsClients/pinpoint/putEvents.test.ts index f39335eaf60..192a1d6af1e 100644 --- a/packages/core/__tests__/awsClients/pinpoint/putEvents.test.ts +++ b/packages/core/__tests__/awsClients/pinpoint/putEvents.test.ts @@ -3,9 +3,9 @@ import { fetchTransferHandler } from '../../../src/clients/handlers/fetch'; import { - putEvents, PutEventsInput, PutEventsOutput, + putEvents, } from '../../../src/awsClients/pinpoint'; import { mockApplicationId, diff --git a/packages/core/__tests__/awsClients/pinpoint/updateEndpoint.test.ts b/packages/core/__tests__/awsClients/pinpoint/updateEndpoint.test.ts index 3927c202a27..99eaed5fdc8 100644 --- a/packages/core/__tests__/awsClients/pinpoint/updateEndpoint.test.ts +++ b/packages/core/__tests__/awsClients/pinpoint/updateEndpoint.test.ts @@ -3,9 +3,9 @@ import { fetchTransferHandler } from '../../../src/clients/handlers/fetch'; import { - updateEndpoint, UpdateEndpointInput, UpdateEndpointOutput, + updateEndpoint, } from '../../../src/awsClients/pinpoint'; import { mockApplicationId, diff --git a/packages/core/__tests__/awsClients/testUtils/data.ts b/packages/core/__tests__/awsClients/testUtils/data.ts index 9227c964aac..6d107c9e660 100644 --- a/packages/core/__tests__/awsClients/testUtils/data.ts +++ b/packages/core/__tests__/awsClients/testUtils/data.ts @@ -20,6 +20,7 @@ export const mockJsonResponse = ({ blob: async () => fail('blob() should not be called'), text: async () => fail('text() should not be called'), } as HttpResponse['body']; + return { statusCode: status, headers, diff --git a/packages/core/__tests__/clients/composeApiHandler.test.ts b/packages/core/__tests__/clients/composeApiHandler.test.ts index f9580604432..9a31abbdac5 100644 --- a/packages/core/__tests__/clients/composeApiHandler.test.ts +++ b/packages/core/__tests__/clients/composeApiHandler.test.ts @@ -20,19 +20,15 @@ describe(composeServiceApi.name, () => { test('should call transfer handler with resolved config including default config values', async () => { const mockTransferHandler = jest.fn().mockResolvedValue(defaultResponse); - const config = { - ...defaultConfig, - foo: 'bar', - }; const api = composeServiceApi( mockTransferHandler, - input => defaultRequest, - async output => ({ + () => defaultRequest, + async () => ({ Result: 'from API', }), defaultConfig, ); - const output = await api({ bar: 'baz', foo: 'foo' }, 'Input'); + await api({ bar: 'baz', foo: 'foo' }, 'Input'); expect(mockTransferHandler).toHaveBeenCalledTimes(1); expect(mockTransferHandler).toHaveBeenCalledWith( defaultRequest, @@ -51,8 +47,8 @@ describe(composeServiceApi.name, () => { }; const api = composeServiceApi( mockTransferHandler, - input => defaultRequest, - async output => ({ + () => defaultRequest, + async () => ({ Result: 'from API', }), defaultConfig, @@ -67,7 +63,7 @@ describe(composeServiceApi.name, () => { test('should call serializer and deserializer', async () => { const mockTransferHandler = jest.fn().mockResolvedValue(defaultResponse); - const defaultConfig = { + const defaultConfigWithEndpointResolver = { foo: 'bar', endpointResolver: jest.fn().mockReturnValue('https://a.b'), }; @@ -79,13 +75,13 @@ describe(composeServiceApi.name, () => { mockTransferHandler, mockSerializer, mockDeserializer, - defaultConfig, + defaultConfigWithEndpointResolver, ); - const output = await api({ bar: 'baz', foo: 'foo' }, 'Input'); + await api({ bar: 'baz', foo: 'foo' }, 'Input'); expect(mockSerializer).toHaveBeenCalledTimes(1); expect(mockSerializer).toHaveBeenCalledWith( 'Input', - defaultConfig.endpointResolver.mock.results[0].value, + defaultConfigWithEndpointResolver.endpointResolver.mock.results[0].value, ); expect(mockDeserializer).toHaveBeenCalledTimes(1); expect(mockDeserializer).toHaveBeenCalledWith(defaultResponse); diff --git a/packages/core/__tests__/clients/composeTransferHandler.test.ts b/packages/core/__tests__/clients/composeTransferHandler.test.ts index de7976c339d..4f4fea727f9 100644 --- a/packages/core/__tests__/clients/composeTransferHandler.test.ts +++ b/packages/core/__tests__/clients/composeTransferHandler.test.ts @@ -11,7 +11,9 @@ import { describe(composeTransferHandler.name, () => { test('should call core handler', async () => { - type HandlerOptions = { foo: string }; + interface HandlerOptions { + foo: string; + } const coreHandler: TransferHandler = jest .fn() .mockResolvedValue({ body: 'Response' } as Response); @@ -25,26 +27,32 @@ describe(composeTransferHandler.name, () => { }); test('should call execute middleware in order', async () => { - type OptionsType = { mockFnInOptions: (calledFrom: string) => void }; + interface OptionsType { + mockFnInOptions(calledFrom: string): void; + } const middlewareA: Middleware = - (options: OptionsType) => (next, context) => async request => { + (options: OptionsType) => next => async request => { request.body += 'A'; options.mockFnInOptions('A'); const resp = await next(request); resp.body += 'A'; + return resp; }; const middlewareB: Middleware = - (options: OptionsType) => (next, context) => async request => { + (options: OptionsType) => (next, _) => async request => { request.body += 'B'; options.mockFnInOptions('B'); const resp = await next(request); resp.body += 'B'; + return resp; }; - const coreHandler: TransferHandler = jest - .fn() - .mockResolvedValueOnce({ body: '' } as Response); + const coreHandler: TransferHandler< + Request, + Response, + Record + > = jest.fn().mockResolvedValueOnce({ body: '' } as Response); const handler = composeTransferHandler<[OptionsType, OptionsType]>( coreHandler, [middlewareA, middlewareB], diff --git a/packages/core/__tests__/clients/fetch.test.ts b/packages/core/__tests__/clients/fetch.test.ts index db72190c826..0c271aae1d3 100644 --- a/packages/core/__tests__/clients/fetch.test.ts +++ b/packages/core/__tests__/clients/fetch.test.ts @@ -23,7 +23,7 @@ describe(fetchTransferHandler.name, () => { const mockFetch = jest.fn(); beforeAll(() => { - global['fetch'] = mockFetch; + (global as any).fetch = mockFetch; }); beforeEach(() => { @@ -32,7 +32,7 @@ describe(fetchTransferHandler.name, () => { }); it('should support abort signal', async () => { - const signal = new AbortController().signal; + const { signal } = new AbortController(); await fetchTransferHandler(mockRequest, { abortSignal: signal }); expect(mockFetch).toHaveBeenCalledTimes(1); expect(mockFetch.mock.calls[0][1]).toEqual( @@ -68,8 +68,8 @@ describe(fetchTransferHandler.name, () => { }); it('should support headers', async () => { - mockFetchResponse.headers.forEach.mockImplementation((cb: any) => { - cb('foo', 'bar'); + mockFetchResponse.headers.forEach.mockImplementation((callback: any) => { + callback('foo', 'bar'); }); const { headers } = await fetchTransferHandler(mockRequest, {}); expect(headers).toEqual({ bar: 'foo' }); diff --git a/packages/core/__tests__/clients/middleware/retry/middleware.test.ts b/packages/core/__tests__/clients/middleware/retry/middleware.test.ts index f17c422d37b..1391f010d23 100644 --- a/packages/core/__tests__/clients/middleware/retry/middleware.test.ts +++ b/packages/core/__tests__/clients/middleware/retry/middleware.test.ts @@ -4,8 +4,8 @@ import { HttpResponse, MiddlewareHandler } from '../../../../src/clients/types'; import { composeTransferHandler } from '../../../../src/clients/internal/composeTransferHandler'; import { - retryMiddlewareFactory, RetryOptions, + retryMiddlewareFactory, } from '../../../../src/clients/middleware/retry'; jest.spyOn(global, 'setTimeout'); @@ -54,9 +54,9 @@ describe(`${retryMiddlewareFactory.name} middleware`, () => { }); const retryableHandler = getRetryableHandler(nextHandler); expect.assertions(2); - let resp; + try { - resp = await retryableHandler(defaultRequest, { + await retryableHandler(defaultRequest, { ...defaultRetryOptions, maxAttempts: 6, }); @@ -94,7 +94,7 @@ describe(`${retryMiddlewareFactory.name} middleware`, () => { (resp, error) => error.message !== 'UnretryableError', ); try { - const resp = await retryableHandler(defaultRequest, { + await retryableHandler(defaultRequest, { ...defaultRetryOptions, retryDecider, }); @@ -155,8 +155,11 @@ describe(`${retryMiddlewareFactory.name} middleware`, () => { const retryDecider = async () => true; const computeDelay = jest.fn().mockImplementation(attempt => { if (attempt === 1) { - setTimeout(() => controller.abort(), 100); + setTimeout(() => { + controller.abort(); + }, 100); } + return 200; }); try { @@ -182,15 +185,16 @@ describe(`${retryMiddlewareFactory.name} middleware`, () => { const betweenRetryFunction = jest .fn() .mockRejectedValueOnce(new Error('MiddlewareRetryableError')) - .mockResolvedValue(void 0); + .mockResolvedValue(undefined); const betweenRetryMiddleware = () => (next: any, context: any) => async (args: any) => { await betweenRetryFunction(args, context); + return next(args); }; const doubleRetryableHandler = composeTransferHandler< - [RetryOptions, {}, RetryOptions] + [RetryOptions, Record, RetryOptions] >(coreHandler, [ retryMiddlewareFactory, betweenRetryMiddleware, @@ -201,6 +205,7 @@ describe(`${retryMiddlewareFactory.name} middleware`, () => { .fn() .mockImplementation((response, error: Error) => { if (error && error.message.endsWith('RetryableError')) return true; + return false; }); const computeDelay = jest.fn().mockReturnValue(0); diff --git a/packages/core/__tests__/clients/middleware/signing/middleware.test.ts b/packages/core/__tests__/clients/middleware/signing/middleware.test.ts index 6c481ecc34c..a3183ebcdb5 100644 --- a/packages/core/__tests__/clients/middleware/signing/middleware.test.ts +++ b/packages/core/__tests__/clients/middleware/signing/middleware.test.ts @@ -3,8 +3,8 @@ import { composeTransferHandler } from '../../../../src/clients/internal/composeTransferHandler'; import { - signingMiddlewareFactory, SigningOptions, + signingMiddlewareFactory, } from '../../../../src/clients/middleware/signing'; import { getSkewCorrectedDate } from '../../../../src/clients/middleware/signing/utils/getSkewCorrectedDate'; import { getUpdatedSystemClockOffset } from '../../../../src/clients/middleware/signing/utils/getUpdatedSystemClockOffset'; @@ -13,6 +13,7 @@ import { HttpResponse, MiddlewareHandler, } from '../../../../src/clients/types'; + import { credentials, signingDate, diff --git a/packages/core/__tests__/clients/middleware/signing/signer/signatureV4/presignUrl.test.ts b/packages/core/__tests__/clients/middleware/signing/signer/signatureV4/presignUrl.test.ts index c27b1bdb6a8..279fc50beea 100644 --- a/packages/core/__tests__/clients/middleware/signing/signer/signatureV4/presignUrl.test.ts +++ b/packages/core/__tests__/clients/middleware/signing/signer/signatureV4/presignUrl.test.ts @@ -3,14 +3,15 @@ import { presignUrl } from '../../../../../../src/clients/middleware/signing/signer/signatureV4/presignUrl'; import { HttpRequest } from '../../../../../../src/clients/types'; +import { PresignUrlOptions } from '../../../../../../src/clients/middleware/signing/signer/signatureV4/types'; +import { getSignature } from '../../../../../../src/clients/middleware/signing/signer/signatureV4/utils/getSignature'; + import { signingTestTable } from './testUtils/signingTestTable'; import { formattedDates, getDefaultRequest, signingOptions, } from './testUtils/data'; -import { PresignUrlOptions } from '../../../../../../src/clients/middleware/signing/signer/signatureV4/types'; -import { getSignature } from '../../../../../../src/clients/middleware/signing/signer/signatureV4/utils/getSignature'; jest.mock( '../../../../../../src/clients/middleware/signing/signer/signatureV4/utils/getSignature', @@ -40,6 +41,7 @@ describe('presignUrl', () => { ...signingOptions, ...options, }; + return [name, updatedRequest, updatedOptions, expectedUrl]; }, ), diff --git a/packages/core/__tests__/clients/middleware/signing/signer/signatureV4/signRequest.test.ts b/packages/core/__tests__/clients/middleware/signing/signer/signatureV4/signRequest.test.ts index 857f56a310b..ef8089b6aea 100644 --- a/packages/core/__tests__/clients/middleware/signing/signer/signatureV4/signRequest.test.ts +++ b/packages/core/__tests__/clients/middleware/signing/signer/signatureV4/signRequest.test.ts @@ -3,14 +3,15 @@ import { signRequest } from '../../../../../../src/clients/middleware/signing/signer/signatureV4/signRequest'; import { HttpRequest } from '../../../../../../src/clients/types'; +import { SignRequestOptions } from '../../../../../../src/clients/middleware/signing/signer/signatureV4/types'; +import { getSignature } from '../../../../../../src/clients/middleware/signing/signer/signatureV4/utils/getSignature'; + import { signingTestTable } from './testUtils/signingTestTable'; import { formattedDates, getDefaultRequest, signingOptions, } from './testUtils/data'; -import { SignRequestOptions } from '../../../../../../src/clients/middleware/signing/signer/signatureV4/types'; -import { getSignature } from '../../../../../../src/clients/middleware/signing/signer/signatureV4/utils/getSignature'; jest.mock( '../../../../../../src/clients/middleware/signing/signer/signatureV4/utils/getSignature', @@ -40,6 +41,7 @@ describe('signRequest', () => { ...signingOptions, ...options, }; + return [name, updatedRequest, updatedOptions, expectedAuthorization]; }, ), diff --git a/packages/core/__tests__/clients/middleware/signing/signer/signatureV4/testUtils/signingTestTable.ts b/packages/core/__tests__/clients/middleware/signing/signer/signatureV4/testUtils/signingTestTable.ts index 575ce159b21..37ff662abd2 100644 --- a/packages/core/__tests__/clients/middleware/signing/signer/signatureV4/testUtils/signingTestTable.ts +++ b/packages/core/__tests__/clients/middleware/signing/signer/signatureV4/testUtils/signingTestTable.ts @@ -3,6 +3,7 @@ import { SignRequestOptions } from '../../../../../../../src/clients/middleware/signing/signer/signatureV4/types'; import { HttpRequest } from '../../../../../../../src/clients/types'; + import { credentialsWithToken, signingOptions, url } from './data'; interface TestCase { diff --git a/packages/core/__tests__/clients/middleware/signing/signer/signatureV4/utils/getSignature.test.ts b/packages/core/__tests__/clients/middleware/signing/signer/signatureV4/utils/getSignature.test.ts index 1a796e7bd2e..8ccc5e5283e 100644 --- a/packages/core/__tests__/clients/middleware/signing/signer/signatureV4/utils/getSignature.test.ts +++ b/packages/core/__tests__/clients/middleware/signing/signer/signatureV4/utils/getSignature.test.ts @@ -2,8 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import { - credentials, credentialScope, + credentials, formattedDates, getDefaultRequest, signingRegion, diff --git a/packages/core/__tests__/parseAWSExports.test.ts b/packages/core/__tests__/parseAWSExports.test.ts index a194ef25528..4e26fc34b2d 100644 --- a/packages/core/__tests__/parseAWSExports.test.ts +++ b/packages/core/__tests__/parseAWSExports.test.ts @@ -243,7 +243,7 @@ describe('parseAWSExports', () => { signUpVerificationMethod: undefined, userAttributes: {}, userPoolClientId: undefined, - userPoolId: userPoolId, + userPoolId, }, }, }); diff --git a/packages/core/__tests__/providers/pinpoint/apis/flushEvents.test.ts b/packages/core/__tests__/providers/pinpoint/apis/flushEvents.test.ts index 367773b2be0..242389e0090 100644 --- a/packages/core/__tests__/providers/pinpoint/apis/flushEvents.test.ts +++ b/packages/core/__tests__/providers/pinpoint/apis/flushEvents.test.ts @@ -3,7 +3,7 @@ import { getEventBuffer } from '../../../../src/providers/pinpoint/utils/getEventBuffer'; import { flushEvents } from '../../../../src/providers/pinpoint'; -import { appId, region, credentials, identityId } from '../testUtils/data'; +import { appId, credentials, identityId, region } from '../testUtils/data'; import { BUFFER_SIZE, FLUSH_INTERVAL, diff --git a/packages/core/__tests__/providers/pinpoint/apis/record.test.ts b/packages/core/__tests__/providers/pinpoint/apis/record.test.ts index fc17952a8c8..0767effeee1 100644 --- a/packages/core/__tests__/providers/pinpoint/apis/record.test.ts +++ b/packages/core/__tests__/providers/pinpoint/apis/record.test.ts @@ -1,4 +1,5 @@ import { v4 } from 'uuid'; + import { putEvents as clientPutEvents } from '../../../../src/awsClients/pinpoint'; import { record } from '../../../../src/providers/pinpoint/apis'; import { updateEndpoint } from '../../../../src/providers/pinpoint/apis/updateEndpoint'; @@ -8,9 +9,9 @@ import { category, credentials, endpointId, - region, - identityId, event, + identityId, + region, uuid, } from '../testUtils/data'; import { getEventBuffer } from '../../../../src/providers/pinpoint/utils/getEventBuffer'; diff --git a/packages/core/__tests__/providers/pinpoint/apis/testUtils/getExpectedPutEventsInput.ts b/packages/core/__tests__/providers/pinpoint/apis/testUtils/getExpectedPutEventsInput.ts index 0101631ce78..ee7ce356a60 100644 --- a/packages/core/__tests__/providers/pinpoint/apis/testUtils/getExpectedPutEventsInput.ts +++ b/packages/core/__tests__/providers/pinpoint/apis/testUtils/getExpectedPutEventsInput.ts @@ -4,8 +4,8 @@ import { appId, endpointId as defaultEndpointId, - uuid, event as defaultEvent, + uuid, } from '../../testUtils/data'; export const getExpectedPutEventsInput = ({ diff --git a/packages/core/__tests__/providers/pinpoint/apis/updateEndpoint.test.ts b/packages/core/__tests__/providers/pinpoint/apis/updateEndpoint.test.ts index 5fa35dd912b..6ad4e718955 100644 --- a/packages/core/__tests__/providers/pinpoint/apis/updateEndpoint.test.ts +++ b/packages/core/__tests__/providers/pinpoint/apis/updateEndpoint.test.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { v4 } from 'uuid'; + import { getClientInfo } from '../../../../src/utils/getClientInfo'; import { updateEndpoint as clientUpdateEndpoint } from '../../../../src/awsClients/pinpoint'; import { cacheEndpointId } from '../../../../src/providers/pinpoint/utils/cacheEndpointId'; @@ -18,6 +19,7 @@ import { userProfile, uuid, } from '../testUtils/data'; + import { getExpectedInput } from './testUtils/getExpectedInput'; jest.mock('uuid'); diff --git a/packages/core/__tests__/singleton/Singleton.test.ts b/packages/core/__tests__/singleton/Singleton.test.ts index 0b12a8e9bac..03c0185359d 100644 --- a/packages/core/__tests__/singleton/Singleton.test.ts +++ b/packages/core/__tests__/singleton/Singleton.test.ts @@ -1,10 +1,12 @@ +import { TextDecoder, TextEncoder } from 'util'; + import { Amplify } from '../../src/singleton'; -import { Hub, AMPLIFY_SYMBOL } from '../../src/Hub'; +import { AMPLIFY_SYMBOL, Hub } from '../../src/Hub'; import { AuthClass as Auth } from '../../src/singleton/Auth'; import { decodeJWT } from '../../src/singleton/Auth/utils'; import { CredentialsAndIdentityId } from '../../src/singleton/Auth/types'; -import { TextEncoder, TextDecoder } from 'util'; -import { fetchAuthSession, ResourcesConfig } from '../../src'; +import { ResourcesConfig, fetchAuthSession } from '../../src'; + Object.assign(global, { TextDecoder, TextEncoder }); jest.mock('../../src/Hub', () => ({ @@ -16,7 +18,9 @@ jest.mock('../../src/Hub', () => ({ const mockHubDispatch = Hub.dispatch as jest.Mock; -type ArgumentTypes = F extends (...args: infer A) => any +type ArgumentTypes any> = F extends ( + ...args: infer A +) => any ? A : never; @@ -281,7 +285,7 @@ describe('Session tests', () => { test('fetchAuthSession with credentials provider only', async () => { const mockCredentials = { accessKeyId: 'accessKeyValue', - secretAccessKey: 'secreatAccessKeyValue', + secretAccessKey: 'secretAccessKeyValue', }; Amplify.configure( {}, @@ -293,7 +297,7 @@ describe('Session tests', () => { credentials: mockCredentials, }; }, - clearCredentialsAndIdentityId: () => {}, + clearCredentialsAndIdentityId: jest.fn(), }, }, }, @@ -359,11 +363,7 @@ describe('Session tests', () => { }; const credentialsSpy = jest.fn( - async ({ - tokens, - authConfig, - identityId, - }): Promise => { + async (): Promise => { return { credentials: { accessKeyId: 'accessKeyIdValue', @@ -389,7 +389,7 @@ describe('Session tests', () => { Auth: { credentialsProvider: { getCredentialsAndIdentityId: credentialsSpy, - clearCredentialsAndIdentityId: () => {}, + clearCredentialsAndIdentityId: jest.fn(), }, tokenProvider: { getTokens: spyTokenProvider, @@ -454,11 +454,7 @@ describe('Session tests', () => { }; const credentialsSpy = jest.fn( - async ({ - tokens, - authConfig, - identityId, - }): Promise => { + async (_): Promise => { return { credentials: { accessKeyId: 'accessKeyIdValue', @@ -479,7 +475,7 @@ describe('Session tests', () => { Auth: { credentialsProvider: { getCredentialsAndIdentityId: credentialsSpy, - clearCredentialsAndIdentityId: () => {}, + clearCredentialsAndIdentityId: jest.fn(), }, tokenProvider: { getTokens: spyTokenProvider, @@ -521,6 +517,7 @@ describe('Session tests', () => { const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MTAyOTMxMzB9.YzDpgJsrB3z-ZU1XxMcXSQsMbgCzwH_e-_76rnfehh0'; const mockToken = decodeJWT(token); + return { accessToken: mockToken, }; @@ -569,8 +566,7 @@ describe('Session tests', () => { }, ); - const action = async () => - await auth.fetchAuthSession({ forceRefresh: true }); + const action = async () => auth.fetchAuthSession({ forceRefresh: true }); await expect(action()).rejects.toThrow('no no no'); diff --git a/packages/core/__tests__/storage/CookieStorage.test.ts b/packages/core/__tests__/storage/CookieStorage.test.ts index 75ef343691c..c16e57920bf 100644 --- a/packages/core/__tests__/storage/CookieStorage.test.ts +++ b/packages/core/__tests__/storage/CookieStorage.test.ts @@ -3,7 +3,7 @@ import { CookieStorage } from '../../src/storage/CookieStorage'; const cookieStorageDomain = 'https://testdomain.com'; describe('CookieStorage', () => { - //defining a DOM to attach a cookie to + // defining a DOM to attach a cookie to Object.defineProperty(document, 'cookie', { writable: true }); describe('Constructor methods', () => { @@ -17,16 +17,16 @@ describe('CookieStorage', () => { const expectedError = 'The sameSite value of cookieStorage must be "lax", "strict" or "none"'; expect(() => { - new CookieStorage({ sameSite: undefined }); + const _ = new CookieStorage({ sameSite: undefined }); }).toThrow(expectedError); expect(() => { - new CookieStorage({ sameSite: 'foo' as any }); + const _ = new CookieStorage({ sameSite: 'foo' as any }); }).toThrow(expectedError); }); it('SameSite value is "none" while secure is false', () => { expect(() => { - new CookieStorage({ + const _ = new CookieStorage({ domain: cookieStorageDomain, secure: false, sameSite: 'none', @@ -63,11 +63,11 @@ describe('CookieStorage', () => { }); it('Clearing cookies should remove all items within the storage', async () => { - const cookieStore = new CookieStorage(cookieStoreData); - await cookieStore.setItem('testKey2', 'testValue'); - const tempReference = await cookieStore.getItem('testKey2'); - await cookieStore.clear(); - expect(await cookieStore.getItem('testKey2')).not.toEqual( + const testCookieStore = new CookieStorage(cookieStoreData); + await testCookieStore.setItem('testKey2', 'testValue'); + const tempReference = await testCookieStore.getItem('testKey2'); + await testCookieStore.clear(); + expect(await testCookieStore.getItem('testKey2')).not.toEqual( tempReference, ); }); diff --git a/packages/core/__tests__/storage/DefaultStorage.test.ts b/packages/core/__tests__/storage/DefaultStorage.test.ts index ca8964f1a5c..47214d899d7 100644 --- a/packages/core/__tests__/storage/DefaultStorage.test.ts +++ b/packages/core/__tests__/storage/DefaultStorage.test.ts @@ -25,15 +25,14 @@ describe('DefaultStorage', () => { expect(await defaultStorage.getItem(key)).toEqual(secondValue); }); - it('should not throw if trying to delete a non existing key', async () => { + it('should not throw if trying to delete a non existing key', () => { const badKey = 'nonExistingKey'; - await expect(() => { - defaultStorage.removeItem(badKey); - }).not.toThrow(); + + expect(defaultStorage.removeItem(badKey)).resolves.toBeUndefined(); }); it('should clear out storage', async () => { await defaultStorage.clear(); - expect(await defaultStorage.getItem(key)).toBeNull(); + expect(defaultStorage.getItem(key)).resolves.toBeNull(); }); }); diff --git a/packages/core/__tests__/storage/SessionStorage.test.ts b/packages/core/__tests__/storage/SessionStorage.test.ts index 6f35ec0c6ff..df6116d823e 100644 --- a/packages/core/__tests__/storage/SessionStorage.test.ts +++ b/packages/core/__tests__/storage/SessionStorage.test.ts @@ -27,6 +27,7 @@ describe('sessionStorage', () => { it('should not throw if trying to delete a non existing key', async () => { const badKey = 'nonExistingKey'; + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression await expect(() => { sessionStorage.removeItem(badKey); }).not.toThrow(); diff --git a/packages/core/__tests__/utils.test.ts b/packages/core/__tests__/utils.test.ts index 09be22a39da..71444c4f8cf 100644 --- a/packages/core/__tests__/utils.test.ts +++ b/packages/core/__tests__/utils.test.ts @@ -1,8 +1,8 @@ 'use strict'; import { - jitteredExponentialRetry, NonRetryableError, + jitteredExponentialRetry, urlSafeDecode, urlSafeEncode, } from '../src/utils'; @@ -14,8 +14,6 @@ import { DateUtils } from '../src/Signer/DateUtils'; ConsoleLogger.LOG_LEVEL = 'DEBUG'; describe('Util', () => { - beforeEach(() => {}); - describe('DateUtils', () => { test('isClockSkewError', () => { expect( diff --git a/packages/core/__tests__/utils/generateRandomString.test.ts b/packages/core/__tests__/utils/generateRandomString.test.ts index 2717e3a3743..d17cb37c9d7 100644 --- a/packages/core/__tests__/utils/generateRandomString.test.ts +++ b/packages/core/__tests__/utils/generateRandomString.test.ts @@ -17,6 +17,7 @@ describe('generateRandomString()', () => { mathRandomSpy.mockImplementation(() => { const returnValue = counter; counter += 5; + return parseFloat(`0.${returnValue}`); }); diff --git a/packages/core/__tests__/utils/globalHelpers/globalHelpers.native.test.ts b/packages/core/__tests__/utils/globalHelpers/globalHelpers.native.test.ts index 131199c3e84..4ca167a5649 100644 --- a/packages/core/__tests__/utils/globalHelpers/globalHelpers.native.test.ts +++ b/packages/core/__tests__/utils/globalHelpers/globalHelpers.native.test.ts @@ -1,9 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -const mockCrypto = { getRandomValues: jest.fn() }; +import { loadGetRandomValues } from '@aws-amplify/react-native'; -import { loadGetRandomValues, loadBase64 } from '@aws-amplify/react-native'; import { getAtob, getBtoa, @@ -14,7 +13,7 @@ jest.mock('react-native'); jest.mock('@aws-amplify/react-native', () => ({ loadGetRandomValues: jest.fn(() => { Object.defineProperty(global, 'crypto', { - value: mockCrypto, + value: { getRandomValues: jest.fn(() => 'mocked') }, writable: true, }); }), @@ -25,7 +24,6 @@ jest.mock('@aws-amplify/react-native', () => ({ })); const mockLoadGetRandomValues = loadGetRandomValues as jest.Mock; -const mockLoadBase64 = loadBase64 as jest.Mock; describe('getGlobal (native)', () => { beforeAll(() => { @@ -35,7 +33,7 @@ describe('getGlobal (native)', () => { describe('getCrypto()', () => { it('returns the polyfill crypto from react-native-get-random-values', () => { - expect(getCrypto()).toEqual(mockCrypto); + expect(getCrypto().getRandomValues(null)).toEqual('mocked'); }); }); diff --git a/packages/core/__tests__/utils/queuedStorage/queuedStorage.native.test.ts b/packages/core/__tests__/utils/queuedStorage/queuedStorage.native.test.ts index a3eef512e09..80395206eb0 100644 --- a/packages/core/__tests__/utils/queuedStorage/queuedStorage.native.test.ts +++ b/packages/core/__tests__/utils/queuedStorage/queuedStorage.native.test.ts @@ -2,11 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 import { loadAsyncStorage } from '@aws-amplify/react-native'; + import { createQueuedStorage, keyPrefix, } from '../../../src/utils/queuedStorage/createQueuedStorage.native'; -import { ItemToAdd, QueuedItem } from '../../../src/utils/queuedStorage/types'; +import { + ItemToAdd, + QueuedItem, + QueuedStorage, +} from '../../../src/utils/queuedStorage/types'; import { getAddItemBytesSize } from '../../../src/utils/queuedStorage/getAddItemBytesSize'; jest.mock('@aws-amplify/react-native', () => ({ @@ -30,7 +35,7 @@ describe('createQueuedStorage', () => { const mockTimestamp = new Date('2024-01-02').toUTCString(); describe('initialization', () => { - let queuedStorage; + let queuedStorage: QueuedStorage; const testBytesSize = 1; const mockKeys = [`${keyPrefix}_key1`, `${keyPrefix}_key2`]; const mockQueuedItems = [ @@ -115,30 +120,30 @@ describe('createQueuedStorage', () => { ['peekAll', undefined], ['delete', [{}]], ['clear', undefined], - ])('when invokes %s it throws', async (method, args) => { - const storage = createQueuedStorage(); - await expect(storage[method](args)).rejects.toThrow(expectedError); - }); + ] as unknown as [keyof QueuedStorage, any])( + 'when invokes %s it throws', + async (method: keyof QueuedStorage, args: any) => { + const storage = createQueuedStorage(); + await expect(storage[method](args)).rejects.toThrow(expectedError); + }, + ); }); describe('method add()', () => { - let queuedStorage; - let originalDate; + let queuedStorage: QueuedStorage; + let dateNowSpy: jest.SpyInstance; const testInput: ItemToAdd = { content: 'some log content', timestamp: mockTimestamp, }; beforeAll(() => { - originalDate = Date; - Date = { - now: jest.fn(() => 123), - } as any; + dateNowSpy = jest.spyOn(Date, 'now').mockReturnValue(123); queuedStorage = createQueuedStorage(); }); afterAll(() => { - Date = originalDate; + dateNowSpy.mockRestore(); }); afterEach(() => { @@ -194,7 +199,7 @@ describe('createQueuedStorage', () => { }); describe('method peek() and peekAll()', () => { - let queuedStorage; + let queuedStorage: QueuedStorage; const mockQueuedItems = [ { @@ -282,7 +287,7 @@ describe('createQueuedStorage', () => { }, ]; - let queuedStorage; + let queuedStorage: QueuedStorage; beforeAll(() => { queuedStorage = createQueuedStorage(); @@ -304,7 +309,7 @@ describe('createQueuedStorage', () => { }); describe('method clear()', () => { - let queuedStorage; + let queuedStorage: QueuedStorage; const testAllKeys = [ `${keyPrefix}_key1`, `${keyPrefix}_key2`, diff --git a/packages/core/__tests__/utils/queuedStorage/queuedStorage.test.ts b/packages/core/__tests__/utils/queuedStorage/queuedStorage.test.ts index e8314d82cff..9d3dc822d3c 100644 --- a/packages/core/__tests__/utils/queuedStorage/queuedStorage.test.ts +++ b/packages/core/__tests__/utils/queuedStorage/queuedStorage.test.ts @@ -14,34 +14,64 @@ import { } from '../../../src/utils/queuedStorage/types'; describe('createQueuedStorage', () => { - let originalIndexedDB; - let originalIDBKeyRange; + let originalIndexedDB: IDBFactory; + let originalIDBKeyRange: typeof IDBKeyRange; const mockTimestamp = new Date('2024-01-02').toUTCString(); const mockAdd = jest.fn(() => ({ set onsuccess(handler) { handler(); }, + get onsuccess() { + return () => { + // no op + }; + }, set onerror(handler) { handler(); }, + get onerror() { + return () => { + // no op + }; + }, })); const mockClear = jest.fn(); const mockGetAll = jest.fn(() => ({ set onsuccess(handler) { handler(); }, + get onsuccess() { + return () => { + // no op + }; + }, set onerror(handler) { handler(); }, + get onerror() { + return () => { + // no op + }; + }, result: [] as QueuedItem[], })); const mockDelete = jest.fn(() => ({ set onsuccess(handler) { handler(); }, + get onsuccess() { + return () => { + // no op + }; + }, set onerror(handler) { handler(); }, + get onerror() { + return () => { + // no op + }; + }, })); const mockObjectStore = jest.fn(() => ({ add: mockAdd, @@ -63,10 +93,27 @@ describe('createQueuedStorage', () => { set onupgradeneeded(handler) { handler(); }, + get onupgradeneeded() { + return () => { + // no op + }; + }, set onsuccess(handler) { handler(); }, - set onerror(_) {}, + get onsuccess() { + return () => { + // no op + }; + }, + set onerror(_) { + // no op + }, + get onerror() { + return () => { + // no op + }; + }, result: mockDB, }; const mockIndexedDBOpen = jest.fn(() => mockIndexedDBOpenRequest); @@ -119,7 +166,19 @@ describe('createQueuedStorage', () => { set onsuccess(handler) { handler(); }, - set onerror(_) {}, + get onsuccess() { + return () => { + // no op + }; + }, + set onerror(_) { + // no-op + }, + get onerror() { + return () => { + // no op + }; + }, result: mockQueuedItems, }); queuedStorage = createQueuedStorage(); @@ -172,10 +231,27 @@ describe('createQueuedStorage', () => { set onupgradeneeded(handler) { handler(); }, - set onsuccess(_) {}, + get onupgradeneeded() { + return () => { + // no op + }; + }, + set onsuccess(_) { + // no-op + }, + get onsuccess() { + return () => { + // no op + }; + }, set onerror(handler) { handler(); }, + get onerror() { + return () => { + // no op + }; + }, error: expectedError, result: mockDB, } as any); @@ -187,10 +263,13 @@ describe('createQueuedStorage', () => { ['peekAll', undefined], ['delete', [{}]], ['clear', undefined], - ])('when invokes %s it throws', async (method, args) => { - const storage = createQueuedStorage(); - await expect(storage[method](args)).rejects.toThrow(expectedError); - }); + ] as unknown as [keyof QueuedStorage, any])( + 'when invokes %s it throws', + async (method: keyof QueuedStorage, args: any) => { + const storage = createQueuedStorage(); + await expect(storage[method](args)).rejects.toThrow(expectedError); + }, + ); }); describe('method add()', () => { @@ -231,7 +310,19 @@ describe('createQueuedStorage', () => { set onsuccess(handler) { handler(); }, - set onerror(_) {}, + get onsuccess() { + return () => { + // no op + }; + }, + set onerror(_) { + // no op + }, + get onerror() { + return () => { + // no op + }; + }, result: mockQueuedItems, }); @@ -273,7 +364,19 @@ describe('createQueuedStorage', () => { set onsuccess(handler) { handler(); }, - set onerror(_) {}, + get onsuccess() { + return () => { + // no op + }; + }, + set onerror(_) { + // no-op + }, + get onerror() { + return () => { + // no op + }; + }, result: [mockQueuedItems[0]], }); const result = await queuedStorage.peek(1); @@ -288,7 +391,19 @@ describe('createQueuedStorage', () => { set onsuccess(handler) { handler(); }, - set onerror(_) {}, + get onsuccess() { + return () => { + // no op + }; + }, + set onerror(_) { + // no op + }, + get onerror() { + return () => { + // no op + }; + }, result: mockQueuedItems, }); From 873a6f47b24b6f8c97bf3af001db70f204f67daa Mon Sep 17 00:00:00 2001 From: Ivan Artemiev <29709626+iartemiev@users.noreply.github.com> Date: Thu, 28 Mar 2024 15:37:20 -0400 Subject: [PATCH 5/7] fix(api-graphql): default selection set regression (#13185) --- .../fixtures/modeled/amplifyconfiguration.ts | 64 ++++++++++++++++++- .../__tests__/fixtures/modeled/schema.ts | 5 ++ .../internals/generateClient.test.ts | 1 + .../__tests__/resolveOwnerFields.test.ts | 5 +- .../src/utils/resolveOwnerFields.ts | 5 +- 5 files changed, 77 insertions(+), 3 deletions(-) diff --git a/packages/api-graphql/__tests__/fixtures/modeled/amplifyconfiguration.ts b/packages/api-graphql/__tests__/fixtures/modeled/amplifyconfiguration.ts index a45cb28da94..77c21ad8757 100644 --- a/packages/api-graphql/__tests__/fixtures/modeled/amplifyconfiguration.ts +++ b/packages/api-graphql/__tests__/fixtures/modeled/amplifyconfiguration.ts @@ -1,5 +1,5 @@ /** - * Generated from `./schema.ts` by samsara. + * Generated from `./schema.ts` by amplify-backend generate config. * * Cognito fields etc. omitted. */ @@ -1656,6 +1656,68 @@ const amplifyConfig = { sortKeyFieldNames: [], }, }, + ModelStaticGroup: { + name: 'ModelStaticGroup', + fields: { + id: { + name: 'id', + isArray: false, + type: 'ID', + isRequired: true, + attributes: [], + }, + description: { + name: 'description', + isArray: false, + type: 'String', + isRequired: false, + attributes: [], + }, + createdAt: { + name: 'createdAt', + isArray: false, + type: 'AWSDateTime', + isRequired: false, + attributes: [], + isReadOnly: true, + }, + updatedAt: { + name: 'updatedAt', + isArray: false, + type: 'AWSDateTime', + isRequired: false, + attributes: [], + isReadOnly: true, + }, + }, + syncable: true, + pluralName: 'ModelStaticGroups', + attributes: [ + { + type: 'model', + properties: {}, + }, + { + type: 'auth', + properties: { + rules: [ + { + groupClaim: 'cognito:groups', + provider: 'userPools', + allow: 'groups', + groups: ['Admin'], + operations: ['create', 'update', 'delete', 'read'], + }, + ], + }, + }, + ], + primaryKeyInfo: { + isCustomPrimaryKey: false, + primaryKeyFieldName: 'id', + sortKeyFieldNames: [], + }, + }, }, enums: { Status: { diff --git a/packages/api-graphql/__tests__/fixtures/modeled/schema.ts b/packages/api-graphql/__tests__/fixtures/modeled/schema.ts index ee74b42e160..d95d8515ca1 100644 --- a/packages/api-graphql/__tests__/fixtures/modeled/schema.ts +++ b/packages/api-graphql/__tests__/fixtures/modeled/schema.ts @@ -220,6 +220,11 @@ const schema = a.schema({ description: a.string(), }) .authorization([a.allow.groupsDefinedIn('groupsField')]), + ModelStaticGroup: a + .model({ + description: a.string(), + }) + .authorization([a.allow.specificGroup('Admin')]), // #endregion }); diff --git a/packages/api-graphql/__tests__/internals/generateClient.test.ts b/packages/api-graphql/__tests__/internals/generateClient.test.ts index 428600cdb4e..ead08f04e17 100644 --- a/packages/api-graphql/__tests__/internals/generateClient.test.ts +++ b/packages/api-graphql/__tests__/internals/generateClient.test.ts @@ -49,6 +49,7 @@ describe('generateClient', () => { 'CustomImplicitOwner', 'ModelGroupDefinedIn', 'ModelGroupsDefinedIn', + 'ModelStaticGroup', ]; it('generates `models` property when Amplify.getConfig() returns valid GraphQL provider config', () => { diff --git a/packages/api-graphql/__tests__/resolveOwnerFields.test.ts b/packages/api-graphql/__tests__/resolveOwnerFields.test.ts index 5feb87f37a5..e43b5119983 100644 --- a/packages/api-graphql/__tests__/resolveOwnerFields.test.ts +++ b/packages/api-graphql/__tests__/resolveOwnerFields.test.ts @@ -14,6 +14,9 @@ describe('owner field resolution', () => { ThingWithOwnerFieldSpecifiedInModel: ['owner'], ThingWithAPIKeyAuth: [], ThingWithoutExplicitAuth: [], + ModelGroupDefinedIn: ['groupField'], + ModelGroupsDefinedIn: ['groupsField'], + ModelStaticGroup: [], }; for (const [modelName, expected] of Object.entries(expectedResolutions)) { @@ -23,7 +26,7 @@ describe('owner field resolution', () => { const model: SchemaModel = modelIntroSchema.models[modelName]; const resolvedField = resolveOwnerFields(model); - expect(resolvedField).toEqual(expected); + expect(resolvedField).toStrictEqual(expected); }); } }); diff --git a/packages/api-graphql/src/utils/resolveOwnerFields.ts b/packages/api-graphql/src/utils/resolveOwnerFields.ts index ff90a315326..47d8cdea3ef 100644 --- a/packages/api-graphql/src/utils/resolveOwnerFields.ts +++ b/packages/api-graphql/src/utils/resolveOwnerFields.ts @@ -42,7 +42,10 @@ export function resolveOwnerFields(model: Model): string[] { for (const rule of attr.properties.rules) { if (rule.allow === 'owner') { ownerFields.add(rule.ownerField || 'owner'); - } else if (rule.allow === 'groups') { + } else if (rule.allow === 'groups' && rule.groupsField !== undefined) { + // only valid for dynamic group(s) + // static group auth will have an array of predefined groups in the attribute, groups: string[] + // but `groupsField` will be undefined ownerFields.add(rule.groupsField); } } From 27c191755430fa2d6949e26654fb81968bfdeae9 Mon Sep 17 00:00:00 2001 From: Ivan Artemiev <29709626+iartemiev@users.noreply.github.com> Date: Thu, 28 Mar 2024 17:43:53 -0400 Subject: [PATCH 6/7] Revert "fix(api-graphql): default selection set regression" (#13187) Revert "fix(api-graphql): default selection set regression (#13185)" This reverts commit 873a6f47b24b6f8c97bf3af001db70f204f67daa. --- .../fixtures/modeled/amplifyconfiguration.ts | 64 +------------------ .../__tests__/fixtures/modeled/schema.ts | 5 -- .../internals/generateClient.test.ts | 1 - .../__tests__/resolveOwnerFields.test.ts | 5 +- .../src/utils/resolveOwnerFields.ts | 5 +- 5 files changed, 3 insertions(+), 77 deletions(-) diff --git a/packages/api-graphql/__tests__/fixtures/modeled/amplifyconfiguration.ts b/packages/api-graphql/__tests__/fixtures/modeled/amplifyconfiguration.ts index 77c21ad8757..a45cb28da94 100644 --- a/packages/api-graphql/__tests__/fixtures/modeled/amplifyconfiguration.ts +++ b/packages/api-graphql/__tests__/fixtures/modeled/amplifyconfiguration.ts @@ -1,5 +1,5 @@ /** - * Generated from `./schema.ts` by amplify-backend generate config. + * Generated from `./schema.ts` by samsara. * * Cognito fields etc. omitted. */ @@ -1656,68 +1656,6 @@ const amplifyConfig = { sortKeyFieldNames: [], }, }, - ModelStaticGroup: { - name: 'ModelStaticGroup', - fields: { - id: { - name: 'id', - isArray: false, - type: 'ID', - isRequired: true, - attributes: [], - }, - description: { - name: 'description', - isArray: false, - type: 'String', - isRequired: false, - attributes: [], - }, - createdAt: { - name: 'createdAt', - isArray: false, - type: 'AWSDateTime', - isRequired: false, - attributes: [], - isReadOnly: true, - }, - updatedAt: { - name: 'updatedAt', - isArray: false, - type: 'AWSDateTime', - isRequired: false, - attributes: [], - isReadOnly: true, - }, - }, - syncable: true, - pluralName: 'ModelStaticGroups', - attributes: [ - { - type: 'model', - properties: {}, - }, - { - type: 'auth', - properties: { - rules: [ - { - groupClaim: 'cognito:groups', - provider: 'userPools', - allow: 'groups', - groups: ['Admin'], - operations: ['create', 'update', 'delete', 'read'], - }, - ], - }, - }, - ], - primaryKeyInfo: { - isCustomPrimaryKey: false, - primaryKeyFieldName: 'id', - sortKeyFieldNames: [], - }, - }, }, enums: { Status: { diff --git a/packages/api-graphql/__tests__/fixtures/modeled/schema.ts b/packages/api-graphql/__tests__/fixtures/modeled/schema.ts index d95d8515ca1..ee74b42e160 100644 --- a/packages/api-graphql/__tests__/fixtures/modeled/schema.ts +++ b/packages/api-graphql/__tests__/fixtures/modeled/schema.ts @@ -220,11 +220,6 @@ const schema = a.schema({ description: a.string(), }) .authorization([a.allow.groupsDefinedIn('groupsField')]), - ModelStaticGroup: a - .model({ - description: a.string(), - }) - .authorization([a.allow.specificGroup('Admin')]), // #endregion }); diff --git a/packages/api-graphql/__tests__/internals/generateClient.test.ts b/packages/api-graphql/__tests__/internals/generateClient.test.ts index ead08f04e17..428600cdb4e 100644 --- a/packages/api-graphql/__tests__/internals/generateClient.test.ts +++ b/packages/api-graphql/__tests__/internals/generateClient.test.ts @@ -49,7 +49,6 @@ describe('generateClient', () => { 'CustomImplicitOwner', 'ModelGroupDefinedIn', 'ModelGroupsDefinedIn', - 'ModelStaticGroup', ]; it('generates `models` property when Amplify.getConfig() returns valid GraphQL provider config', () => { diff --git a/packages/api-graphql/__tests__/resolveOwnerFields.test.ts b/packages/api-graphql/__tests__/resolveOwnerFields.test.ts index e43b5119983..5feb87f37a5 100644 --- a/packages/api-graphql/__tests__/resolveOwnerFields.test.ts +++ b/packages/api-graphql/__tests__/resolveOwnerFields.test.ts @@ -14,9 +14,6 @@ describe('owner field resolution', () => { ThingWithOwnerFieldSpecifiedInModel: ['owner'], ThingWithAPIKeyAuth: [], ThingWithoutExplicitAuth: [], - ModelGroupDefinedIn: ['groupField'], - ModelGroupsDefinedIn: ['groupsField'], - ModelStaticGroup: [], }; for (const [modelName, expected] of Object.entries(expectedResolutions)) { @@ -26,7 +23,7 @@ describe('owner field resolution', () => { const model: SchemaModel = modelIntroSchema.models[modelName]; const resolvedField = resolveOwnerFields(model); - expect(resolvedField).toStrictEqual(expected); + expect(resolvedField).toEqual(expected); }); } }); diff --git a/packages/api-graphql/src/utils/resolveOwnerFields.ts b/packages/api-graphql/src/utils/resolveOwnerFields.ts index 47d8cdea3ef..ff90a315326 100644 --- a/packages/api-graphql/src/utils/resolveOwnerFields.ts +++ b/packages/api-graphql/src/utils/resolveOwnerFields.ts @@ -42,10 +42,7 @@ export function resolveOwnerFields(model: Model): string[] { for (const rule of attr.properties.rules) { if (rule.allow === 'owner') { ownerFields.add(rule.ownerField || 'owner'); - } else if (rule.allow === 'groups' && rule.groupsField !== undefined) { - // only valid for dynamic group(s) - // static group auth will have an array of predefined groups in the attribute, groups: string[] - // but `groupsField` will be undefined + } else if (rule.allow === 'groups') { ownerFields.add(rule.groupsField); } } From 5e16682c8311b9020e4828143c679f4ea645dcd2 Mon Sep 17 00:00:00 2001 From: David McAfee Date: Fri, 29 Mar 2024 09:44:31 -0700 Subject: [PATCH 7/7] fix(data): conditional inclusion of sort keys / `sortDirection` for list (#13189) --- .../__snapshots__/generateClient.test.ts.snap | 250 ++++-------------- ...rateClientWithAmplifyInstance.test.ts.snap | 50 +--- .../api-graphql/src/internals/APIClient.ts | 19 +- 3 files changed, 70 insertions(+), 249 deletions(-) diff --git a/packages/api-graphql/__tests__/internals/__snapshots__/generateClient.test.ts.snap b/packages/api-graphql/__tests__/internals/__snapshots__/generateClient.test.ts.snap index b468f595825..2f24c9d4731 100644 --- a/packages/api-graphql/__tests__/internals/__snapshots__/generateClient.test.ts.snap +++ b/packages/api-graphql/__tests__/internals/__snapshots__/generateClient.test.ts.snap @@ -252,14 +252,8 @@ exports[`generateClient basic model operations - authMode: CuP override at the t "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelNoteFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listNotes( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelNoteFilterInput, $limit: Int, $nextToken: String) { + listNotes(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id body @@ -494,14 +488,8 @@ exports[`generateClient basic model operations - authMode: CuP override at the t "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelNoteFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listNotes( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelNoteFilterInput, $limit: Int, $nextToken: String) { + listNotes(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id body @@ -620,14 +608,8 @@ exports[`generateClient basic model operations - authMode: CuP override at the t "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listTodos( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { + listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id name @@ -965,14 +947,8 @@ exports[`generateClient basic model operations - authMode: CuP override in the c "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelNoteFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listNotes( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelNoteFilterInput, $limit: Int, $nextToken: String) { + listNotes(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id body @@ -1207,14 +1183,8 @@ exports[`generateClient basic model operations - authMode: CuP override in the c "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelNoteFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listNotes( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelNoteFilterInput, $limit: Int, $nextToken: String) { + listNotes(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id body @@ -1333,14 +1303,8 @@ exports[`generateClient basic model operations - authMode: CuP override in the c "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listTodos( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { + listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id name @@ -1678,14 +1642,8 @@ exports[`generateClient basic model operations - authMode: lambda override at th "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelNoteFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listNotes( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelNoteFilterInput, $limit: Int, $nextToken: String) { + listNotes(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id body @@ -1923,14 +1881,8 @@ exports[`generateClient basic model operations - authMode: lambda override at th "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelNoteFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listNotes( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelNoteFilterInput, $limit: Int, $nextToken: String) { + listNotes(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id body @@ -2055,14 +2007,8 @@ exports[`generateClient basic model operations - authMode: lambda override at th "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listTodos( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { + listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id name @@ -2400,14 +2346,8 @@ exports[`generateClient basic model operations - authMode: lambda override in th "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelNoteFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listNotes( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelNoteFilterInput, $limit: Int, $nextToken: String) { + listNotes(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id body @@ -2645,14 +2585,8 @@ exports[`generateClient basic model operations - authMode: lambda override in th "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelNoteFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listNotes( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelNoteFilterInput, $limit: Int, $nextToken: String) { + listNotes(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id body @@ -2777,14 +2711,8 @@ exports[`generateClient basic model operations - authMode: lambda override in th "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listTodos( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { + listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id name @@ -3330,14 +3258,8 @@ exports[`generateClient basic model operations - custom client and request heade "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listTodos( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { + listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id name @@ -3384,14 +3306,8 @@ exports[`generateClient basic model operations - custom client and request heade "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listTodos( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { + listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id name @@ -3750,14 +3666,8 @@ exports[`generateClient basic model operations can lazy load @hasMany 1`] = ` "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelNoteFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listNotes( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelNoteFilterInput, $limit: Int, $nextToken: String) { + listNotes(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id body @@ -3838,14 +3748,8 @@ exports[`generateClient basic model operations can lazy load @hasMany with limit "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelNoteFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listNotes( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelNoteFilterInput, $limit: Int, $nextToken: String) { + listNotes(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id body @@ -3927,14 +3831,8 @@ exports[`generateClient basic model operations can lazy load @hasMany with nextT "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelNoteFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listNotes( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelNoteFilterInput, $limit: Int, $nextToken: String) { + listNotes(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id body @@ -4051,14 +3949,8 @@ exports[`generateClient basic model operations can list() 1`] = ` "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listTodos( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { + listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id name @@ -4104,14 +3996,8 @@ exports[`generateClient basic model operations can list() with limit 1`] = ` "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listTodos( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { + listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id name @@ -4158,14 +4044,8 @@ exports[`generateClient basic model operations can list() with nextToken 1`] = ` "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listTodos( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { + listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id name @@ -4648,14 +4528,8 @@ exports[`generateClient basic model operations with Amplify configuration option "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listTodos( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { + listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id name @@ -4703,14 +4577,8 @@ exports[`generateClient basic model operations with Amplify configuration option "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listTodos( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { + listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id name @@ -5094,14 +4962,8 @@ exports[`generateClient custom operations can return model (Post) that with lazy "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelCommentFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listComments( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelCommentFilterInput, $limit: Int, $nextToken: String) { + listComments(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id content @@ -5635,14 +5497,8 @@ exports[`generateClient observeQuery can paginate through initial results 1`] = "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listTodos( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { + listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id name @@ -5678,14 +5534,8 @@ exports[`generateClient observeQuery can paginate through initial results 1`] = "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listTodos( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { + listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id name diff --git a/packages/api-graphql/__tests__/internals/server/__snapshots__/generateClientWithAmplifyInstance.test.ts.snap b/packages/api-graphql/__tests__/internals/server/__snapshots__/generateClientWithAmplifyInstance.test.ts.snap index d35a077077c..c5364335a3c 100644 --- a/packages/api-graphql/__tests__/internals/server/__snapshots__/generateClientWithAmplifyInstance.test.ts.snap +++ b/packages/api-graphql/__tests__/internals/server/__snapshots__/generateClientWithAmplifyInstance.test.ts.snap @@ -39,14 +39,8 @@ exports[`server generateClient with cookies can list 1`] = ` "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listTodos( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { + listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id name @@ -92,14 +86,8 @@ exports[`server generateClient with cookies can list with limit 1`] = ` "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listTodos( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { + listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id name @@ -146,14 +134,8 @@ exports[`server generateClient with cookies can list with nextToken 1`] = ` "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listTodos( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { + listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id name @@ -200,14 +182,8 @@ exports[`server generateClient with cookies can list with sort direction (ascend "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listTodos( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { + listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id name @@ -250,14 +226,8 @@ exports[`server generateClient with cookies can list with sort direction (descen "abortController": AbortController {}, "options": { "body": { - "query": "query ($filter: ModelTodoFilterInput, $sortDirection: ModelSortDirection, $id: ID, $limit: Int, $nextToken: String) { - listTodos( - filter: $filter - sortDirection: $sortDirection - id: $id - limit: $limit - nextToken: $nextToken - ) { + "query": "query ($filter: ModelTodoFilterInput, $limit: Int, $nextToken: String) { + listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) { items { id name diff --git a/packages/api-graphql/src/internals/APIClient.ts b/packages/api-graphql/src/internals/APIClient.ts index d6f39844401..4609703f894 100644 --- a/packages/api-graphql/src/internals/APIClient.ts +++ b/packages/api-graphql/src/internals/APIClient.ts @@ -732,15 +732,16 @@ export function generateGraphQLDocument( graphQLArguments ?? (graphQLArguments = { filter: `Model${name}FilterInput`, - sortDirection: 'ModelSortDirection', - ...[primaryKeyFieldName, ...sortKeyFieldNames].reduce( - (acc: Record, fieldName) => { - acc[fieldName] = `${fields[fieldName].type}`; - - return acc; - }, - [], - ), + ...(sortKeyFieldNames.length > 0 + ? [primaryKeyFieldName, ...sortKeyFieldNames].reduce( + (acc: Record, fieldName) => { + acc[fieldName] = `${fields[fieldName].type}`; + + return acc; + }, + { sortDirection: 'ModelSortDirection' }, + ) + : []), limit: 'Int', nextToken: 'String', });