Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mighty-buses-travel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'apollo-angular': minor
---

Support HttpContext in HttpLink option and operation context
22 changes: 20 additions & 2 deletions packages/apollo-angular/http/src/http-batch-link.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { print } from 'graphql';
import { Observable } from 'rxjs';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { HttpClient, HttpContext, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { ApolloLink } from '@apollo/client';
import { BatchLink } from '@apollo/client/link/batch';
import type { HttpLink } from './http-link';
import { Body, Context, OperationPrinter, Request } from './types';
import { createHeadersWithClientAwareness, fetch, mergeHeaders, prioritize } from './utils';
import {
createHeadersWithClientAwareness,
fetch,
mergeHeaders,
mergeHttpContext,
prioritize,
} from './utils';

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

if (typeof uri === 'function') {
Expand All @@ -74,6 +81,7 @@ export class HttpBatchLinkHandler extends ApolloLink {
options: {
withCredentials,
headers,
context,
},
};

Expand Down Expand Up @@ -162,6 +170,16 @@ export class HttpBatchLinkHandler extends ApolloLink {
);
}

private createHttpContext(operations: ApolloLink.Operation[]): HttpContext {
return operations.reduce(
(context: HttpContext, operation: ApolloLink.Operation) => {
const { httpContext } = operation.getContext();
return httpContext ? mergeHttpContext(httpContext, context) : context;
},
mergeHttpContext(this.options.httpContext, new HttpContext()),
);
}

private createBatchKey(operation: ApolloLink.Operation): string {
const context: Context & { skipBatching?: boolean } = operation.getContext();

Expand Down
9 changes: 7 additions & 2 deletions packages/apollo-angular/http/src/http-link.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { print } from 'graphql';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpContext } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { ApolloLink } from '@apollo/client';
import { pick } from './http-batch-link';
Expand All @@ -13,7 +13,7 @@ import {
OperationPrinter,
Request,
} from './types';
import { createHeadersWithClientAwareness, fetch, mergeHeaders } from './utils';
import { createHeadersWithClientAwareness, fetch, mergeHeaders, mergeHttpContext } from './utils';

export declare namespace HttpLink {
export interface Options extends FetchOptions, HttpRequestOptions {
Expand Down Expand Up @@ -49,6 +49,10 @@ export class HttpLinkHandler extends ApolloLink {
const withCredentials = pick(context, this.options, 'withCredentials');
const useMultipart = pick(context, this.options, 'useMultipart');
const useGETForQueries = this.options.useGETForQueries === true;
const httpContext = mergeHttpContext(
context.httpContext,
mergeHttpContext(this.options.httpContext, new HttpContext()),
);

const isQuery = operation.query.definitions.some(
def => def.kind === 'OperationDefinition' && def.operation === 'query',
Expand All @@ -69,6 +73,7 @@ export class HttpLinkHandler extends ApolloLink {
withCredentials,
useMultipart,
headers: this.options.headers,
context: httpContext,
},
};

Expand Down
9 changes: 7 additions & 2 deletions packages/apollo-angular/http/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DocumentNode } from 'graphql';
import { HttpHeaders } from '@angular/common/http';
import { HttpContext, HttpHeaders } from '@angular/common/http';
import { ApolloLink } from '@apollo/client';

declare module '@apollo/client' {
Expand All @@ -10,6 +10,11 @@ export type HttpRequestOptions = {
headers?: HttpHeaders;
withCredentials?: boolean;
useMultipart?: boolean;
httpContext?: HttpContext;
};

export type RequestOptions = Omit<HttpRequestOptions, 'httpContext'> & {
context?: HttpContext;
};

export type URIFunction = (operation: ApolloLink.Operation) => string;
Expand All @@ -36,7 +41,7 @@ export type Request = {
method: string;
url: string;
body: Body | Body[];
options: HttpRequestOptions;
options: RequestOptions;
};

export type ExtractedFiles = {
Expand Down
16 changes: 15 additions & 1 deletion packages/apollo-angular/http/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Observable } from 'rxjs';
import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http';
import { HttpClient, HttpContext, HttpHeaders, HttpResponse } from '@angular/common/http';
import { Body, ExtractedFiles, ExtractFiles, Request } from './types';

export const fetch = (
Expand Down Expand Up @@ -121,6 +121,20 @@ export const mergeHeaders = (
return destination || source;
};

export const mergeHttpContext = (
source: HttpContext | undefined,
destination: HttpContext,
): HttpContext => {
if (source && destination) {
return [...source.keys()].reduce(
(context, name) => context.set(name, source.get(name)),
destination,
);
}

return destination || source;
};

export function prioritize<T>(
...values: [NonNullable<T>, ...T[]] | [...T[], NonNullable<T>]
): NonNullable<T> {
Expand Down
63 changes: 61 additions & 2 deletions packages/apollo-angular/http/tests/http-batch-link.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { HttpHeaders, provideHttpClient } from '@angular/common/http';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import {
HttpClient,
HttpContext,
HttpContextToken,
HttpHeaders,
provideHttpClient,
} from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { ApolloLink, gql } from '@apollo/client';
Expand Down Expand Up @@ -759,4 +765,57 @@ describe('HttpBatchLink', () => {
done();
}, 50);
}));

test('should merge httpContext from options and batch context and pass it on to HttpClient', () =>
new Promise<void>(done => {
const requestSpy = vi.spyOn(TestBed.inject(HttpClient), 'request');
const contextToken1 = new HttpContextToken(() => '');
const contextToken2 = new HttpContextToken(() => '');
const contextToken3 = new HttpContextToken(() => '');
const link = httpLink.create({
uri: 'graphql',
httpContext: new HttpContext().set(contextToken1, 'options'),
batchKey: () => 'batchKey',
});

const op1 = {
query: gql`
query heroes {
heroes {
name
}
}
`,
context: {
httpContext: new HttpContext().set(contextToken2, 'foo'),
},
};

const op2 = {
query: gql`
query heroes {
heroes {
name
}
}
`,
context: {
httpContext: new HttpContext().set(contextToken3, 'bar'),
},
};

execute(link, op1).subscribe(noop);
execute(link, op2).subscribe(noop);

setTimeout(() => {
httpBackend.match(() => {
const callOptions = requestSpy.mock.calls[0][2];
expect(callOptions?.context?.get(contextToken1)).toBe('options');
expect(callOptions?.context?.get(contextToken2)).toBe('foo');
expect(callOptions?.context?.get(contextToken3)).toBe('bar');
done();
return true;
});
}, 50);
}));
});
45 changes: 43 additions & 2 deletions packages/apollo-angular/http/tests/http-link.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { print, stripIgnoredCharacters } from 'graphql';
import { map, mergeMap } from 'rxjs/operators';
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { HttpHeaders, provideHttpClient } from '@angular/common/http';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import {
HttpClient,
HttpContext,
HttpContextToken,
HttpHeaders,
provideHttpClient,
} from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { ApolloLink, gql, InMemoryCache } from '@apollo/client';
Expand Down Expand Up @@ -750,4 +756,39 @@ describe('HttpLink', () => {

expect(httpBackend.expectOne('graphql').cancelled).toBe(true);
});

test('should merge httpContext from options and query context and pass it on to HttpClient', () =>
new Promise<void>(done => {
const requestSpy = vi.spyOn(TestBed.inject(HttpClient), 'request');
const optionsToken = new HttpContextToken(() => '');
const queryToken = new HttpContextToken(() => '');

const optionsContext = new HttpContext().set(optionsToken, 'foo');
const queryContext = new HttpContext().set(queryToken, 'bar');

const link = httpLink.create({ uri: 'graphql', httpContext: optionsContext });
const op = {
query: gql`
query heroes {
heroes {
name
}
}
`,
context: {
httpContext: queryContext,
},
};

execute(link, op).subscribe(() => {
const callOptions = requestSpy.mock.calls[0][2];
expect(callOptions?.context?.get(optionsToken)).toBe('foo');
expect(callOptions?.context?.get(queryToken)).toBe('bar');
expect(optionsContext.get(queryToken)).toBe('');
expect(queryContext.get(optionsToken)).toBe('');
done();
});

httpBackend.expectOne('graphql').flush({ data: {} });
}));
});