Skip to content

Commit 9ed9a08

Browse files
authored
Merge pull request #2390 from vz-tl/feature/http-context-support
Support HttpContext in LinkOptions and Context
2 parents e85d81a + eed80ad commit 9ed9a08

File tree

7 files changed

+158
-11
lines changed

7 files changed

+158
-11
lines changed

.changeset/mighty-buses-travel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'apollo-angular': minor
3+
---
4+
5+
Support HttpContext in HttpLink option and operation context

packages/apollo-angular/http/src/http-batch-link.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import { print } from 'graphql';
22
import { Observable } from 'rxjs';
3-
import { HttpClient, HttpHeaders } from '@angular/common/http';
3+
import { HttpClient, HttpContext, HttpHeaders } from '@angular/common/http';
44
import { Injectable } from '@angular/core';
55
import { ApolloLink } from '@apollo/client';
66
import { BatchLink } from '@apollo/client/link/batch';
77
import type { HttpLink } from './http-link';
88
import { Body, Context, OperationPrinter, Request } from './types';
9-
import { createHeadersWithClientAwareness, fetch, mergeHeaders, prioritize } from './utils';
9+
import {
10+
createHeadersWithClientAwareness,
11+
fetch,
12+
mergeHeaders,
13+
mergeHttpContext,
14+
prioritize,
15+
} from './utils';
1016

