diff --git a/packages/api-graphql/__tests__/AWSAppSyncRealTimeProvider.test.ts b/packages/api-graphql/__tests__/AWSAppSyncRealTimeProvider.test.ts index 70156efb52..4500ce8a56 100644 --- a/packages/api-graphql/__tests__/AWSAppSyncRealTimeProvider.test.ts +++ b/packages/api-graphql/__tests__/AWSAppSyncRealTimeProvider.test.ts @@ -1055,6 +1055,34 @@ describe('AWSAppSyncRealTimeProvider', () => { ); }); + test('authenticating with userPool / custom library options token', async () => { + expect.assertions(1); + + provider + .subscribe({ + appSyncGraphqlEndpoint: 'ws://localhost:8080', + authenticationType: 'userPool', + /** + * When Amplify is configured with a `header` function + * that returns an `Authorization` token, the GraphQL + * API will pass this function as the `libraryConfigHeaders` + * option to the AWSAppSyncRealTimeProvider's `subscribe` + * function. + */ + libraryConfigHeaders: async () => ({ + Authorization: 'test', + }), + }) + .subscribe({ error: () => {} }); + + await fakeWebSocketInterface?.readyForUse; + + expect(loggerSpy).toHaveBeenCalledWith( + 'DEBUG', + 'Authenticating with "userPool"' + ); + }); + test('authenticating with AWS_LAMBDA/custom w/ custom header function', async () => { expect.assertions(1); diff --git a/packages/api-graphql/__tests__/generateClient.test.ts b/packages/api-graphql/__tests__/generateClient.test.ts index efe6ac50b5..272d765c40 100644 --- a/packages/api-graphql/__tests__/generateClient.test.ts +++ b/packages/api-graphql/__tests__/generateClient.test.ts @@ -7,6 +7,7 @@ import { expectSub, expectSubWithHeaders, expectSubWithHeadersFn, + expectSubWithlibraryConfigHeaders, } from './utils/expects'; import { Observable, from } from 'rxjs'; import * as internals from '../src/internals/'; @@ -4606,6 +4607,868 @@ describe('generateClient', () => { }); }); }); + describe('basic model operations with Amplify configuration options headers', () => { + beforeEach(() => { + jest.clearAllMocks(); + + Amplify.configure(configFixture as any, { + API: { + GraphQL: { + // This is what we're testing: + headers: async () => ({ + Authorization: 'amplify-config-auth-token', + }), + }, + }, + }); + }); + + test('can create() - with library config headers', async () => { + const spy = mockApiResponse({ + data: { + createTodo: { + __typename: 'Todo', + ...serverManagedFields, + name: 'some name', + description: 'something something', + }, + }, + }); + + const client = generateClient({ + amplify: Amplify, + headers: { + 'client-header': 'should exist', + }, + }); + + const { data } = await client.models.Todo.create({ + name: 'some name', + description: 'something something', + }); + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + options: expect.objectContaining({ + headers: expect.objectContaining({ + 'X-Api-Key': 'FAKE-KEY', + Authorization: 'amplify-config-auth-token', + }), + body: { + query: expect.stringContaining('createTodo(input: $input)'), + variables: { + input: { + name: 'some name', + description: 'something something', + }, + }, + }, + }), + }) + ); + + expect(data).toEqual( + expect.objectContaining({ + __typename: 'Todo', + id: 'some-id', + owner: 'wirejobviously', + name: 'some name', + description: 'something something', + }) + ); + }); + + test('can create() - custom client headers should not overwrite library config headers', async () => { + const spy = mockApiResponse({ + data: { + createTodo: { + __typename: 'Todo', + ...serverManagedFields, + name: 'some name', + description: 'something something', + }, + }, + }); + + const client = generateClient({ + amplify: Amplify, + headers: { + 'client-header': 'should exist', + }, + }); + + const { data } = await client.models.Todo.create({ + name: 'some name', + description: 'something something', + }); + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + options: expect.objectContaining({ + headers: expect.objectContaining({ + 'X-Api-Key': 'FAKE-KEY', + 'client-header': 'should exist', + Authorization: 'amplify-config-auth-token', + }), + body: { + query: expect.stringContaining('createTodo(input: $input)'), + variables: { + input: { + name: 'some name', + description: 'something something', + }, + }, + }, + }), + }) + ); + + expect(data).toEqual( + expect.objectContaining({ + __typename: 'Todo', + id: 'some-id', + owner: 'wirejobviously', + name: 'some name', + description: 'something something', + }) + ); + }); + + test('can create() - custom request headers should not overwrite library config headers', async () => { + const spy = mockApiResponse({ + data: { + createTodo: { + __typename: 'Todo', + ...serverManagedFields, + name: 'some name', + description: 'something something', + }, + }, + }); + + const client = generateClient({ + amplify: Amplify, + }); + + const { data } = await client.models.Todo.create( + { + name: 'some name', + description: 'something something', + }, + { + headers: { + 'request-header': 'should exist', + }, + } + ); + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + options: expect.objectContaining({ + headers: expect.objectContaining({ + 'X-Api-Key': 'FAKE-KEY', + 'request-header': 'should exist', + Authorization: 'amplify-config-auth-token', + }), + body: { + query: expect.stringContaining('createTodo(input: $input)'), + variables: { + input: { + name: 'some name', + description: 'something something', + }, + }, + }, + }), + }) + ); + + expect(data).toEqual( + expect.objectContaining({ + __typename: 'Todo', + id: 'some-id', + owner: 'wirejobviously', + name: 'some name', + description: 'something something', + }) + ); + }); + + test('can get() - custom client headers should not overwrite library config headers', async () => { + const spy = mockApiResponse({ + data: { + getTodo: { + __typename: 'Todo', + ...serverManagedFields, + name: 'some name', + description: 'something something', + }, + }, + }); + + const client = generateClient({ + amplify: Amplify, + headers: { + 'client-header': 'should exist', + }, + }); + const { data } = await client.models.Todo.get({ id: 'asdf' }); + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + options: expect.objectContaining({ + headers: expect.objectContaining({ + 'X-Api-Key': 'FAKE-KEY', + 'client-header': 'should exist', + Authorization: 'amplify-config-auth-token', + }), + body: { + query: expect.stringContaining('getTodo(id: $id)'), + variables: { + id: 'asdf', + }, + }, + }), + }) + ); + + expect(data).toEqual( + expect.objectContaining({ + __typename: 'Todo', + id: 'some-id', + owner: 'wirejobviously', + name: 'some name', + description: 'something something', + }) + ); + }); + + test('can get() - custom request headers overwrite client headers, but not library config headers', async () => { + const spy = mockApiResponse({ + data: { + getTodo: { + __typename: 'Todo', + ...serverManagedFields, + name: 'some name', + description: 'something something', + }, + }, + }); + + const client = generateClient({ + amplify: Amplify, + headers: { + 'client-header': 'should not exist', + }, + }); + const { data } = await client.models.Todo.get( + { id: 'asdf' }, + { + headers: { + 'request-header': 'should exist', + }, + } + ); + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + options: expect.objectContaining({ + headers: expect.objectContaining({ + 'X-Api-Key': 'FAKE-KEY', + 'request-header': 'should exist', + Authorization: 'amplify-config-auth-token', + }), + body: { + query: expect.stringContaining('getTodo(id: $id)'), + variables: { + id: 'asdf', + }, + }, + }), + }) + ); + + // Request headers should overwrite client headers: + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + options: expect.objectContaining({ + headers: expect.not.objectContaining({ + 'client-header': 'should not exist', + }), + }), + }) + ); + + expect(data).toEqual( + expect.objectContaining({ + __typename: 'Todo', + id: 'some-id', + owner: 'wirejobviously', + name: 'some name', + description: 'something something', + }) + ); + }); + + test('can list() - custom client headers should not overwrite library config headers', async () => { + const spy = mockApiResponse({ + data: { + listTodos: { + items: [ + { + __typename: 'Todo', + ...serverManagedFields, + name: 'some name', + description: 'something something', + }, + ], + }, + }, + }); + + const client = generateClient({ + amplify: Amplify, + headers: { + 'client-header': 'should exist', + }, + }); + const { data } = await client.models.Todo.list({ + filter: { name: { contains: 'name' } }, + }); + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + options: expect.objectContaining({ + headers: expect.objectContaining({ + 'X-Api-Key': 'FAKE-KEY', + 'client-header': 'should exist', + Authorization: 'amplify-config-auth-token', + }), + body: { + query: expect.stringContaining( + 'listTodos(filter: $filter, limit: $limit, nextToken: $nextToken)' + ), + variables: { + filter: { + name: { + contains: 'name', + }, + }, + }, + }, + }), + }) + ); + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + options: expect.objectContaining({ + body: expect.objectContaining({ + // match nextToken in selection set + query: expect.stringMatching(/^\s*nextToken\s*$/m), + }), + }), + }) + ); + + expect(data.length).toBe(1); + expect(data[0]).toEqual( + expect.objectContaining({ + __typename: 'Todo', + id: 'some-id', + owner: 'wirejobviously', + name: 'some name', + description: 'something something', + }) + ); + }); + + test('can list() - custom request headers should overwrite client headers but not library config headers', async () => { + const spy = mockApiResponse({ + data: { + listTodos: { + items: [ + { + __typename: 'Todo', + ...serverManagedFields, + name: 'some name', + description: 'something something', + }, + ], + }, + }, + }); + + const client = generateClient({ + amplify: Amplify, + headers: { + 'client-header': 'should not exist', + }, + }); + const { data } = await client.models.Todo.list({ + filter: { name: { contains: 'name' } }, + headers: { + 'request-header': 'should exist', + }, + }); + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + options: expect.objectContaining({ + headers: expect.objectContaining({ + 'X-Api-Key': 'FAKE-KEY', + 'request-header': 'should exist', + Authorization: 'amplify-config-auth-token', + }), + body: { + query: expect.stringContaining( + 'listTodos(filter: $filter, limit: $limit, nextToken: $nextToken)' + ), + variables: { + filter: { + name: { + contains: 'name', + }, + }, + }, + }, + }), + }) + ); + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + options: expect.objectContaining({ + headers: expect.not.objectContaining({ + 'client-header': 'should not exist', + }), + }), + }) + ); + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + options: expect.objectContaining({ + body: expect.objectContaining({ + // match nextToken in selection set + query: expect.stringMatching(/^\s*nextToken\s*$/m), + }), + }), + }) + ); + + expect(data.length).toBe(1); + expect(data[0]).toEqual( + expect.objectContaining({ + __typename: 'Todo', + id: 'some-id', + owner: 'wirejobviously', + name: 'some name', + description: 'something something', + }) + ); + }); + + test('can update() - custom client headers should not overwrite library config headers', async () => { + const spy = mockApiResponse({ + data: { + updateTodo: { + __typename: 'Todo', + ...serverManagedFields, + name: 'some other name', + description: 'something something', + }, + }, + }); + + const client = generateClient({ + amplify: Amplify, + headers: { + 'client-header': 'should exist', + }, + }); + const { data } = await client.models.Todo.update({ + id: 'some-id', + name: 'some other name', + }); + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + options: expect.objectContaining({ + headers: expect.objectContaining({ + 'X-Api-Key': 'FAKE-KEY', + 'client-header': 'should exist', + Authorization: 'amplify-config-auth-token', + }), + body: { + query: expect.stringContaining('updateTodo(input: $input)'), + variables: { + input: { + id: 'some-id', + name: 'some other name', + }, + }, + }, + }), + }) + ); + + expect(data).toEqual( + expect.objectContaining({ + __typename: 'Todo', + id: 'some-id', + owner: 'wirejobviously', + name: 'some other name', + description: 'something something', + }) + ); + }); + + test('can update() - custom request headers should overwrite client headers but not library config headers', async () => { + const spy = mockApiResponse({ + data: { + updateTodo: { + __typename: 'Todo', + ...serverManagedFields, + name: 'some other name', + description: 'something something', + }, + }, + }); + + const client = generateClient({ + amplify: Amplify, + headers: { + 'client-header': 'should exist', + }, + }); + const { data } = await client.models.Todo.update( + { + id: 'some-id', + name: 'some other name', + }, + { + headers: { + 'request-header': 'should exist', + }, + } + ); + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + options: expect.objectContaining({ + headers: expect.objectContaining({ + 'X-Api-Key': 'FAKE-KEY', + 'request-header': 'should exist', + Authorization: 'amplify-config-auth-token', + }), + body: { + query: expect.stringContaining('updateTodo(input: $input)'), + variables: { + input: { + id: 'some-id', + name: 'some other name', + }, + }, + }, + }), + }) + ); + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + options: expect.objectContaining({ + headers: expect.not.objectContaining({ + 'client-header': 'should not exist', + }), + }), + }) + ); + + expect(data).toEqual( + expect.objectContaining({ + __typename: 'Todo', + id: 'some-id', + owner: 'wirejobviously', + name: 'some other name', + description: 'something something', + }) + ); + }); + + test('can delete() - custom client headers should not overwrite library config headers', async () => { + const spy = mockApiResponse({ + data: { + deleteTodo: { + __typename: 'Todo', + ...serverManagedFields, + name: 'some name', + description: 'something something', + }, + }, + }); + + const client = generateClient({ + amplify: Amplify, + headers: { + 'client-header': 'should exist', + }, + }); + const { data } = await client.models.Todo.delete({ + id: 'some-id', + }); + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + options: expect.objectContaining({ + headers: expect.objectContaining({ + 'X-Api-Key': 'FAKE-KEY', + 'client-header': 'should exist', + Authorization: 'amplify-config-auth-token', + }), + body: { + query: expect.stringContaining('deleteTodo(input: $input)'), + variables: { + input: { + id: 'some-id', + }, + }, + }, + }), + }) + ); + + expect(data).toEqual( + expect.objectContaining({ + __typename: 'Todo', + id: 'some-id', + owner: 'wirejobviously', + name: 'some name', + description: 'something something', + }) + ); + }); + + test('can delete() - custom request headers should overwrite client headers but not library config headers', async () => { + const spy = mockApiResponse({ + data: { + deleteTodo: { + __typename: 'Todo', + ...serverManagedFields, + name: 'some name', + description: 'something something', + }, + }, + }); + + const client = generateClient({ + amplify: Amplify, + headers: { + 'client-header': 'should not exist', + }, + }); + const { data } = await client.models.Todo.delete( + { + id: 'some-id', + }, + { + headers: { + 'request-header': 'should exist', + }, + } + ); + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + options: expect.objectContaining({ + headers: expect.objectContaining({ + 'X-Api-Key': 'FAKE-KEY', + 'request-header': 'should exist', + Authorization: 'amplify-config-auth-token', + }), + body: { + query: expect.stringContaining('deleteTodo(input: $input)'), + variables: { + input: { + id: 'some-id', + }, + }, + }, + }), + }) + ); + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + options: expect.objectContaining({ + headers: expect.not.objectContaining({ + 'client-header': 'should not exist', + }), + }), + }) + ); + + expect(data).toEqual( + expect.objectContaining({ + __typename: 'Todo', + id: 'some-id', + owner: 'wirejobviously', + name: 'some name', + description: 'something something', + }) + ); + }); + + test('can subscribe to onCreate() - with custom headers and library config headers', done => { + const noteToSend = { + __typename: 'Note', + ...serverManagedFields, + body: 'a very good note', + }; + + const graphqlMessage = { + data: { + onCreateNote: noteToSend, + }, + }; + + const graphqlVariables = { + filter: { + body: { contains: 'good note' }, + }, + }; + + const customHeaders = { + 'subscription-header': 'should-exist', + }; + + const client = generateClient({ amplify: Amplify }); + + const spy = jest.fn(() => from([graphqlMessage])); + (raw.GraphQLAPI as any).appSyncRealTime = { subscribe: spy }; + + client.models.Note.onCreate({ + filter: graphqlVariables.filter, + headers: customHeaders, + }).subscribe({ + next(value) { + // This util checks for the existence of library config headers: + expectSubWithlibraryConfigHeaders( + spy, + 'onCreateNote', + graphqlVariables, + customHeaders + ); + expect(value).toEqual(expect.objectContaining(noteToSend)); + done(); + }, + error(error) { + expect(error).toBeUndefined(); + done('bad news!'); + }, + }); + }); + + test('can subscribe to onUpdate() - with a custom header and library config headers', done => { + const noteToSend = { + __typename: 'Note', + ...serverManagedFields, + body: 'a very good note', + }; + + const graphqlMessage = { + data: { + onUpdateNote: noteToSend, + }, + }; + + const graphqlVariables = { + filter: { + body: { contains: 'good note' }, + }, + }; + + const customHeaders = { + 'subscription-header': 'should-exist', + }; + + const client = generateClient({ amplify: Amplify }); + + const spy = jest.fn(() => from([graphqlMessage])); + (raw.GraphQLAPI as any).appSyncRealTime = { subscribe: spy }; + + client.models.Note.onUpdate({ + filter: graphqlVariables.filter, + headers: customHeaders, + }).subscribe({ + next(value) { + // This util checks for the existence of library config headers: + expectSubWithlibraryConfigHeaders( + spy, + 'onUpdateNote', + graphqlVariables, + customHeaders + ); + expect(value).toEqual(expect.objectContaining(noteToSend)); + done(); + }, + error(error) { + expect(error).toBeUndefined(); + done('bad news!'); + }, + }); + }); + + test('can subscribe to onDelete() - with custom headers and library config headers', done => { + const noteToSend = { + __typename: 'Note', + ...serverManagedFields, + body: 'a very good note', + }; + + const graphqlMessage = { + data: { + onDeleteNote: noteToSend, + }, + }; + + const graphqlVariables = { + filter: { + body: { contains: 'good note' }, + }, + }; + + const customHeaders = { + 'subscription-header': 'should-exist', + }; + + const client = generateClient({ amplify: Amplify }); + + const spy = jest.fn(() => from([graphqlMessage])); + (raw.GraphQLAPI as any).appSyncRealTime = { subscribe: spy }; + + client.models.Note.onDelete({ + filter: graphqlVariables.filter, + headers: customHeaders, + }).subscribe({ + next(value) { + // This util checks for the existence of library config headers: + expectSubWithlibraryConfigHeaders( + spy, + 'onDeleteNote', + graphqlVariables, + customHeaders + ); + expect(value).toEqual(expect.objectContaining(noteToSend)); + done(); + }, + error(error) { + expect(error).toBeUndefined(); + done('bad news!'); + }, + }); + }); + }); describe('observeQuery', () => { beforeEach(() => { diff --git a/packages/api-graphql/__tests__/utils/expects.ts b/packages/api-graphql/__tests__/utils/expects.ts index 2ffc07aa54..6794a82943 100644 --- a/packages/api-graphql/__tests__/utils/expects.ts +++ b/packages/api-graphql/__tests__/utils/expects.ts @@ -176,3 +176,38 @@ export function expectSubWithHeadersFn( } ); } + +/** + * Performs an `expect()` on a jest spy with some basic nested argument checks + * based on the given subscription `opName` and `item`. + * Used specifically for testing subscriptions with additional headers. + * + * @param spy The jest spy to check. + * @param opName The name of the graphql operation. E.g., `onCreateTodo`. + * @param item The item we expect to have been in the `variables` + * @param libraryConfigHeaders TODO + */ +export function expectSubWithlibraryConfigHeaders( + spy: jest.SpyInstance, + opName: string, + item: Record, + headers?: CustomHeaders +) { + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + authenticationType: 'apiKey', + apiKey: 'FAKE-KEY', + appSyncGraphqlEndpoint: 'https://localhost/graphql', + // Code-gen'd queries have an owner param; TypeBeast queries don't: + query: expect.stringContaining(`${opName}(filter: $filter`), + variables: expect.objectContaining(item), + additionalHeaders: expect.objectContaining(headers), + // `headers` that are included in `Amplify.configure` options + libraryConfigHeaders: expect.any(Function), + }), + { + action: '1', + category: 'api', + } + ); +} diff --git a/packages/api-graphql/src/GraphQLAPI.ts b/packages/api-graphql/src/GraphQLAPI.ts index 41f79741bc..31699d6981 100644 --- a/packages/api-graphql/src/GraphQLAPI.ts +++ b/packages/api-graphql/src/GraphQLAPI.ts @@ -29,7 +29,7 @@ export class GraphQLAPIClass extends InternalGraphQLAPIClass { * Executes a GraphQL operation * * @param options - GraphQL Options - * @param [additionalHeaders] - headers to merge in after any `graphql_headers` set in the config + * @param [additionalHeaders] - headers to merge in after any `libraryConfigHeaders` set in the config * @returns An Observable if the query is a subscription query, else a promise of the graphql result. */ graphql( diff --git a/packages/api-graphql/src/Providers/AWSAppSyncRealTimeProvider/index.ts b/packages/api-graphql/src/Providers/AWSAppSyncRealTimeProvider/index.ts index 7b62404c58..a8fb82a05c 100644 --- a/packages/api-graphql/src/Providers/AWSAppSyncRealTimeProvider/index.ts +++ b/packages/api-graphql/src/Providers/AWSAppSyncRealTimeProvider/index.ts @@ -97,7 +97,7 @@ export interface AWSAppSyncRealTimeProviderOptions { variables?: Record; apiKey?: string; region?: string; - graphql_headers?: () => {} | (() => Promise<{}>); + libraryConfigHeaders?: () => {} | (() => Promise<{}>); additionalHeaders?: | Record | (() => Promise>); @@ -203,6 +203,7 @@ export class AWSAppSyncRealTimeProvider { additionalHeaders, apiKey, authToken, + libraryConfigHeaders, } = options || {}; return new Observable(observer => { @@ -235,6 +236,7 @@ export class AWSAppSyncRealTimeProvider { additionalHeaders, apiKey, authToken, + libraryConfigHeaders, }, observer, subscriptionId, @@ -313,7 +315,7 @@ export class AWSAppSyncRealTimeProvider { variables, apiKey, region, - graphql_headers = () => ({}), + libraryConfigHeaders = () => ({}), additionalHeaders = {}, authToken, } = options; @@ -361,7 +363,7 @@ export class AWSAppSyncRealTimeProvider { region, additionalCustomHeaders, })), - ...(await graphql_headers()), + ...(await libraryConfigHeaders()), ...additionalCustomHeaders, [USER_AGENT_HEADER]: getAmplifyUserAgent(customUserAgentDetails), }; diff --git a/packages/api-graphql/src/internals/InternalGraphQLAPI.ts b/packages/api-graphql/src/internals/InternalGraphQLAPI.ts index c473d64c91..31d7223c9a 100644 --- a/packages/api-graphql/src/internals/InternalGraphQLAPI.ts +++ b/packages/api-graphql/src/internals/InternalGraphQLAPI.ts @@ -149,7 +149,7 @@ export class InternalGraphQLAPIClass { * Executes a GraphQL operation * * @param options - GraphQL Options - * @param [additionalHeaders] - headers to merge in after any `graphql_headers` set in the config + * @param [additionalHeaders] - headers to merge in after any `libraryConfigHeaders` set in the config * @returns An Observable if the query is a subscription query, else a promise of the graphql result. */ graphql( @@ -236,7 +236,7 @@ export class InternalGraphQLAPIClass { endpoint: appSyncGraphqlEndpoint, customEndpoint, customEndpointRegion, - defaultAuthMode + defaultAuthMode, } = resolveConfig(amplify); const authMode = explicitAuthMode || defaultAuthMode || 'iam'; @@ -424,6 +424,16 @@ export class InternalGraphQLAPIClass { ): Observable { const config = resolveConfig(amplify); + /** + * Retrieve library options from Amplify configuration. + * `libraryConfigHeaders` are from the Amplify configuration options, + * and will not be overwritten by other custom headers. These are *not* + * the same as `additionalHeaders`, which are custom headers that are + * either 1)included when configuring the API client or 2) passed along + * with individual requests. + */ + const { headers: libraryConfigHeaders } = resolveLibraryOptions(amplify); + return this.appSyncRealTime.subscribe( { query: print(query as DocumentNode), @@ -434,6 +444,7 @@ export class InternalGraphQLAPIClass { apiKey: config?.apiKey, additionalHeaders, authToken, + libraryConfigHeaders, }, customUserAgentDetails ); diff --git a/packages/api-graphql/src/types/index.ts b/packages/api-graphql/src/types/index.ts index ff4c1a98b9..8703a23ea0 100644 --- a/packages/api-graphql/src/types/index.ts +++ b/packages/api-graphql/src/types/index.ts @@ -175,7 +175,7 @@ export interface AWSAppSyncRealTimeProviderOptions { variables?: Record; apiKey?: string; region?: string; - graphql_headers?: () => {} | (() => Promise<{}>); + libraryConfigHeaders?: () => {} | (() => Promise<{}>); additionalHeaders?: CustomHeaders; } diff --git a/packages/api-rest/src/apis/common/publicApis.ts b/packages/api-rest/src/apis/common/publicApis.ts index 4c82663e2f..1034f77b81 100644 --- a/packages/api-rest/src/apis/common/publicApis.ts +++ b/packages/api-rest/src/apis/common/publicApis.ts @@ -39,14 +39,14 @@ const publicHandler = ( apiPath, apiOptions?.queryParams ); - const libraryOptionsHeaders = + const libraryConfigHeaders = await amplify.libraryOptions?.API?.REST?.headers?.({ apiName, }); const { headers: invocationHeaders = {} } = apiOptions; const headers = { // custom headers from invocation options should precede library options - ...libraryOptionsHeaders, + ...libraryConfigHeaders, ...invocationHeaders, }; const signingServiceInfo = parseSigningInfo(url, { diff --git a/packages/api/src/internals/InternalAPI.ts b/packages/api/src/internals/InternalAPI.ts index d9d7aa5628..c8c7e6d8f5 100644 --- a/packages/api/src/internals/InternalAPI.ts +++ b/packages/api/src/internals/InternalAPI.ts @@ -62,7 +62,7 @@ export class InternalAPIClass { * Executes a GraphQL operation * * @param options - GraphQL Options - * @param [additionalHeaders] - headers to merge in after any `graphql_headers` set in the config + * @param [additionalHeaders] - headers to merge in after any `libraryConfigHeaders` set in the config * @returns An Observable if queryType is 'subscription', else a promise of the graphql result from the query. */ graphql( diff --git a/packages/core/src/singleton/API/types.ts b/packages/core/src/singleton/API/types.ts index 0a94301c5d..ca0d17b0f9 100644 --- a/packages/core/src/singleton/API/types.ts +++ b/packages/core/src/singleton/API/types.ts @@ -6,10 +6,10 @@ import { AtLeastOne } from '../types'; export type LibraryAPIOptions = { GraphQL?: { // custom headers for given GraphQL service. Will be applied to all operations. - headers?: (options: { - query: string; + headers?: (options?: { + query?: string; variables?: Record; - }) => Promise; + }) => Promise; withCredentials?: boolean; }; REST?: {