1117
export declare namespace HttpBatchLink {
1218
export type Options = {
@@ -61,6 +67,7 @@ export class HttpBatchLinkHandler extends ApolloLink {
6167
return new Observable((observer: any) => {
6268
const body = this.createBody(operations);
6369
const headers = this.createHeaders(operations);
70+
const context = this.createHttpContext(operations);
6471
const { method, uri, withCredentials } = this.createOptions(operations);
6572

6673
if (typeof uri === 'function') {
@@ -74,6 +81,7 @@ export class HttpBatchLinkHandler extends ApolloLink {
7481
options: {
7582
withCredentials,
7683
headers,
84+
context,
7785
},
7886
};
7987

@@ -162,6 +170,16 @@ export class HttpBatchLinkHandler extends ApolloLink {
162170
);
163171
}
164172

173+
private createHttpContext(operations: ApolloLink.Operation[]): HttpContext {
174+
return operations.reduce(
175+
(context: HttpContext, operation: ApolloLink.Operation) => {
176+
const { httpContext } = operation.getContext();
177+
return httpContext ? mergeHttpContext(httpContext, context) : context;
178+
},
179+
mergeHttpContext(this.options.httpContext, new HttpContext()),
180+
);
181+
}
182+
165183
private createBatchKey(operation: ApolloLink.Operation): string {
166184
const context: Context & { skipBatching?: boolean } = operation.getContext();
167185

packages/apollo-angular/http/src/http-link.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { print } from 'graphql';
22
import { Observable } from 'rxjs';
3-
import { HttpClient } from '@angular/common/http';
3+
import { HttpClient, HttpContext } from '@angular/common/http';
44
import { Injectable } from '@angular/core';
55
import { ApolloLink } from '@apollo/client';
66
import { pick } from './http-batch-link';
@@ -13,7 +13,7 @@ import {
1313
OperationPrinter,
1414
Request,
1515
} from './types';
16-
import { createHeadersWithClientAwareness, fetch, mergeHeaders } from './utils';
16+
import { createHeadersWithClientAwareness, fetch, mergeHeaders, mergeHttpContext } from './utils';
1717

1818
export declare namespace HttpLink {
1919
export interface Options extends FetchOptions, HttpRequestOptions {
@@ -49,6 +49,10 @@ export class HttpLinkHandler extends ApolloLink {
4949
const withCredentials = pick(context, this.options, 'withCredentials');
5050
const useMultipart = pick(context, this.options, 'useMultipart');
5151
const useGETForQueries = this.options.useGETForQueries === true;
52+
const httpContext = mergeHttpContext(
53+
context.httpContext,
54+
mergeHttpContext(this.options.httpContext, new HttpContext()),
55+
);
5256

5357
const isQuery = operation.query.definitions.some(
5458
def => def.kind === 'OperationDefinition' && def.operation === 'query',
@@ -69,6 +73,7 @@ export class HttpLinkHandler extends ApolloLink {
6973
withCredentials,
7074
useMultipart,
7175
headers: this.options.headers,
76+
context: httpContext,
7277
},
7378
};
7479

packages/apollo-angular/http/src/types.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { DocumentNode } from 'graphql';
2-
import { HttpHeaders } from '@angular/common/http';
2+
import { HttpContext, HttpHeaders } from '@angular/common/http';
33
import { ApolloLink } from '@apollo/client';
44

55
declare module '@apollo/client' {
@@ -10,6 +10,11 @@ export type HttpRequestOptions = {
1010
headers?: HttpHeaders;
1111
withCredentials?: boolean;
1212
useMultipart?: boolean;
13+
httpContext?: HttpContext;
14+
};
15+
16+
export type RequestOptions = Omit<HttpRequestOptions, 'httpContext'> & {
17+
context?: HttpContext;
1318
};
1419

1520
export type URIFunction = (operation: ApolloLink.Operation) => string;
@@ -36,7 +41,7 @@ export type Request = {
3641
method: string;
3742
url: string;
3843
body: Body | Body[];
39-
options: HttpRequestOptions;
44+
options: RequestOptions;
4045
};
4146

4247
export type ExtractedFiles = {

packages/apollo-angular/http/src/utils.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Observable } from 'rxjs';
2-
import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http';
2+
import { HttpClient, HttpContext, HttpHeaders, HttpResponse } from '@angular/common/http';
33
import { Body, ExtractedFiles, ExtractFiles, Request } from './types';
44

55
export const fetch = (
@@ -121,6 +121,20 @@ export const mergeHeaders = (
121121
return destination || source;
122122
};
123123

124+
export const mergeHttpContext = (
125+
source: HttpContext | undefined,
126+
destination: HttpContext,
127+
): HttpContext => {
128+
if (source && destination) {
129+
return [...source.keys()].reduce(
130+
(context, name) => context.set(name, source.get(name)),
131+
destination,
132+
);
133+
}
134+
135+
return destination || source;
136+
};
137+
124138
export function prioritize<T>(
125139
...values: [NonNullable<T>, ...T[]] | [...T[], NonNullable<T>]
126140
): NonNullable<T> {

packages/apollo-angular/http/tests/http-batch-link.spec.ts

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
2-
import { HttpHeaders, provideHttpClient } from '@angular/common/http';
1+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
2+
import {
3+
HttpClient,
4+
HttpContext,
5+
HttpContextToken,
6+
HttpHeaders,
7+
provideHttpClient,
8+
} from '@angular/common/http';
39
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
410
import { TestBed } from '@angular/core/testing';
511
import { ApolloLink, gql } from '@apollo/client';
@@ -759,4 +765,57 @@ describe('HttpBatchLink', () => {
759765
done();
760766
}, 50);
761767
}));
768+
769+
test('should merge httpContext from options and batch context and pass it on to HttpClient', () =>
770+
new Promise<void>(done => {
771+
const requestSpy = vi.spyOn(TestBed.inject(HttpClient), 'request');
772+
const contextToken1 = new HttpContextToken(() => '');
773+
const contextToken2 = new HttpContextToken(() => '');
774+
const contextToken3 = new HttpContextToken(() => '');
775+
const link = httpLink.create({
776+
uri: 'graphql',
777+
httpContext: new HttpContext().set(contextToken1, 'options'),
778+
batchKey: () => 'batchKey',
779+
});
780+
781+
const op1 = {
782+
query: gql`
783+
query heroes {
784+
heroes {
785+
name
786+
}
787+
}
788+
`,
789+
context: {
790+
httpContext: new HttpContext().set(contextToken2, 'foo'),
791+
},
792+
};
793+
794+
const op2 = {
795+
query: gql`
796+
query heroes {
797+
heroes {
798+
name
799+
}
800+
}
801+
`,
802+
context: {
803+
httpContext: new HttpContext().set(contextToken3, 'bar'),
804+
},
805+
};
806+
807+
execute(link, op1).subscribe(noop);
808+
execute(link, op2).subscribe(noop);
809+
810+
setTimeout(() => {
811+
httpBackend.match(() => {
812+
const callOptions = requestSpy.mock.calls[0][2];
813+
expect(callOptions?.context?.get(contextToken1)).toBe('options');
814+
expect(callOptions?.context?.get(contextToken2)).toBe('foo');
815+
expect(callOptions?.context?.get(contextToken3)).toBe('bar');
816+
done();
817+
return true;
818+
});
819+
}, 50);
820+
}));
762821
});

packages/apollo-angular/http/tests/http-link.spec.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { print, stripIgnoredCharacters } from 'graphql';
22
import { map, mergeMap } from 'rxjs/operators';
3-
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
4-
import { HttpHeaders, provideHttpClient } from '@angular/common/http';
3+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
4+
import {
5+
HttpClient,
6+
HttpContext,
7+
HttpContextToken,
8+
HttpHeaders,
9+
provideHttpClient,
10+
} from '@angular/common/http';
511
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
612
import { TestBed } from '@angular/core/testing';
713
import { ApolloLink, gql, InMemoryCache } from '@apollo/client';
@@ -750,4 +756,39 @@ describe('HttpLink', () => {
750756

751757
expect(httpBackend.expectOne('graphql').cancelled).toBe(true);
752758
});
759+
760+
test('should merge httpContext from options and query context and pass it on to HttpClient', () =>
761+
new Promise<void>(done => {
762+
const requestSpy = vi.spyOn(TestBed.inject(HttpClient), 'request');
763+
const optionsToken = new HttpContextToken(() => '');
764+
const queryToken = new HttpContextToken(() => '');
765+
766+
const optionsContext = new HttpContext().set(optionsToken, 'foo');
767+
const queryContext = new HttpContext().set(queryToken, 'bar');
768+
769+
const link = httpLink.create({ uri: 'graphql', httpContext: optionsContext });
770+
const op = {
771+
query: gql`
772+
query heroes {
773+
heroes {
774+
name
775+
}
776+
}
777+
`,
778+
context: {
779+
httpContext: queryContext,
780+
},
781+
};
782+
783+
execute(link, op).subscribe(() => {
784+
const callOptions = requestSpy.mock.calls[0][2];
785+
expect(callOptions?.context?.get(optionsToken)).toBe('foo');
786+
expect(callOptions?.context?.get(queryToken)).toBe('bar');
787+
expect(optionsContext.get(queryToken)).toBe('');
788+
expect(queryContext.get(optionsToken)).toBe('');
789+
done();
790+
});
791+
792+
httpBackend.expectOne('graphql').flush({ data: {} });
793+
}));
753794
});

0 commit comments

Comments
 (0)