diff --git a/.changeset/apollo-angular-2355-dependencies.md b/.changeset/apollo-angular-2355-dependencies.md new file mode 100644 index 000000000..0315df00a --- /dev/null +++ b/.changeset/apollo-angular-2355-dependencies.md @@ -0,0 +1,6 @@ +--- +"apollo-angular": patch +--- +dependencies updates: + - Updated dependency [`@apollo/client@^4.0.1` ↗︎](https://www.npmjs.com/package/@apollo/client/v/4.0.1) (from `^3.13.1`, in `peerDependencies`) + - Updated dependency [`rxjs@^7.3.0` ↗︎](https://www.npmjs.com/package/rxjs/v/7.3.0) (from `^6.0.0 || ^7.0.0`, in `peerDependencies`) diff --git a/.changeset/chatty-cherries-drum.md b/.changeset/chatty-cherries-drum.md new file mode 100644 index 000000000..6291b1455 --- /dev/null +++ b/.changeset/chatty-cherries-drum.md @@ -0,0 +1,56 @@ +--- +'apollo-angular': major +--- + +Namespaced types + +Before: + +```ts +import type { + Options, + BatchOptions +} from 'apollo-angular/http'; + +import type { + MutationOptionsAlone, + QueryOptionsAlone, + SubscriptionOptionsAlone, + WatchQueryOptions, + WatchQueryOptionsAlone, +} from 'apollo-angular'; + +type AllTypes = + | Options + | BatchOptions + | MutationOptionsAlone + | QueryOptionsAlone + | SubscriptionOptionsAlone + | WatchQueryOptions + | WatchQueryOptionsAlone; +``` + +After: + +```ts +import type { + HttpBatchLink, + HttpLink +} from 'apollo-angular/http'; + +import type { + Apollo, + Mutation, + Query, + Subscription, +} from 'apollo-angular'; + +type AllTypes = + | HttpLink.Options + | HttpBatchLink.Options + | Mutation.MutateOptions + | Query.FetchOptions + | Subscription.SubscribeOptions + | Apollo.WatchQueryOptions + | Query.WatchOptions; +``` diff --git a/.changeset/funny-trainers-look.md b/.changeset/funny-trainers-look.md new file mode 100644 index 000000000..e7f79476a --- /dev/null +++ b/.changeset/funny-trainers-look.md @@ -0,0 +1,12 @@ +--- +'apollo-angular': major +--- + +`httpHeaders` is a class + +Migrate your code like so: + +```diff +- const link = httpHeaders(); ++ const link = new HttpHeadersLink(); +``` diff --git a/.changeset/giant-clouds-shout.md b/.changeset/giant-clouds-shout.md new file mode 100644 index 000000000..e90d9021f --- /dev/null +++ b/.changeset/giant-clouds-shout.md @@ -0,0 +1,11 @@ +--- +'apollo-angular': major +--- + +Move `useZone` option into subscription options + + +```diff +- const obs = apollo.subscribe(options, { useZone: false }); ++ const obs = apollo.subscribe({ ...options, useZone: false }); +``` diff --git a/.changeset/tough-masks-search.md b/.changeset/tough-masks-search.md new file mode 100644 index 000000000..150e6bf9f --- /dev/null +++ b/.changeset/tough-masks-search.md @@ -0,0 +1,26 @@ +--- +'apollo-angular': major +--- + +Combined parameters of `Query`, `Mutation` and `Subscription` classes generated via codegen + +Migrate your code like so: + +```diff +class MyComponent { + myQuery = inject(MyQuery); + myMutation = inject(MyMutation); + mySubscription = inject(MySubscription); + + constructor() { +- myQuery.watch({ myVariable: 'foo' }, { fetchPolicy: 'cache-and-network' }); ++ myQuery.watch({ variables: { myVariable: 'foo' }, fetchPolicy: 'cache-and-network' }) + +- myMutation.mutate({ myVariable: 'foo' }, { errorPolicy: 'ignore' }); ++ myMutation.mutate({ variables: { myVariable: 'foo' }, errorPolicy: 'ignore' }); + +- mySubscription.subscribe({ myVariable: 'foo' }, { fetchPolicy: 'network-only' }); ++ mySubscription.subscribe({ variables: { myVariable: 'foo' }, fetchPolicy: 'network-only' }); + } +} +``` diff --git a/package.json b/package.json index 73d10872b..70da51699 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@angular/platform-browser-dynamic": "^18.0.0", "@angular/platform-server": "^18.0.0", "@angular/router": "^18.0.0", - "@apollo/client": "^3.13.1", + "@apollo/client": "4.0.1", "@babel/core": "^7.24.6", "@babel/preset-env": "^7.24.6", "@changesets/changelog-github": "^0.5.0", diff --git a/packages/apollo-angular/headers/src/index.ts b/packages/apollo-angular/headers/src/index.ts index f2ed5f233..c88b7d6d2 100644 --- a/packages/apollo-angular/headers/src/index.ts +++ b/packages/apollo-angular/headers/src/index.ts @@ -1,18 +1,17 @@ import { HttpHeaders } from '@angular/common/http'; -import { ApolloLink, NextLink, Operation } from '@apollo/client/core'; +import { ApolloLink } from '@apollo/client'; -export const httpHeaders = () => { - return new ApolloLink((operation: Operation, forward: NextLink) => { - const { getContext, setContext } = operation; - const context = getContext(); +export class HttpHeadersLink extends ApolloLink { + constructor() { + super((operation, forward) => { + const { getContext, setContext } = operation; + const context = getContext(); - if (context.headers) { - setContext({ - ...context, - headers: new HttpHeaders(context.headers), - }); - } + if (context.headers) { + setContext({ headers: new HttpHeaders(context.headers) }); + } - return forward(operation); - }); -}; + return forward(operation); + }); + } +} diff --git a/packages/apollo-angular/headers/tests/index.spec.ts b/packages/apollo-angular/headers/tests/index.spec.ts index 759f93591..61bd89308 100644 --- a/packages/apollo-angular/headers/tests/index.spec.ts +++ b/packages/apollo-angular/headers/tests/index.spec.ts @@ -1,7 +1,8 @@ +import { of } from 'rxjs'; import { describe, expect, test } from 'vitest'; import { HttpHeaders } from '@angular/common/http'; -import { ApolloLink, execute, gql, Observable as LinkObservable } from '@apollo/client/core'; -import { httpHeaders } from '../src'; +import { ApolloClient, ApolloLink, execute, gql, InMemoryCache } from '@apollo/client'; +import { HttpHeadersLink } from '../src'; const query = gql` query heroes { @@ -13,10 +14,12 @@ const query = gql` `; const data = { heroes: [{ name: 'Foo', __typename: 'Hero' }] }; -describe('httpHeaders', () => { +const dummyClient = new ApolloClient({ cache: new InMemoryCache(), link: ApolloLink.empty() }); + +describe('HttpHeadersLink', () => { test('should turn object into HttpHeaders', () => new Promise(done => { - const headersLink = httpHeaders(); + const headersLink = new HttpHeadersLink(); const mockLink = new ApolloLink(operation => { const { headers } = operation.getContext(); @@ -24,19 +27,23 @@ describe('httpHeaders', () => { expect(headers instanceof HttpHeaders).toBe(true); expect(headers.get('Authorization')).toBe('Bearer Foo'); - return LinkObservable.of({ data }); + return of({ data }); }); const link = headersLink.concat(mockLink); - execute(link, { - query, - context: { - headers: { - Authorization: 'Bearer Foo', + execute( + link, + { + query, + context: { + headers: { + Authorization: 'Bearer Foo', + }, }, }, - }).subscribe(result => { + { client: dummyClient }, + ).subscribe(result => { expect(result.data).toEqual(data); done(); }); @@ -44,21 +51,19 @@ describe('httpHeaders', () => { test('should not set headers when not defined', () => new Promise(done => { - const headersLink = httpHeaders(); + const headersLink = new HttpHeadersLink(); const mockLink = new ApolloLink(operation => { const { headers } = operation.getContext(); expect(headers).toBeUndefined(); - return LinkObservable.of({ data }); + return of({ data }); }); const link = headersLink.concat(mockLink); - execute(link, { - query, - }).subscribe(result => { + execute(link, { query }, { client: dummyClient }).subscribe(result => { expect(result.data).toEqual(data); done(); }); diff --git a/packages/apollo-angular/http/src/http-batch-link.ts b/packages/apollo-angular/http/src/http-batch-link.ts index 93a70e434..be63af9dc 100644 --- a/packages/apollo-angular/http/src/http-batch-link.ts +++ b/packages/apollo-angular/http/src/http-batch-link.ts @@ -1,16 +1,21 @@ import { print } from 'graphql'; +import { Observable } from 'rxjs'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { - ApolloLink, - FetchResult, - Observable as LinkObservable, - Operation, -} from '@apollo/client/core'; -import { BatchHandler, BatchLink } from '@apollo/client/link/batch'; -import { BatchOptions, Body, Context, OperationPrinter, Options, Request } from './types'; +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'; +export declare namespace HttpBatchLink { + export type Options = { + batchMax?: number; + batchInterval?: number; + batchKey?: (operation: ApolloLink.Operation) => string; + } & HttpLink.Options; +} + export const defaults = { batchInterval: 10, batchMax: 10, @@ -27,9 +32,9 @@ export const defaults = { */ export function pick>( context: Context, - options: Options, + options: HttpBatchLink.Options, key: K, -): ReturnType> { +): ReturnType> { return prioritize(context[key], options[key], defaults[key]); } @@ -41,7 +46,7 @@ export class HttpBatchLinkHandler extends ApolloLink { constructor( private readonly httpClient: HttpClient, - private readonly options: BatchOptions, + private readonly options: HttpBatchLink.Options, ) { super(); @@ -52,8 +57,8 @@ export class HttpBatchLinkHandler extends ApolloLink { this.print = this.options.operationPrinter; } - const batchHandler: BatchHandler = (operations: Operation[]) => { - return new LinkObservable((observer: any) => { + const batchHandler: BatchLink.BatchHandler = (operations: ApolloLink.Operation[]) => { + return new Observable((observer: any) => { const body = this.createBody(operations); const headers = this.createHeaders(operations); const { method, uri, withCredentials } = this.createOptions(operations); @@ -90,7 +95,7 @@ export class HttpBatchLinkHandler extends ApolloLink { const batchKey = options.batchKey || - ((operation: Operation) => { + ((operation: ApolloLink.Operation) => { return this.createBatchKey(operation); }); @@ -103,8 +108,8 @@ export class HttpBatchLinkHandler extends ApolloLink { } private createOptions( - operations: Operation[], - ): Required> { + operations: ApolloLink.Operation[], + ): Required> { const context: Context = operations[0].getContext(); return { @@ -114,7 +119,7 @@ export class HttpBatchLinkHandler extends ApolloLink { }; } - private createBody(operations: Operation[]): Body[] { + private createBody(operations: ApolloLink.Operation[]): Body[] { return operations.map(operation => { const includeExtensions = prioritize( operation.getContext().includeExtensions, @@ -144,10 +149,11 @@ export class HttpBatchLinkHandler extends ApolloLink { }); } - private createHeaders(operations: Operation[]): HttpHeaders { + private createHeaders(operations: ApolloLink.Operation[]): HttpHeaders { return operations.reduce( - (headers: HttpHeaders, operation: Operation) => { - return mergeHeaders(headers, operation.getContext().headers); + (headers: HttpHeaders, operation: ApolloLink.Operation) => { + const { headers: contextHeaders } = operation.getContext(); + return contextHeaders ? mergeHeaders(headers, contextHeaders) : headers; }, createHeadersWithClientAwareness({ headers: this.options.headers, @@ -156,7 +162,7 @@ export class HttpBatchLinkHandler extends ApolloLink { ); } - private createBatchKey(operation: Operation): string { + private createBatchKey(operation: ApolloLink.Operation): string { const context: Context & { skipBatching?: boolean } = operation.getContext(); if (context.skipBatching) { @@ -175,8 +181,11 @@ export class HttpBatchLinkHandler extends ApolloLink { return prioritize(context.uri, this.options.uri, '') + opts; } - public request(op: Operation): LinkObservable | null { - return this.batcher.request(op); + public request( + op: ApolloLink.Operation, + forward: ApolloLink.ForwardFunction, + ): Observable { + return this.batcher.request(op, forward); } } @@ -186,7 +195,7 @@ export class HttpBatchLinkHandler extends ApolloLink { export class HttpBatchLink { constructor(private readonly httpClient: HttpClient) {} - public create(options: BatchOptions): HttpBatchLinkHandler { + public create(options: HttpBatchLink.Options): HttpBatchLinkHandler { return new HttpBatchLinkHandler(this.httpClient, options); } } diff --git a/packages/apollo-angular/http/src/http-link.ts b/packages/apollo-angular/http/src/http-link.ts index 7d40d281f..4528589a5 100644 --- a/packages/apollo-angular/http/src/http-link.ts +++ b/packages/apollo-angular/http/src/http-link.ts @@ -1,24 +1,36 @@ import { print } from 'graphql'; +import { Observable } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { - ApolloLink, - FetchResult, - Observable as LinkObservable, - Operation, -} from '@apollo/client/core'; +import { ApolloLink } from '@apollo/client'; import { pick } from './http-batch-link'; -import { Body, Context, OperationPrinter, Options, Request } from './types'; +import { + Body, + Context, + ExtractFiles, + FetchOptions, + HttpRequestOptions, + OperationPrinter, + Request, +} from './types'; import { createHeadersWithClientAwareness, fetch, mergeHeaders } from './utils'; +export declare namespace HttpLink { + export interface Options extends FetchOptions, HttpRequestOptions { + operationPrinter?: OperationPrinter; + useGETForQueries?: boolean; + extractFiles?: ExtractFiles; + } +} + // XXX find a better name for it export class HttpLinkHandler extends ApolloLink { - public requester: (operation: Operation) => LinkObservable | null; + public requester: (operation: ApolloLink.Operation) => Observable; private print: OperationPrinter = print; constructor( private readonly httpClient: HttpClient, - private readonly options: Options, + private readonly options: HttpLink.Options, ) { super(); @@ -26,8 +38,8 @@ export class HttpLinkHandler extends ApolloLink { this.print = this.options.operationPrinter; } - this.requester = (operation: Operation) => - new LinkObservable((observer: any) => { + this.requester = (operation: ApolloLink.Operation) => + new Observable((observer: any) => { const context: Context = operation.getContext(); let method = pick(context, this.options, 'method'); @@ -89,7 +101,7 @@ export class HttpLinkHandler extends ApolloLink { }); } - public request(op: Operation): LinkObservable | null { + public request(op: ApolloLink.Operation): Observable { return this.requester(op); } } @@ -100,7 +112,7 @@ export class HttpLinkHandler extends ApolloLink { export class HttpLink { constructor(private readonly httpClient: HttpClient) {} - public create(options: Options): HttpLinkHandler { + public create(options: HttpLink.Options): HttpLinkHandler { return new HttpLinkHandler(this.httpClient, options); } } diff --git a/packages/apollo-angular/http/src/index.ts b/packages/apollo-angular/http/src/index.ts index 7791a9da5..4c05cd559 100644 --- a/packages/apollo-angular/http/src/index.ts +++ b/packages/apollo-angular/http/src/index.ts @@ -2,5 +2,3 @@ export { HttpLink, HttpLinkHandler } from './http-link'; // http-batch export { HttpBatchLink, HttpBatchLinkHandler } from './http-batch-link'; -// common -export { BatchOptions, Options } from './types'; diff --git a/packages/apollo-angular/http/src/types.ts b/packages/apollo-angular/http/src/types.ts index 58ddf3612..b6802d3cc 100644 --- a/packages/apollo-angular/http/src/types.ts +++ b/packages/apollo-angular/http/src/types.ts @@ -1,6 +1,10 @@ import { DocumentNode } from 'graphql'; import { HttpHeaders } from '@angular/common/http'; -import { Operation } from '@apollo/client/core'; +import { ApolloLink } from '@apollo/client'; + +declare module '@apollo/client' { + export interface DefaultContext extends Context {} +} export type HttpRequestOptions = { headers?: HttpHeaders; @@ -8,7 +12,7 @@ export type HttpRequestOptions = { useMultipart?: boolean; }; -export type URIFunction = (operation: Operation) => string; +export type URIFunction = (operation: ApolloLink.Operation) => string; export type FetchOptions = { method?: string; @@ -19,12 +23,6 @@ export type FetchOptions = { export type OperationPrinter = (operation: DocumentNode) => string; -export interface Options extends FetchOptions, HttpRequestOptions { - operationPrinter?: OperationPrinter; - useGETForQueries?: boolean; - extractFiles?: ExtractFiles; -} - export type Body = { query?: string; variables?: Record; @@ -47,9 +45,3 @@ export type ExtractedFiles = { }; export type ExtractFiles = (body: Body | Body[]) => ExtractedFiles; - -export type BatchOptions = { - batchMax?: number; - batchInterval?: number; - batchKey?: (operation: Operation) => string; -} & Options; diff --git a/packages/apollo-angular/http/tests/http-batch-link.spec.ts b/packages/apollo-angular/http/tests/http-batch-link.spec.ts index 2e348da8e..be91a77d1 100644 --- a/packages/apollo-angular/http/tests/http-batch-link.spec.ts +++ b/packages/apollo-angular/http/tests/http-batch-link.spec.ts @@ -2,8 +2,10 @@ import { afterEach, beforeEach, describe, expect, test } from 'vitest'; import { HttpHeaders, provideHttpClient } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { TestBed } from '@angular/core/testing'; -import { ApolloLink, execute, gql, Operation } from '@apollo/client/core'; +import { ApolloLink, gql } from '@apollo/client'; +import { getOperationName } from '@apollo/client/utilities/internal'; import { HttpBatchLink } from '../src/http-batch-link'; +import { executeWithDefaultContext as execute } from './utils'; const noop = () => { // @@ -36,7 +38,6 @@ describe('HttpBatchLink', () => { } } `, - operationName: 'heroes', variables: {}, }; const data = { @@ -71,7 +72,6 @@ describe('HttpBatchLink', () => { } } `, - operationName: 'heroes', variables: {}, }; @@ -96,7 +96,6 @@ describe('HttpBatchLink', () => { } } `, - operationName: 'heroes-1', variables: {}, }; const op2 = { @@ -107,7 +106,6 @@ describe('HttpBatchLink', () => { } } `, - operationName: 'heroes-2', variables: {}, }; @@ -116,8 +114,8 @@ describe('HttpBatchLink', () => { setTimeout(() => { httpBackend.match(req => { - expect(req.body[0].operationName).toEqual(op1.operationName); - expect(req.body[1].operationName).toEqual(op2.operationName); + expect(req.body[0].operationName).toEqual(getOperationName(op1.query)); + expect(req.body[1].operationName).toEqual(getOperationName(op2.query)); done(); return true; }); @@ -135,7 +133,6 @@ describe('HttpBatchLink', () => { } } `, - operationName: 'heroes', variables: {}, }; @@ -143,7 +140,7 @@ describe('HttpBatchLink', () => { setTimeout(() => { httpBackend.match(req => { - expect(req.body[0].operationName).toEqual(op.operationName); + expect(req.body[0].operationName).toEqual(getOperationName(op.query)); expect(req.reportProgress).toEqual(false); expect(req.responseType).toEqual('json'); expect(req.detectContentTypeHeader()).toEqual('application/json'); @@ -164,7 +161,6 @@ describe('HttpBatchLink', () => { } } `, - operationName: 'heroes', variables: {}, }; @@ -173,7 +169,7 @@ describe('HttpBatchLink', () => { setTimeout(() => { httpBackend.match(req => { expect(req.method).toEqual('POST'); - expect(req.body[0].operationName).toEqual(op.operationName); + expect(req.body[0].operationName).toEqual(getOperationName(op.query)); expect(req.detectContentTypeHeader()).toEqual('application/json'); done(); return true; @@ -196,7 +192,6 @@ describe('HttpBatchLink', () => { } } `, - operationName: 'heroes', variables: { up: 'dog' }, extensions: { what: 'what' }, }; @@ -525,7 +520,7 @@ describe('HttpBatchLink', () => { execute(link, { query: gql` - query heroes($first: Int!) { + query op1($first: Int!) { heroes(first: $first) { name } @@ -534,17 +529,15 @@ describe('HttpBatchLink', () => { variables: { first: 5, }, - operationName: 'op1', }).subscribe(noop); execute(link, { query: gql` - query heroes { + query op2 { heroes { name } } `, - operationName: 'op2', }).subscribe(noop); setTimeout(() => { @@ -571,7 +564,8 @@ describe('HttpBatchLink', () => { new Promise(done => { const link = httpLink.create({ uri: 'graphql', - batchKey: (operation: Operation) => operation.getContext().uri || 'graphql', + batchKey: (operation: ApolloLink.Operation) => + (operation.getContext().uri as string) || 'graphql', }); execute(link, { @@ -582,7 +576,6 @@ describe('HttpBatchLink', () => { } } `, - operationName: 'op1', }).subscribe(noop); execute(link, { @@ -593,7 +586,6 @@ describe('HttpBatchLink', () => { } } `, - operationName: 'op2', context: { uri: 'gql', }, @@ -639,7 +631,6 @@ describe('HttpBatchLink', () => { } } `, - operationName: 'op1', }).subscribe(noop); execute(link, { @@ -650,7 +641,6 @@ describe('HttpBatchLink', () => { } } `, - operationName: 'op2', context: { uri: 'gql', }, @@ -698,7 +688,6 @@ describe('HttpBatchLink', () => { } } `, - operationName: 'op1', }).subscribe(noop); execute(link, { @@ -709,7 +698,6 @@ describe('HttpBatchLink', () => { } } `, - operationName: 'op2', context: { skipBatching: true, }, @@ -752,7 +740,6 @@ describe('HttpBatchLink', () => { } } `, - operationName: 'heroes', variables: {}, }; diff --git a/packages/apollo-angular/http/tests/http-link.spec.ts b/packages/apollo-angular/http/tests/http-link.spec.ts index c0d84ac3c..36516b20e 100644 --- a/packages/apollo-angular/http/tests/http-link.spec.ts +++ b/packages/apollo-angular/http/tests/http-link.spec.ts @@ -1,12 +1,13 @@ import { print, stripIgnoredCharacters } from 'graphql'; -import { mergeMap } from 'rxjs/operators'; +import { map, mergeMap } from 'rxjs/operators'; import { afterEach, beforeEach, describe, expect, test } from 'vitest'; import { HttpHeaders, provideHttpClient } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { TestBed } from '@angular/core/testing'; -import { ApolloLink, execute, gql, InMemoryCache } from '@apollo/client/core'; +import { ApolloLink, gql, InMemoryCache } from '@apollo/client'; import { Apollo } from '../../src'; import { HttpLink } from '../src/http-link'; +import { executeWithDefaultContext as execute } from './utils'; const noop = () => { // @@ -512,14 +513,16 @@ describe('HttpLink', () => { test('should set response in context', () => new Promise(done => { const afterware = new ApolloLink((op, forward) => { - return forward(op).map(response => { - const context = op.getContext(); + return forward(op).pipe( + map(response => { + const context = op.getContext(); - expect(context.response).toBeDefined(); - done(); + expect(context.response).toBeDefined(); + done(); - return response; - }); + return response; + }), + ); }); const link = afterware.concat( httpLink.create({ diff --git a/packages/apollo-angular/http/tests/ssr.spec.ts b/packages/apollo-angular/http/tests/ssr.spec.ts index 13b43b539..6f6e4cf30 100644 --- a/packages/apollo-angular/http/tests/ssr.spec.ts +++ b/packages/apollo-angular/http/tests/ssr.spec.ts @@ -10,8 +10,9 @@ import { renderModule, ServerModule, } from '@angular/platform-server'; -import { execute, gql } from '@apollo/client/core'; +import { gql } from '@apollo/client'; import { HttpLink } from '../src/http-link'; +import { executeWithDefaultContext as execute } from './utils'; describe.skip('integration', () => { let doc: string; diff --git a/packages/apollo-angular/http/tests/utils.ts b/packages/apollo-angular/http/tests/utils.ts new file mode 100644 index 000000000..177efa97c --- /dev/null +++ b/packages/apollo-angular/http/tests/utils.ts @@ -0,0 +1,19 @@ +import { Observable } from 'rxjs'; +import { ApolloClient, ApolloLink, execute, InMemoryCache } from '@apollo/client'; + +export function createDefaultExecuteContext(): ApolloLink.ExecuteContext { + return { + client: new ApolloClient({ + cache: new InMemoryCache(), + link: ApolloLink.empty(), + }), + }; +} + +export function executeWithDefaultContext( + link: ApolloLink, + request: ApolloLink.Request, + context: ApolloLink.ExecuteContext = createDefaultExecuteContext(), +): Observable { + return execute(link, request, context); +} diff --git a/packages/apollo-angular/package.json b/packages/apollo-angular/package.json index c9c03fca5..3adbf0c8c 100644 --- a/packages/apollo-angular/package.json +++ b/packages/apollo-angular/package.json @@ -39,9 +39,9 @@ }, "peerDependencies": { "@angular/core": "^18.0.0 || ^19.0.0 || ^20.0.0", - "@apollo/client": "^3.13.1", + "@apollo/client": "^4.0.1", "graphql": "^16.0.0", - "rxjs": "^6.0.0 || ^7.0.0" + "rxjs": "^7.3.0" }, "dependencies": { "tslib": "^2.6.2" diff --git a/packages/apollo-angular/persisted-queries/src/index.ts b/packages/apollo-angular/persisted-queries/src/index.ts index f2ec7ce46..f0c0be207 100644 --- a/packages/apollo-angular/persisted-queries/src/index.ts +++ b/packages/apollo-angular/persisted-queries/src/index.ts @@ -1,10 +1,10 @@ -import { setContext } from '@apollo/client/link/context'; -import { ApolloLink } from '@apollo/client/link/core'; -import { createPersistedQueryLink as _createPersistedQueryLink } from '@apollo/client/link/persisted-queries'; +import { ApolloLink } from '@apollo/client/link'; +import { SetContextLink } from '@apollo/client/link/context'; +import { PersistedQueryLink } from '@apollo/client/link/persisted-queries'; -export type Options = Parameters[0]; +export type Options = PersistedQueryLink.Options; -const transformLink = setContext((_, context) => { +const transformLink = new SetContextLink(context => { const ctx: any = {}; if (context.http) { @@ -19,5 +19,5 @@ const transformLink = setContext((_, context) => { return ctx; }); -export const createPersistedQueryLink = (options: Options) => - ApolloLink.from([_createPersistedQueryLink(options), transformLink as any]); +export const createPersistedQueryLink = (options: PersistedQueryLink.Options) => + ApolloLink.from([new PersistedQueryLink(options), transformLink]); diff --git a/packages/apollo-angular/persisted-queries/tests/persisted-queries.spec.ts b/packages/apollo-angular/persisted-queries/tests/persisted-queries.spec.ts index 16f662f44..05b3a1dec 100644 --- a/packages/apollo-angular/persisted-queries/tests/persisted-queries.spec.ts +++ b/packages/apollo-angular/persisted-queries/tests/persisted-queries.spec.ts @@ -1,5 +1,12 @@ +import { Observable } from 'rxjs'; import { describe, expect, test, vi } from 'vitest'; -import { ApolloLink, execute, FetchResult, gql, Observable, Operation } from '@apollo/client/core'; +import { + ApolloClient, + ApolloLink, + execute as executeLink, + gql, + InMemoryCache, +} from '@apollo/client'; import { createPersistedQueryLink } from '../src'; const query = gql` @@ -12,6 +19,12 @@ const query = gql` `; const data = { heroes: [{ name: 'Foo', __typename: 'Hero' }] }; +function execute(link: ApolloLink, request: ApolloLink.Request) { + return executeLink(link, request, { + client: new ApolloClient({ cache: new InMemoryCache(), link: ApolloLink.empty() }), + }); +} + class MockLink extends ApolloLink { public showNotFound: boolean = true; @@ -23,8 +36,8 @@ class MockLink extends ApolloLink { : data; } - public request(operation: Operation) { - return new Observable(observer => { + public request(operation: ApolloLink.Operation) { + return new Observable(observer => { const request: any = {}; if (operation.getContext().includeQuery) { @@ -54,7 +67,7 @@ describe('createPersistedQueryLink', () => { query, }).subscribe(() => { const firstReq = spyRequester.calls[0][0] as any; - const secondOp = spyRequest.calls[1][0] as Operation; + const secondOp = spyRequest.calls[1][0] as ApolloLink.Operation; const secondReq = spyRequester.calls[1][0] as any; const secondContext = secondOp.getContext(); @@ -86,7 +99,7 @@ describe('createPersistedQueryLink', () => { execute(link, { query, }).subscribe(() => { - const op = spyRequest.calls[1][0] as Operation; + const op = spyRequest.calls[1][0] as ApolloLink.Operation; const ctx = op.getContext(); // should be compatible with apollo-angular-link-http diff --git a/packages/apollo-angular/schematics/install/files/module/graphql.module.ts b/packages/apollo-angular/schematics/install/files/module/graphql.module.ts index f1eea19fd..7bc609bda 100644 --- a/packages/apollo-angular/schematics/install/files/module/graphql.module.ts +++ b/packages/apollo-angular/schematics/install/files/module/graphql.module.ts @@ -1,9 +1,9 @@ import { provideApollo } from 'apollo-angular'; import { HttpLink } from 'apollo-angular/http'; import { inject, NgModule } from '@angular/core'; -import { ApolloClientOptions, InMemoryCache } from '@apollo/client/core'; +import { ApolloClient, InMemoryCache } from '@apollo/client'; -export function createApollo(): ApolloClientOptions { +export function createApollo(): ApolloClient.Options { const uri = '<%= endpoint %>'; // <-- add the URL of the GraphQL server here const httpLink = inject(HttpLink); diff --git a/packages/apollo-angular/schematics/install/index.cts b/packages/apollo-angular/schematics/install/index.cts index 0894c4c21..e1e54457a 100644 --- a/packages/apollo-angular/schematics/install/index.cts +++ b/packages/apollo-angular/schematics/install/index.cts @@ -1,6 +1,4 @@ import { dirname } from 'path'; -import { CompilerOptions } from 'typescript'; -import { tags } from '@angular-devkit/core'; import { apply, chain, @@ -23,8 +21,6 @@ import { Schema } from './schema.cjs'; export function factory(options: Schema): Rule { return chain([ addDependencies(options), - includeAsyncIterableLib(), - allowSyntheticDefaultImports(), addSetupFiles(options), importHttpClient(options), importSetup(options), @@ -33,8 +29,8 @@ export function factory(options: Schema): Rule { export function createDependenciesMap(options: Schema): Record { return { - 'apollo-angular': '^7.0.0', - '@apollo/client': '^3.0.0', + 'apollo-angular': '^9.0.0', + '@apollo/client': '^4.0.1', graphql: `^${options.graphql ?? '16.0.0'}`, }; } @@ -71,96 +67,6 @@ function addDependencies(options: Schema): Rule { }; } -function includeAsyncIterableLib(): Rule { - const requiredLib = 'esnext.asynciterable'; - - function updateFn(tsconfig: any): boolean { - const compilerOptions: CompilerOptions = tsconfig.compilerOptions; - - if ( - compilerOptions && - compilerOptions.lib && - !compilerOptions.lib.find(lib => lib.toLowerCase() === requiredLib) - ) { - compilerOptions.lib.push(requiredLib); - return true; - } - - return false; - } - - return (host: Tree) => { - if ( - !updateTSConfig('tsconfig.json', host, updateFn) && - !updateTSConfig('tsconfig.base.json', host, updateFn) - ) { - console.error( - '\n' + - tags.stripIndent` - We couldn't find '${requiredLib}' in the list of library files to be included in the compilation. - It's required by '@apollo/client/core' package so please add it to your tsconfig. - ` + - '\n', - ); - } - - return host; - }; -} - -function updateTSConfig( - tsconfigPath: string, - host: Tree, - updateFn: (tsconfig: any) => boolean, -): boolean { - try { - const tsconfig = getJsonFile(host, tsconfigPath); - - if (updateFn(tsconfig)) { - host.overwrite(tsconfigPath, JSON.stringify(tsconfig, null, 2)); - - return true; - } - } catch (error) { - // - } - - return false; -} - -function allowSyntheticDefaultImports(): Rule { - function updateFn(tsconfig: any): boolean { - if ( - tsconfig?.compilerOptions && - tsconfig?.compilerOptions?.lib && - !tsconfig.compilerOptions.allowSyntheticDefaultImports - ) { - tsconfig.compilerOptions.allowSyntheticDefaultImports = true; - return true; - } - - return false; - } - - return (host: Tree) => { - if ( - !updateTSConfig('tsconfig.json', host, updateFn) && - !updateTSConfig('tsconfig.base.json', host, updateFn) - ) { - console.error( - '\n' + - tags.stripIndent` - We couldn't enable 'allowSyntheticDefaultImports' flag. - It's required by '@apollo/client/core' package so please add it to your tsconfig. - ` + - '\n', - ); - } - - return host; - }; -} - function addSetupFiles(options: Schema): Rule { return async (host: Tree) => { const mainPath = await getMainFilePath(host, options.project); @@ -201,12 +107,17 @@ function importSetup(options: Schema): Rule { link: httpLink.create({ uri: '<%= endpoint %>', }), - cache: new ${external('InMemoryCache', '@apollo/client/core')}(), + cache: new ${external('InMemoryCache', '@apollo/client')}(), }; })`; }); } else { - await addModuleImportToRootModule(host, 'GraphQLModule', './graphql.module', options.project); + return addModuleImportToRootModule( + host, + 'GraphQLModule', + './graphql.module', + options.project, + ); } }; } @@ -219,7 +130,7 @@ function importHttpClient(options: Schema): Rule { return code`${external('provideHttpClient', '@angular/common/http')}()`; }); } else { - await addModuleImportToRootModule( + return addModuleImportToRootModule( host, 'HttpClientModule', '@angular/common/http', diff --git a/packages/apollo-angular/schematics/tests/ng-add.spec.cts b/packages/apollo-angular/schematics/tests/ng-add.spec.cts index a6753b2ce..c03d39a67 100644 --- a/packages/apollo-angular/schematics/tests/ng-add.spec.cts +++ b/packages/apollo-angular/schematics/tests/ng-add.spec.cts @@ -1,5 +1,3 @@ - -import {CompilerOptions} from 'typescript'; import {UnitTestTree} from '@angular-devkit/schematics/testing'; import {createDependenciesMap} from '../install/index.cjs'; import {getFileContent, getJsonFile, runNgAdd} from '../utils/index.cjs'; @@ -50,13 +48,6 @@ describe('ng-add with module', () => { expect(content).toMatch(/import { HttpClientModule } from '@angular\/common\/http'/); }); - - it('should add esnext.asynciterable to tsconfig.json', async () => { - const config = getJsonFile(tree, '/tsconfig.json'); - const compilerOptions: CompilerOptions = config.compilerOptions; - - expect(compilerOptions.lib).toContain('esnext.asynciterable'); - }); }); describe('ng-add with standalone', () => { @@ -98,11 +89,4 @@ describe('ng-add with standalone', () => { expect(content).toMatch(/import { provideHttpClient } from '@angular\/common\/http'/); }); - - it('should add esnext.asynciterable to tsconfig.json', async () => { - const config = getJsonFile(tree, '/tsconfig.json'); - const compilerOptions: CompilerOptions = config.compilerOptions; - - expect(compilerOptions.lib).toContain('esnext.asynciterable'); - }); }); diff --git a/packages/apollo-angular/schematics/tsconfig.test.json b/packages/apollo-angular/schematics/tsconfig.test.json index f905753d8..914541345 100644 --- a/packages/apollo-angular/schematics/tsconfig.test.json +++ b/packages/apollo-angular/schematics/tsconfig.test.json @@ -1,8 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "emitDecoratorMetadata": true, - "allowSyntheticDefaultImports": true + "emitDecoratorMetadata": true }, "include": ["tests/*.spec.cts"] } diff --git a/packages/apollo-angular/src/apollo-module.ts b/packages/apollo-angular/src/apollo-module.ts index 889c48ccc..49fcb62c8 100644 --- a/packages/apollo-angular/src/apollo-module.ts +++ b/packages/apollo-angular/src/apollo-module.ts @@ -1,11 +1,11 @@ import { Provider } from '@angular/core'; -import { ApolloClientOptions } from '@apollo/client/core'; +import { ApolloClient } from '@apollo/client'; import { Apollo } from './apollo'; import { APOLLO_FLAGS, APOLLO_NAMED_OPTIONS, APOLLO_OPTIONS } from './tokens'; import { Flags, NamedOptions } from './types'; -export function provideApollo( - optionsFactory: () => ApolloClientOptions, +export function provideApollo( + optionsFactory: () => ApolloClient.Options, flags: Flags = {}, ): Provider { return [ diff --git a/packages/apollo-angular/src/apollo.ts b/packages/apollo-angular/src/apollo.ts index 63b23bcac..da82f2863 100644 --- a/packages/apollo-angular/src/apollo.ts +++ b/packages/apollo-angular/src/apollo.ts @@ -1,69 +1,93 @@ -import { from, Observable } from 'rxjs'; +import { Observable } from 'rxjs'; import { Inject, Injectable, NgZone, Optional } from '@angular/core'; -import type { - ApolloClientOptions, - ApolloQueryResult, - FetchResult, - ObservableQuery, - OperationVariables, - QueryOptions, - SubscriptionOptions, - WatchFragmentResult, -} from '@apollo/client/core'; -import { ApolloClient } from '@apollo/client/core'; +import type { OperationVariables } from '@apollo/client'; +import { ApolloClient } from '@apollo/client'; import { QueryRef } from './query-ref'; import { APOLLO_FLAGS, APOLLO_NAMED_OPTIONS, APOLLO_OPTIONS } from './tokens'; -import type { - EmptyObject, - ExtraSubscriptionOptions, - Flags, - MutationOptions, - MutationResult, - NamedOptions, - WatchFragmentOptions, - WatchQueryOptions, -} from './types'; -import { fixObservable, fromPromise, useMutationLoading, wrapWithZone } from './utils'; - -export class ApolloBase { - private useInitialLoading: boolean; +import type { EmptyObject, Flags, NamedOptions } from './types'; +import { fromLazyPromise, useMutationLoading, wrapWithZone } from './utils'; + +export declare namespace Apollo { + export type WatchQueryOptions< + TData = unknown, + TVariables extends OperationVariables = EmptyObject, + > = ApolloClient.WatchQueryOptions; + + export type QueryOptions< + TData = unknown, + TVariables extends OperationVariables = EmptyObject, + > = ApolloClient.QueryOptions; + + export type QueryResult = ApolloClient.QueryResult; + + export type MutateOptions< + TData = unknown, + TVariables extends OperationVariables = EmptyObject, + > = ApolloClient.MutateOptions & { + /** + * Observable starts with `{ loading: true }`. + * + * Disabled by default + */ + useMutationLoading?: boolean; + }; + + export type MutateResult = ApolloClient.MutateResult & { + loading?: boolean; + }; + + export type SubscribeOptions< + TData = unknown, + TVariables extends OperationVariables = EmptyObject, + > = ApolloClient.SubscribeOptions & { + useZone?: boolean; + }; + + export type SubscribeResult = ApolloClient.SubscribeResult; + + export interface WatchFragmentOptions< + TData = unknown, + TVariables extends OperationVariables = EmptyObject, + > extends ApolloClient.WatchFragmentOptions { + useZone?: boolean; + } + + export type WatchFragmentResult = ApolloClient.WatchFragmentResult; +} + +export class ApolloBase { private useMutationLoading: boolean; constructor( protected readonly ngZone: NgZone, protected readonly flags?: Flags, - protected _client?: ApolloClient, + protected _client?: ApolloClient, ) { - this.useInitialLoading = flags?.useInitialLoading ?? false; this.useMutationLoading = flags?.useMutationLoading ?? false; } public watchQuery( - options: WatchQueryOptions, + options: Apollo.WatchQueryOptions, ): QueryRef { return new QueryRef( - this.ensureClient().watchQuery({ - ...options, - }) as ObservableQuery, + this.ensureClient().watchQuery({ ...options }), this.ngZone, - { - useInitialLoading: this.useInitialLoading, - ...options, - }, ); } - public query( - options: QueryOptions, - ): Observable> { - return fromPromise>(() => this.ensureClient().query({ ...options })); + public query( + options: Apollo.QueryOptions, + ): Observable> { + return fromLazyPromise>(() => + this.ensureClient().query({ ...options }), + ); } - public mutate( - options: MutationOptions, - ): Observable> { + public mutate( + options: Apollo.MutateOptions, + ): Observable> { return useMutationLoading( - fromPromise(() => this.ensureClient().mutate({ ...options })), + fromLazyPromise(() => this.ensureClient().mutate({ ...options })), options.useMutationLoading ?? this.useMutationLoading, ); } @@ -72,29 +96,29 @@ export class ApolloBase { TFragmentData = unknown, TVariables extends OperationVariables = EmptyObject, >( - options: WatchFragmentOptions, - extra?: ExtraSubscriptionOptions, - ): Observable> { - const obs = from( - fixObservable(this.ensureClient().watchFragment({ ...options })), - ); + options: Apollo.WatchFragmentOptions, + ): Observable> { + const { useZone, ...opts } = options; + const obs = this.ensureClient().watchFragment({ ...opts }); - return extra && extra.useZone !== true ? obs : wrapWithZone(obs, this.ngZone); + return useZone !== true ? obs : wrapWithZone(obs, this.ngZone); } - public subscribe( - options: SubscriptionOptions, - extra?: ExtraSubscriptionOptions, - ): Observable> { - const obs = from(fixObservable(this.ensureClient().subscribe({ ...options }))); + public subscribe( + options: Apollo.SubscribeOptions, + ): Observable> { + const { useZone, ...opts } = options; + const obs = this.ensureClient().subscribe({ + ...opts, + } as ApolloClient.SubscribeOptions); - return extra && extra.useZone !== true ? obs : wrapWithZone(obs, this.ngZone); + return useZone !== true ? obs : wrapWithZone(obs, this.ngZone); } /** * Get an instance of ApolloClient */ - public get client(): ApolloClient { + public get client(): ApolloClient { return this.ensureClient(); } @@ -104,7 +128,7 @@ export class ApolloBase { * * @param client ApolloClient instance */ - public set client(client: ApolloClient) { + public set client(client: ApolloClient) { if (this._client) { throw new Error('Client has been already defined'); } @@ -112,13 +136,13 @@ export class ApolloBase { this._client = client; } - private ensureClient(): ApolloClient { + private ensureClient(): ApolloClient { this.checkInstance(); return this._client!; } - private checkInstance(): this is { _client: ApolloClient } { + private checkInstance(): this is { _client: ApolloClient } { if (this._client) { return true; } else { @@ -128,14 +152,14 @@ export class ApolloBase { } @Injectable() -export class Apollo extends ApolloBase { - private map: Map> = new Map>(); +export class Apollo extends ApolloBase { + private map: Map = new Map(); constructor( ngZone: NgZone, @Optional() @Inject(APOLLO_OPTIONS) - apolloOptions?: ApolloClientOptions, + apolloOptions?: ApolloClient.Options, @Inject(APOLLO_NAMED_OPTIONS) @Optional() apolloNamedOptions?: NamedOptions, @Inject(APOLLO_FLAGS) @Optional() flags?: Flags, ) { @@ -160,18 +184,18 @@ export class Apollo extends ApolloBase { * @param options Options required to create ApolloClient * @param name client's name */ - public create(options: ApolloClientOptions, name?: string): void { + public create(options: ApolloClient.Options, name?: string): void { if (isNamed(name)) { - this.createNamed(name, options); + this.createNamed(name, options); } else { - this.createDefault(options); + this.createDefault(options); } } /** * Use a default ApolloClient */ - public default(): ApolloBase { + public default(): ApolloBase { return this; } @@ -179,7 +203,7 @@ export class Apollo extends ApolloBase { * Use a named ApolloClient * @param name client's name */ - public use(name: string): ApolloBase { + public use(name: string): ApolloBase { if (isNamed(name)) { return this.map.get(name)!; } else { @@ -191,12 +215,12 @@ export class Apollo extends ApolloBase { * Create a default ApolloClient, same as `apollo.create(options)` * @param options ApolloClient's options */ - public createDefault(options: ApolloClientOptions): void { + public createDefault(options: ApolloClient.Options): void { if (this._client) { throw new Error('Apollo has been already created.'); } - this.client = this.ngZone.runOutsideAngular(() => new ApolloClient(options)); + this.client = this.ngZone.runOutsideAngular(() => new ApolloClient(options)); } /** @@ -204,7 +228,7 @@ export class Apollo extends ApolloBase { * @param name client's name * @param options ApolloClient's options */ - public createNamed(name: string, options: ApolloClientOptions): void { + public createNamed(name: string, options: ApolloClient.Options): void { if (this.map.has(name)) { throw new Error(`Client ${name} has been already created`); } @@ -213,7 +237,7 @@ export class Apollo extends ApolloBase { new ApolloBase( this.ngZone, this.flags, - this.ngZone.runOutsideAngular(() => new ApolloClient(options)), + this.ngZone.runOutsideAngular(() => new ApolloClient(options)), ), ); } diff --git a/packages/apollo-angular/src/gql.ts b/packages/apollo-angular/src/gql.ts index e58ff8d95..b2363efeb 100644 --- a/packages/apollo-angular/src/gql.ts +++ b/packages/apollo-angular/src/gql.ts @@ -1,4 +1,4 @@ -import { gql as gqlTag, TypedDocumentNode } from '@apollo/client/core'; +import { gql as gqlTag, TypedDocumentNode } from '@apollo/client'; const typedGQLTag: ( literals: ReadonlyArray | Readonly, diff --git a/packages/apollo-angular/src/index.ts b/packages/apollo-angular/src/index.ts index 0b4484e53..1fb141f72 100644 --- a/packages/apollo-angular/src/index.ts +++ b/packages/apollo-angular/src/index.ts @@ -1,4 +1,4 @@ -export type { TypedDocumentNode } from '@apollo/client/core'; +export type { TypedDocumentNode } from '@apollo/client'; export { provideApollo, provideNamedApollo } from './apollo-module'; export { Apollo, ApolloBase } from './apollo'; export { QueryRef, QueryRefFromDocument } from './query-ref'; @@ -6,17 +6,5 @@ export { Query } from './query'; export { Mutation } from './mutation'; export { Subscription } from './subscription'; export { APOLLO_OPTIONS, APOLLO_NAMED_OPTIONS, APOLLO_FLAGS } from './tokens'; -export type { - ExtraSubscriptionOptions, - Flags, - MutationOptionsAlone, - MutationResult, - NamedOptions, - QueryOptionsAlone, - ResultOf, - SubscriptionOptionsAlone, - VariablesOf, - WatchQueryOptions, - WatchQueryOptionsAlone, -} from './types'; +export type { Flags, NamedOptions, ResultOf, VariablesOf } from './types'; export { gql } from './gql'; diff --git a/packages/apollo-angular/src/mutation.ts b/packages/apollo-angular/src/mutation.ts index 7d706ece6..9f79ddc44 100644 --- a/packages/apollo-angular/src/mutation.ts +++ b/packages/apollo-angular/src/mutation.ts @@ -1,25 +1,35 @@ import type { DocumentNode } from 'graphql'; import type { Observable } from 'rxjs'; import { Injectable } from '@angular/core'; -import type { OperationVariables, TypedDocumentNode } from '@apollo/client/core'; +import type { OperationVariables, TypedDocumentNode } from '@apollo/client'; import { Apollo } from './apollo'; -import type { EmptyObject, MutationOptionsAlone, MutationResult } from './types'; +import type { EmptyObject } from './types'; + +export declare namespace Mutation { + export type MutateOptions< + TData = unknown, + TVariables extends OperationVariables = EmptyObject, + > = Omit, 'mutation'>; +} @Injectable() -export abstract class Mutation { - public abstract readonly document: DocumentNode | TypedDocumentNode; +export abstract class Mutation< + TData = unknown, + TVariables extends OperationVariables = EmptyObject, +> { + public abstract readonly document: DocumentNode | TypedDocumentNode; public client = 'default'; constructor(protected readonly apollo: Apollo) {} public mutate( - variables?: V, - options?: MutationOptionsAlone, - ): Observable> { - return this.apollo.use(this.client).mutate({ + ...[options]: {} extends TVariables + ? [options?: Mutation.MutateOptions] + : [options: Mutation.MutateOptions] + ): Observable> { + return this.apollo.use(this.client).mutate({ ...options, - variables, mutation: this.document, - }); + } as Apollo.MutateOptions); } } diff --git a/packages/apollo-angular/src/query-ref.ts b/packages/apollo-angular/src/query-ref.ts index 4d4fb52b5..0f336e057 100644 --- a/packages/apollo-angular/src/query-ref.ts +++ b/packages/apollo-angular/src/query-ref.ts @@ -1,66 +1,28 @@ import { from, Observable } from 'rxjs'; import { NgZone } from '@angular/core'; import type { - ApolloQueryResult, - FetchMoreQueryOptions, + ApolloClient, MaybeMasked, ObservableQuery, OperationVariables, - SubscribeToMoreOptions, TypedDocumentNode, - Unmasked, -} from '@apollo/client/core'; -import { NetworkStatus } from '@apollo/client/core'; -import { EmptyObject, WatchQueryOptions } from './types'; -import { fixObservable, wrapWithZone } from './utils'; - -function useInitialLoading(obsQuery: ObservableQuery) { - return function useInitialLoadingOperator(source: Observable): Observable { - return new Observable(function useInitialLoadingSubscription(subscriber) { - const currentResult = obsQuery.getCurrentResult(); - const { loading, errors, error, partial, data } = currentResult; - const { partialRefetch, fetchPolicy } = obsQuery.options; - - const hasError = errors || error; - - if ( - partialRefetch && - partial && - (!data || Object.keys(data).length === 0) && - fetchPolicy !== 'cache-only' && - !loading && - !hasError - ) { - subscriber.next({ - ...currentResult, - loading: true, - networkStatus: NetworkStatus.loading, - } as any); - } - - return source.subscribe(subscriber); - }); - }; -} +} from '@apollo/client'; +import { EmptyObject } from './types'; +import { wrapWithZone } from './utils'; export type QueryRefFromDocument = - T extends TypedDocumentNode ? QueryRef : never; + T extends TypedDocumentNode + ? QueryRef + : never; export class QueryRef { - public readonly valueChanges: Observable>; - public readonly queryId: ObservableQuery['queryId']; + public readonly valueChanges: Observable>; constructor( private readonly obsQuery: ObservableQuery, ngZone: NgZone, - options: WatchQueryOptions, ) { - const wrapped = wrapWithZone(from(fixObservable(this.obsQuery)), ngZone); - - this.valueChanges = options.useInitialLoading - ? wrapped.pipe(useInitialLoading(this.obsQuery)) - : wrapped; - this.queryId = this.obsQuery.queryId; + this.valueChanges = wrapWithZone(from(this.obsQuery), ngZone); } // ObservableQuery's methods @@ -73,26 +35,10 @@ export class QueryRef['result']> { - return this.obsQuery.result(); - } - public getCurrentResult(): ReturnType['getCurrentResult']> { return this.obsQuery.getCurrentResult(); } - public getLastResult(): ReturnType['getLastResult']> { - return this.obsQuery.getLastResult(); - } - - public getLastError(): ReturnType['getLastError']> { - return this.obsQuery.getLastError(); - } - - public resetLastResults(): ReturnType['resetLastResults']> { - return this.obsQuery.resetLastResults(); - } - public refetch( variables?: Parameters['refetch']>[0], ): ReturnType['refetch']> { @@ -100,16 +46,8 @@ export class QueryRef( - fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: ( - previousQueryResult: Unmasked, - options: { - fetchMoreResult: Unmasked; - variables: TFetchVars; - }, - ) => Unmasked; - }, - ): Promise>> { + fetchMoreOptions: ObservableQuery.FetchMoreOptions, + ): Promise>> { return this.obsQuery.fetchMore(fetchMoreOptions); } @@ -117,7 +55,12 @@ export class QueryRef( - options: SubscribeToMoreOptions, + options: ObservableQuery.SubscribeToMoreOptions< + TData, + TSubscriptionVariables, + TSubscriptionData, + TVariables + >, ): ReturnType['subscribeToMore']> { return this.obsQuery.subscribeToMore(options); } @@ -138,15 +81,15 @@ export class QueryRef['setOptions']>[0], - ): ReturnType['setOptions']> { - return this.obsQuery.setOptions(opts); - } - public setVariables( variables: Parameters['setVariables']>[0], ): ReturnType['setVariables']> { return this.obsQuery.setVariables(variables); } + + public reobserve( + options: ObservableQuery.Options, + ): ReturnType['reobserve']> { + return this.obsQuery.reobserve(options); + } } diff --git a/packages/apollo-angular/src/query.ts b/packages/apollo-angular/src/query.ts index 00b2a5d82..4b165a6fa 100644 --- a/packages/apollo-angular/src/query.ts +++ b/packages/apollo-angular/src/query.ts @@ -1,31 +1,49 @@ import type { DocumentNode } from 'graphql'; import type { Observable } from 'rxjs'; import { Injectable } from '@angular/core'; -import type { ApolloQueryResult, OperationVariables, TypedDocumentNode } from '@apollo/client/core'; +import type { OperationVariables, TypedDocumentNode } from '@apollo/client'; import { Apollo } from './apollo'; import { QueryRef } from './query-ref'; -import { EmptyObject, QueryOptionsAlone, WatchQueryOptionsAlone } from './types'; +import { EmptyObject } from './types'; + +export declare namespace Query { + export type WatchOptions< + TData = unknown, + TVariables extends OperationVariables = EmptyObject, + > = Omit, 'query'>; + + export type FetchOptions< + TData = unknown, + TVariables extends OperationVariables = EmptyObject, + > = Omit, 'query'>; +} @Injectable() -export abstract class Query { - public abstract readonly document: DocumentNode | TypedDocumentNode; +export abstract class Query { + public abstract readonly document: DocumentNode | TypedDocumentNode; public client = 'default'; constructor(protected readonly apollo: Apollo) {} - public watch(variables?: V, options?: WatchQueryOptionsAlone): QueryRef { - return this.apollo.use(this.client).watchQuery({ + public watch( + ...[options]: {} extends TVariables + ? [options?: Query.WatchOptions] + : [options: Query.WatchOptions] + ): QueryRef { + return this.apollo.use(this.client).watchQuery({ ...options, - variables, query: this.document, - }); + } as Apollo.WatchQueryOptions); } - public fetch(variables?: V, options?: QueryOptionsAlone): Observable> { - return this.apollo.use(this.client).query({ + public fetch( + ...[options]: {} extends TVariables + ? [options?: Query.FetchOptions] + : [options: Query.FetchOptions] + ): Observable> { + return this.apollo.use(this.client).query({ ...options, - variables, query: this.document, - }); + } as Apollo.QueryOptions); } } diff --git a/packages/apollo-angular/src/subscription.ts b/packages/apollo-angular/src/subscription.ts index 5468529c7..912de96a6 100644 --- a/packages/apollo-angular/src/subscription.ts +++ b/packages/apollo-angular/src/subscription.ts @@ -1,29 +1,35 @@ import type { DocumentNode } from 'graphql'; import type { Observable } from 'rxjs'; import { Injectable } from '@angular/core'; -import type { FetchResult, OperationVariables, TypedDocumentNode } from '@apollo/client/core'; +import type { OperationVariables, TypedDocumentNode } from '@apollo/client'; import { Apollo } from './apollo'; -import { EmptyObject, ExtraSubscriptionOptions, SubscriptionOptionsAlone } from './types'; +import { EmptyObject } from './types'; + +export declare namespace Subscription { + export type SubscribeOptions< + TData = unknown, + TVariables extends OperationVariables = EmptyObject, + > = Omit, 'query'>; +} @Injectable() -export abstract class Subscription { - public abstract readonly document: DocumentNode | TypedDocumentNode; +export abstract class Subscription< + TData = unknown, + TVariables extends OperationVariables = EmptyObject, +> { + public abstract readonly document: DocumentNode | TypedDocumentNode; public client = 'default'; constructor(protected readonly apollo: Apollo) {} public subscribe( - variables?: V, - options?: SubscriptionOptionsAlone, - extra?: ExtraSubscriptionOptions, - ): Observable> { - return this.apollo.use(this.client).subscribe( - { - ...options, - variables, - query: this.document, - }, - extra, - ); + ...[options]: {} extends TVariables + ? [options?: Subscription.SubscribeOptions] + : [options: Subscription.SubscribeOptions] + ): Observable> { + return this.apollo.use(this.client).subscribe({ + ...options, + query: this.document, + } as Apollo.SubscribeOptions); } } diff --git a/packages/apollo-angular/src/tokens.ts b/packages/apollo-angular/src/tokens.ts index b4bfcd303..0a8bda2f2 100644 --- a/packages/apollo-angular/src/tokens.ts +++ b/packages/apollo-angular/src/tokens.ts @@ -1,9 +1,9 @@ import { InjectionToken } from '@angular/core'; -import type { ApolloClientOptions } from '@apollo/client/core'; +import type { ApolloClient } from '@apollo/client'; import type { Flags, NamedOptions } from './types'; export const APOLLO_FLAGS = new InjectionToken('APOLLO_FLAGS'); -export const APOLLO_OPTIONS = new InjectionToken>('APOLLO_OPTIONS'); +export const APOLLO_OPTIONS = new InjectionToken('APOLLO_OPTIONS'); export const APOLLO_NAMED_OPTIONS = new InjectionToken('APOLLO_NAMED_OPTIONS'); diff --git a/packages/apollo-angular/src/types.ts b/packages/apollo-angular/src/types.ts index 1259f49a2..dc45338e0 100644 --- a/packages/apollo-angular/src/types.ts +++ b/packages/apollo-angular/src/types.ts @@ -1,14 +1,4 @@ -import type { - ApolloClientOptions, - MutationOptions as CoreMutationOptions, - QueryOptions as CoreQueryOptions, - SubscriptionOptions as CoreSubscriptionOptions, - WatchFragmentOptions as CoreWatchFragmentOptions, - WatchQueryOptions as CoreWatchQueryOptions, - FetchResult, - OperationVariables, - TypedDocumentNode, -} from '@apollo/client/core'; +import type { ApolloClient, TypedDocumentNode } from '@apollo/client'; export type EmptyObject = { [key: string]: any; @@ -19,65 +9,11 @@ export type ResultOf = export type VariablesOf = T extends TypedDocumentNode ? V : never; -export interface ExtraSubscriptionOptions { - useZone?: boolean; -} - -export type MutationResult = FetchResult & { - loading?: boolean; -}; - export type Omit = Pick>; -export interface WatchQueryOptionsAlone< - TVariables extends OperationVariables = EmptyObject, - TData = any, -> extends Omit, 'query' | 'variables'> {} - -export interface QueryOptionsAlone - extends Omit, 'query' | 'variables'> {} - -export interface MutationOptionsAlone - extends Omit, 'mutation' | 'variables'> {} - -export interface SubscriptionOptionsAlone - extends Omit, 'query' | 'variables'> {} - -export interface WatchQueryOptions - extends CoreWatchQueryOptions { - /** - * Observable starts with `{ loading: true }`. - * There's a big chance the next major version will enable that by default. - * - * Disabled by default - */ - useInitialLoading?: boolean; -} - -export interface MutationOptions - extends CoreMutationOptions { - /** - * Observable starts with `{ loading: true }`. - * There's a big chance the next major version will enable that by default. - * - * Disabled by default - */ - useMutationLoading?: boolean; -} - -export interface WatchFragmentOptions - extends CoreWatchFragmentOptions {} - -export type NamedOptions = Record>; +export type NamedOptions = Record; export type Flags = { - /** - * Observable starts with `{ loading: true }`. - * There's a big chance the next major version will enable that by default. - * - * Disabled by default - */ - useInitialLoading?: boolean; /** * Observable starts with `{ loading: true }`. * diff --git a/packages/apollo-angular/src/utils.ts b/packages/apollo-angular/src/utils.ts index 02f21136e..d4db5d3d8 100644 --- a/packages/apollo-angular/src/utils.ts +++ b/packages/apollo-angular/src/utils.ts @@ -1,16 +1,13 @@ -import type { SchedulerAction, SchedulerLike, Subscription } from 'rxjs'; -import { Observable, observable, queueScheduler } from 'rxjs'; +import { Observable, queueScheduler, SchedulerAction, SchedulerLike, Subscription } from 'rxjs'; import { map, observeOn, startWith } from 'rxjs/operators'; import { NgZone } from '@angular/core'; -import type { - Observable as AObservable, - ApolloQueryResult, - FetchResult, - ObservableQuery, -} from '@apollo/client/core'; -import { MutationResult } from './types'; +import type { ApolloClient } from '@apollo/client'; +import { Apollo } from './apollo'; -export function fromPromise(promiseFn: () => Promise): Observable { +/** + * Like RxJS's `fromPromise()`, but starts the promise only when the observable is subscribed to. + */ +export function fromLazyPromise(promiseFn: () => Promise): Observable { return new Observable(subscriber => { promiseFn().then( result => { @@ -30,10 +27,13 @@ export function fromPromise(promiseFn: () => Promise): Observable { }); } -export function useMutationLoading(source: Observable>, enabled: boolean) { +export function useMutationLoading( + source: Observable>, + enabled: boolean, +) { if (!enabled) { return source.pipe( - map, MutationResult>(result => ({ + map, Apollo.MutateResult>(result => ({ ...result, loading: false, })), @@ -41,20 +41,21 @@ export function useMutationLoading(source: Observable>, enable } return source.pipe( - startWith>({ - loading: true, - }), - map, MutationResult>(result => ({ + map, Apollo.MutateResult>(result => ({ ...result, - loading: !!result.loading, + loading: false, })), + startWith>({ + data: undefined, + loading: true, + }), ); } export class ZoneScheduler implements SchedulerLike { constructor(private readonly zone: NgZone) {} - public now = Date.now ? Date.now : () => +new Date(); + public readonly now = Date.now; public schedule( work: (this: SchedulerAction, state?: T) => void, @@ -65,18 +66,6 @@ export class ZoneScheduler implements SchedulerLike { } } -// XXX: Apollo's QueryObservable is not compatible with RxJS -// TODO: remove it in one of future releases -// https://github.com/ReactiveX/rxjs/blob/9fb0ce9e09c865920cf37915cc675e3b3a75050b/src/internal/util/subscribeTo.ts#L32 -export function fixObservable(obs: ObservableQuery): Observable>; -export function fixObservable(obs: AObservable): Observable; -export function fixObservable( - obs: AObservable | ObservableQuery, -): Observable> | Observable { - (obs as any)[observable] = () => obs; - return obs as any; -} - export function wrapWithZone(obs: Observable, ngZone: NgZone): Observable { return obs.pipe(observeOn(new ZoneScheduler(ngZone))); } diff --git a/packages/apollo-angular/test-utils/ObservableStream.ts b/packages/apollo-angular/test-utils/ObservableStream.ts new file mode 100644 index 000000000..4603d3903 --- /dev/null +++ b/packages/apollo-angular/test-utils/ObservableStream.ts @@ -0,0 +1,169 @@ +/** + * Adapted from + * https://github.com/apollographql/apollo-client/blob/1d165ba37eca7e5d667055553aacc4c26be56065/src/testing/internal/ObservableStream.ts + * + * The MIT License (MIT) + * + * Copyright (c) 2022 Apollo Graph, Inc. (Formerly Meteor Development Group, Inc.) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import { ReadableStream } from 'node:stream/web'; +import type { Observable, Subscribable, Unsubscribable } from 'rxjs'; +import { expect } from 'vitest'; +import { equals, iterableEquality, JEST_MATCHERS_OBJECT } from '@vitest/expect'; +import { printDiffOrStringify } from '@vitest/utils/diff'; + +export interface TakeOptions { + timeout?: number; +} +type ObservableEvent = + | { type: 'next'; value: T } + | { type: 'error'; error: any } + | { type: 'complete' }; + +function formatMessage(expected: ObservableEvent, actual: ObservableEvent) { + return printDiffOrStringify(expected, actual, { expand: true }); +} + +export class EventMismatchError extends Error { + static is(error: unknown): error is EventMismatchError { + return error instanceof Error && error.name === 'EventMismatchError'; + } + + constructor(expected: ObservableEvent, actual: ObservableEvent) { + super(formatMessage(expected, actual)); + this.name = 'EventMismatchError'; + + Object.setPrototypeOf(this, EventMismatchError.prototype); + } +} + +export class ObservableStream { + private reader: ReadableStreamDefaultReader>; + private subscription!: Unsubscribable; + private readerQueue: Array>> = []; + + constructor(observable: Observable | Subscribable) { + this.unsubscribe = this.unsubscribe.bind(this); + this.reader = new ReadableStream>({ + start: controller => { + this.subscription = observable.subscribe({ + next: value => controller.enqueue({ type: 'next', value }), + error: error => controller.enqueue({ type: 'error', error }), + complete: () => controller.enqueue({ type: 'complete' }), + }); + }, + }).getReader(); + } + + peek({ timeout = 100 }: TakeOptions = {}) { + // Calling `peek` multiple times in a row should not advance the reader + // multiple times until this value has been consumed. + let readerPromise = this.readerQueue[0]; + + if (!readerPromise) { + // Since this.reader.read() advances the reader in the stream, we don't + // want to consume this promise entirely, otherwise we will miss it when + // calling `take`. Instead, we push it into a queue that can be consumed + // by `take` the next time its called so that we avoid advancing the + // reader until we are finished processing all peeked values. + readerPromise = this.readNextValue(); + this.readerQueue.push(readerPromise); + } + + return Promise.race([ + readerPromise, + new Promise>((_, reject) => { + setTimeout(reject, timeout, new Error('Timeout waiting for next event')); + }), + ]); + } + + take({ timeout = 100 }: TakeOptions = {}) { + return Promise.race([ + this.readerQueue.shift() || this.readNextValue(), + new Promise>((_, reject) => { + setTimeout(reject, timeout, new Error('Timeout waiting for next event')); + }), + ]).then(value => { + if (value.type === 'next') { + this.current = value.value; + } + return value; + }); + } + + [Symbol.dispose]() { + this.unsubscribe(); + } + + unsubscribe() { + this.subscription.unsubscribe(); + } + + async takeNext(options?: TakeOptions): Promise { + const event = await this.take(options); + if (event.type !== 'next') { + throw new EventMismatchError({ type: 'next', value: expect.anything() }, event); + } + return (event as ObservableEvent & { type: 'next' }).value; + } + + async takeError(options?: TakeOptions): Promise { + const event = await this.take(options); + validateEquals(event, { type: 'error', error: expect.anything() }); + return (event as ObservableEvent & { type: 'error' }).error; + } + + async takeComplete(options?: TakeOptions): Promise { + const event = await this.take(options); + validateEquals(event, { type: 'complete' }); + } + + private async readNextValue() { + return this.reader.read().then(result => result.value!); + } + + private current?: T; + getCurrent() { + return this.current; + } +} + +// Lightweight expect(...).toEqual(...) check that avoids using `expect` so that +// `expect.assertions(num)` does not double count assertions when using the take* +// functions inside of expect(stream).toEmit* matchers. +function validateEquals(actualEvent: ObservableEvent, expectedEvent: ObservableEvent) { + // Uses the same matchers as expect(...).toEqual(...) + // https://github.com/vitest-dev/vitest/blob/438c44e7fb8f3a6a36db8ff504f852c01963ba88/packages/expect/src/jest-expect.ts#L107-L110 + const isEqual = equals(actualEvent, expectedEvent, [ + ...getCustomEqualityTesters(), + iterableEquality, + ]); + + if (!isEqual) { + throw new EventMismatchError(expectedEvent, actualEvent); + } +} + +// https://github.com/vitest-dev/vitest/blob/438c44e7fb8f3a6a36db8ff504f852c01963ba88/packages/expect/src/jest-matcher-utils.ts#L157-L159 +function getCustomEqualityTesters() { + return (globalThis as any)[JEST_MATCHERS_OBJECT].customEqualityTesters; +} diff --git a/packages/apollo-angular/test-utils/matchers.ts b/packages/apollo-angular/test-utils/matchers.ts new file mode 100644 index 000000000..2385bebc8 --- /dev/null +++ b/packages/apollo-angular/test-utils/matchers.ts @@ -0,0 +1,6 @@ +import { expect } from 'vitest'; +import { toEmitAnything } from './matchers/toEmitAnything'; + +expect.extend({ + toEmitAnything, +}); diff --git a/packages/apollo-angular/test-utils/matchers/toEmitAnything.ts b/packages/apollo-angular/test-utils/matchers/toEmitAnything.ts new file mode 100644 index 000000000..a4f6730cb --- /dev/null +++ b/packages/apollo-angular/test-utils/matchers/toEmitAnything.ts @@ -0,0 +1,61 @@ +/** + * Adapted from + * https://github.com/apollographql/apollo-client/blob/1d165ba37eca7e5d667055553aacc4c26be56065/src/testing/matchers/toEmitAnything.ts + * + * The MIT License (MIT) + * + * Copyright (c) 2022 Apollo Graph, Inc. (Formerly Meteor Development Group, Inc.) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import { ObservableStream, TakeOptions } from 'test-utils/ObservableStream'; +import { RawMatcherFn } from '@vitest/expect'; + +export const toEmitAnything: RawMatcherFn = async function toEmitAnything( + actual, + options?: TakeOptions, +) { + const stream = actual as ObservableStream; + const hint = this.utils.matcherHint('toEmitAnything', 'stream'); + + try { + const value = await stream.peek(options); + + return { + pass: true, + message: () => { + return ( + hint + + '\n\nExpected stream not to emit anything but it did.' + + '\n\nReceived:\n' + + this.utils.printReceived(value) + ); + }, + }; + } catch (error) { + if (error instanceof Error && error.message === 'Timeout waiting for next event') { + return { + pass: false, + message: () => hint + '\n\nExpected stream to emit an event but it did not.', + }; + } else { + throw error; + } + } +}; diff --git a/packages/apollo-angular/testing/src/backend.ts b/packages/apollo-angular/testing/src/backend.ts index 245aadc08..b1a03c89d 100644 --- a/packages/apollo-angular/testing/src/backend.ts +++ b/packages/apollo-angular/testing/src/backend.ts @@ -1,7 +1,8 @@ import { DocumentNode, print } from 'graphql'; -import { Observer } from 'rxjs'; +import { Observable, Observer } from 'rxjs'; import { Injectable } from '@angular/core'; -import { FetchResult, Observable as LinkObservable } from '@apollo/client/core'; +import { ApolloLink } from '@apollo/client'; +import { addTypenameToDocument } from '@apollo/client/utilities'; import { ApolloTestingController, MatchOperation } from './controller'; import { Operation, TestOperation } from './operation'; @@ -23,8 +24,8 @@ export class ApolloTestingBackend implements ApolloTestingController { /** * Handle an incoming operation by queueing it in the list of open operations. */ - public handle(op: Operation): LinkObservable { - return new LinkObservable((observer: Observer) => { + public handle(op: Operation): Observable { + return new Observable((observer: Observer) => { const testOp = new TestOperation(op, observer); this.open.push(testOp); }); @@ -40,7 +41,9 @@ export class ApolloTestingBackend implements ApolloTestingController { return this.open.filter(testOp => match(testOp.operation)); } else { if (this.isDocumentNode(match)) { - return this.open.filter(testOp => print(testOp.operation.query) === print(match)); + return this.open.filter( + testOp => print(testOp.operation.query) === print(addTypenameToDocument(match)), + ); } return this.open.filter(testOp => this.matchOp(match, testOp)); @@ -54,7 +57,7 @@ export class ApolloTestingBackend implements ApolloTestingController { const sameName = this.compare(match.operationName, testOp.operation.operationName); const sameVariables = this.compare(variables, testOp.operation.variables); - const sameQuery = print(testOp.operation.query) === print(match.query); + const sameQuery = print(testOp.operation.query) === print(addTypenameToDocument(match.query)); const sameExtensions = this.compare(extensions, testOp.operation.extensions); diff --git a/packages/apollo-angular/testing/src/module.ts b/packages/apollo-angular/testing/src/module.ts index f5b3a3b53..9269d0cd1 100644 --- a/packages/apollo-angular/testing/src/module.ts +++ b/packages/apollo-angular/testing/src/module.ts @@ -1,20 +1,13 @@ import { Apollo } from 'apollo-angular'; import { Inject, InjectionToken, NgModule, Optional } from '@angular/core'; -import { - ApolloCache, - ApolloLink, - InMemoryCache, - Operation as LinkOperation, -} from '@apollo/client/core'; +import { ApolloCache, ApolloLink, InMemoryCache } from '@apollo/client'; import { ApolloTestingBackend } from './backend'; import { ApolloTestingController } from './controller'; import { Operation } from './operation'; -export type NamedCaches = Record | undefined | null>; +export type NamedCaches = Record; -export const APOLLO_TESTING_CACHE = new InjectionToken>( - 'apollo-angular/testing cache', -); +export const APOLLO_TESTING_CACHE = new InjectionToken('apollo-angular/testing cache'); export const APOLLO_TESTING_NAMED_CACHE = new InjectionToken( 'apollo-angular/testing named cache', @@ -24,7 +17,7 @@ export const APOLLO_TESTING_CLIENTS = new InjectionToken( 'apollo-angular/testing named clients', ); -function addClient(name: string, op: LinkOperation): Operation { +function addClient(name: string, op: ApolloLink.Operation): Operation { (op as Operation).clientName = name; return op as Operation; @@ -46,20 +39,16 @@ export class ApolloTestingModuleCore { namedClients?: string[], @Optional() @Inject(APOLLO_TESTING_CACHE) - cache?: ApolloCache, + cache?: ApolloCache, @Optional() @Inject(APOLLO_TESTING_NAMED_CACHE) namedCaches?: NamedCaches, ) { - function createOptions(name: string, c?: ApolloCache | null) { + function createOptions(name: string, c?: ApolloCache | null) { return { connectToDevTools: false, link: new ApolloLink(operation => backend.handle(addClient(name, operation))), - cache: - c || - new InMemoryCache({ - addTypename: false, - }), + cache: c || new InMemoryCache(), }; } diff --git a/packages/apollo-angular/testing/src/operation.ts b/packages/apollo-angular/testing/src/operation.ts index 4be45d4e0..1f11257b0 100644 --- a/packages/apollo-angular/testing/src/operation.ts +++ b/packages/apollo-angular/testing/src/operation.ts @@ -1,33 +1,26 @@ -import { FormattedExecutionResult, GraphQLError, Kind, OperationTypeNode } from 'graphql'; +import { GraphQLFormattedError, OperationTypeNode } from 'graphql'; import { Observer } from 'rxjs'; -import { ApolloError, FetchResult, Operation as LinkOperation } from '@apollo/client/core'; -import { getMainDefinition } from '@apollo/client/utilities'; +import { ApolloLink, ErrorLike } from '@apollo/client'; +import { isErrorLike } from '@apollo/client/errors'; -const isApolloError = (err: any): err is ApolloError => err && err.hasOwnProperty('graphQLErrors'); - -export type Operation = LinkOperation & { +export type Operation = ApolloLink.Operation & { clientName: string; }; export class TestOperation { constructor( public readonly operation: Operation, - private readonly observer: Observer>, + private readonly observer: Observer>, ) {} - public flush(result: FormattedExecutionResult | ApolloError): void { - if (isApolloError(result)) { + public flush(result: ApolloLink.Result | ErrorLike): void { + if (isErrorLike(result)) { this.observer.error(result); } else { const fetchResult = result ? { ...result } : result; this.observer.next(fetchResult); - const definition = getMainDefinition(this.operation.query); - - if ( - definition.kind === Kind.OPERATION_DEFINITION && - definition.operation !== OperationTypeNode.SUBSCRIPTION - ) { + if (this.operation.operationType !== OperationTypeNode.SUBSCRIPTION) { this.complete(); } } @@ -38,20 +31,14 @@ export class TestOperation { } public flushData(data: T | null): void { - this.flush({ - data, - }); + this.flush({ data }); } - public networkError(error: Error): void { - const apolloError = new ApolloError({ - networkError: error, - }); - - this.flush(apolloError); + public networkError(error: ErrorLike): void { + this.flush(error); } - public graphqlErrors(errors: GraphQLError[]): void { + public graphqlErrors(errors: GraphQLFormattedError[]): void { this.flush({ errors, }); diff --git a/packages/apollo-angular/testing/tests/integration.spec.ts b/packages/apollo-angular/testing/tests/integration.spec.ts index 4143acc2a..f88ff42ef 100644 --- a/packages/apollo-angular/testing/tests/integration.spec.ts +++ b/packages/apollo-angular/testing/tests/integration.spec.ts @@ -1,16 +1,17 @@ import { print } from 'graphql'; import { afterEach, beforeEach, describe, expect, test } from 'vitest'; import { TestBed } from '@angular/core/testing'; -import { gql, InMemoryCache } from '@apollo/client/core'; +import { gql } from '@apollo/client'; import { addTypenameToDocument } from '@apollo/client/utilities'; import { Apollo } from '../../src'; -import { APOLLO_TESTING_CACHE, ApolloTestingController, ApolloTestingModule } from '../src'; +import { ApolloTestingController, ApolloTestingModule } from '../src'; describe('Integration', () => { let apollo: Apollo; let backend: ApolloTestingController; beforeEach(() => { + TestBed.resetTestingModule(); TestBed.configureTestingModule({ imports: [ApolloTestingModule], }); @@ -38,6 +39,7 @@ describe('Integration', () => { heroes: [ { name: 'Superman', + __typename: 'Character', }, ], }; @@ -72,6 +74,7 @@ describe('Integration', () => { heroes: [ { name: 'Superman', + __typename: 'Character', }, ], }; @@ -106,6 +109,7 @@ describe('Integration', () => { heroes: [ { name: 'Superman', + __typename: 'Character', }, ], }; @@ -131,6 +135,7 @@ describe('Integration', () => { query heroes($first: Int!) { heroes(first: $first) { name + __typename } } `, @@ -143,6 +148,7 @@ describe('Integration', () => { heroes: [ { name: 'Superman', + __typename: 'Character', }, ], }; @@ -171,17 +177,6 @@ describe('Integration', () => { test('it should be able to test with fragments', () => new Promise(done => { - TestBed.resetTestingModule(); - TestBed.configureTestingModule({ - imports: [ApolloTestingModule], - providers: [ - { - provide: APOLLO_TESTING_CACHE, - useValue: new InMemoryCache({ addTypename: true }), - }, - ], - }); - const apollo = TestBed.inject(Apollo); const backend = TestBed.inject(ApolloTestingController); diff --git a/packages/apollo-angular/testing/tests/module.spec.ts b/packages/apollo-angular/testing/tests/module.spec.ts index 6a27345f7..81dc4fb86 100644 --- a/packages/apollo-angular/testing/tests/module.spec.ts +++ b/packages/apollo-angular/testing/tests/module.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest'; import { TestBed } from '@angular/core/testing'; -import { ApolloReducerConfig, gql, InMemoryCache } from '@apollo/client/core'; +import { gql, InMemoryCache } from '@apollo/client'; import { Apollo } from '../../src'; import { APOLLO_TESTING_CACHE, ApolloTestingController, ApolloTestingModule } from '../src'; @@ -12,14 +12,12 @@ describe('ApolloTestingModule', () => { const apollo = TestBed.inject(Apollo); const cache = apollo.client.cache as InMemoryCache; - const config: ApolloReducerConfig = (cache as any).config; expect(cache).toBeInstanceOf(InMemoryCache); - expect(config.addTypename).toBe(false); }); test('should allow to use custom ApolloCache', () => { - const cache = new InMemoryCache({ addTypename: true }); + const cache = new InMemoryCache(); TestBed.configureTestingModule({ imports: [ApolloTestingModule], diff --git a/packages/apollo-angular/testing/tests/operation.spec.ts b/packages/apollo-angular/testing/tests/operation.spec.ts index 4dc586d9f..1cd24075e 100644 --- a/packages/apollo-angular/testing/tests/operation.spec.ts +++ b/packages/apollo-angular/testing/tests/operation.spec.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, test } from 'vitest'; -import { ApolloLink, execute, FetchResult, gql } from '@apollo/client/core'; +import { ApolloLink, gql } from '@apollo/client'; import { ApolloTestingBackend } from '../src/backend'; -import { buildOperationForLink } from './utils'; +import { buildOperationForLink, executeWithDefaultContext as execute } from './utils'; const testQuery = gql` query allHeroes { @@ -73,7 +73,7 @@ describe('TestOperation', () => { test('should leave the operation open for a subscription', () => new Promise(done => { const operation = buildOperationForLink(testSubscription, {}); - const emittedResults: FetchResult[] = []; + const emittedResults: ApolloLink.Result[] = []; execute(link, operation).subscribe({ next(result) { @@ -112,7 +112,7 @@ describe('TestOperation', () => { test('should close the operation after a query', () => new Promise(done => { const operation = buildOperationForLink(testQuery, {}); - const emittedResults: FetchResult[] = []; + const emittedResults: ApolloLink.Result[] = []; execute(link, operation).subscribe({ next(result) { @@ -144,7 +144,7 @@ describe('TestOperation', () => { test('should close the operation after a mutation', () => new Promise(done => { const operation = buildOperationForLink(testMutation, { hero: 'firstHero' }); - const emittedResults: FetchResult[] = []; + const emittedResults: ApolloLink.Result[] = []; execute(link, operation).subscribe({ next(result) { diff --git a/packages/apollo-angular/testing/tests/utils.ts b/packages/apollo-angular/testing/tests/utils.ts index 3305e6e49..3140b29c9 100644 --- a/packages/apollo-angular/testing/tests/utils.ts +++ b/packages/apollo-angular/testing/tests/utils.ts @@ -1,15 +1,33 @@ import { DocumentNode } from 'graphql'; -import type { GraphQLRequest } from '@apollo/client/link/core/types'; -import { getOperationName } from '@apollo/client/utilities'; +import { Observable } from 'rxjs'; +import { ApolloClient, execute, InMemoryCache, OperationVariables } from '@apollo/client'; +import { ApolloLink } from '@apollo/client/link'; +import { addTypenameToDocument } from '@apollo/client/utilities'; -export function buildOperationForLink>( +export function buildOperationForLink( document: DocumentNode, - variables: TVariables, -): GraphQLRequest { + variables: OperationVariables | undefined, +): ApolloLink.Request { return { - query: document, + query: addTypenameToDocument(document), variables, - operationName: getOperationName(document) || undefined, context: {}, }; } + +export function createDefaultExecuteContext(): ApolloLink.ExecuteContext { + return { + client: new ApolloClient({ + cache: new InMemoryCache(), + link: ApolloLink.empty(), + }), + }; +} + +export function executeWithDefaultContext( + link: ApolloLink, + request: ApolloLink.Request, + context: ApolloLink.ExecuteContext = createDefaultExecuteContext(), +): Observable { + return execute(link, request, context); +} diff --git a/packages/apollo-angular/tests/Apollo.spec.ts b/packages/apollo-angular/tests/Apollo.spec.ts index a123f0ffa..953bcd257 100644 --- a/packages/apollo-angular/tests/Apollo.spec.ts +++ b/packages/apollo-angular/tests/Apollo.spec.ts @@ -3,11 +3,12 @@ import { mergeMap } from 'rxjs/operators'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import { NgZone } from '@angular/core'; import { TestBed } from '@angular/core/testing'; -import { ApolloLink, InMemoryCache, NetworkStatus } from '@apollo/client/core'; -import { mockSingleLink } from '@apollo/client/testing'; +import { ApolloLink, InMemoryCache, NetworkStatus } from '@apollo/client'; +import { MockLink } from '@apollo/client/testing'; import { Apollo, ApolloBase } from '../src/apollo'; import { gql } from '../src/gql'; import { ZoneScheduler } from '../src/utils'; +import { ObservableStream } from '../test-utils/ObservableStream'; function mockApollo(link: ApolloLink, _ngZone: NgZone) { const apollo = new Apollo(_ngZone); @@ -40,7 +41,7 @@ describe('Apollo', () => { const apollo = new Apollo(ngZone); apollo.create({ - link: mockSingleLink(), + link: new MockLink([]), cache: new InMemoryCache(), }); @@ -55,7 +56,7 @@ describe('Apollo', () => { apollo.create( { - link: mockSingleLink(), + link: new MockLink([]), cache: new InMemoryCache(), }, 'extra', @@ -71,7 +72,7 @@ describe('Apollo', () => { const apollo = new Apollo(ngZone); apollo.create({ - link: mockSingleLink(), + link: new MockLink([]), cache: new InMemoryCache(), }); @@ -90,73 +91,77 @@ describe('Apollo', () => { expect(client.watchQuery).toBeCalledWith(options); }); - test('should be able to refetch', () => - new Promise(done => { - expect.assertions(3); - const query = gql` - query refetch($first: Int) { - heroes(first: $first) { - name - __typename - } + test('should be able to refetch', async () => { + const query = gql` + query refetch($first: Int) { + heroes(first: $first) { + name + __typename } - `; + } + `; - const data1 = { heroes: [{ name: 'Foo', __typename: 'Hero' }] }; - const variables1 = { first: 0 }; + const data1 = { heroes: [{ name: 'Foo', __typename: 'Hero' }] }; + const variables1 = { first: 0 }; - const data2 = { heroes: [{ name: 'Bar', __typename: 'Hero' }] }; - const variables2 = { first: 1 }; + const data2 = { heroes: [{ name: 'Bar', __typename: 'Hero' }] }; + const variables2 = { first: 1 }; - const link = mockSingleLink( - { - request: { query, variables: variables1 }, - result: { data: data1 }, - }, - { - request: { query, variables: variables2 }, - result: { data: data2 }, - }, - ); + const link = new MockLink([ + { + request: { query, variables: variables1 }, + result: { data: data1 }, + }, + { + request: { query, variables: variables2 }, + result: { data: data2 }, + }, + ]); - const apollo = mockApollo(link, ngZone); - const options = { query, variables: variables1 }; - const obs = apollo.watchQuery(options); + const apollo = mockApollo(link, ngZone); + const options = { query, variables: variables1 }; + const obs = apollo.watchQuery(options); - let calls = 0; + const stream = new ObservableStream(obs.valueChanges); - obs.valueChanges.subscribe({ - next: ({ data }) => { - calls++; + await expect(stream.takeNext()).resolves.toEqual({ + data: undefined, + dataState: 'empty', + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); - try { - if (calls === 1) { - expect(data).toMatchObject(data1); - } else if (calls === 2) { - expect(data).toMatchObject(data2); - done(); - } else if (calls > 2) { - throw new Error('Called third time'); - } - } catch (e: any) { - throw e; - } - }, - error: err => { - throw err; - }, - }); + await expect(stream.takeNext()).resolves.toEqual({ + data: data1, + dataState: 'complete', + loading: false, + networkStatus: NetworkStatus.ready, + partial: false, + }); - setTimeout(() => { - obs.refetch(variables2).then(({ data }) => { - try { - expect(data).toMatchObject(data2); - } catch (e: any) { - throw e; - } - }); - }); - })); + await expect(stream).not.toEmitAnything(); + + await expect(obs.refetch(variables2)).resolves.toEqual({ data: data2 }); + + await expect(stream.takeNext()).resolves.toEqual({ + data: undefined, + dataState: 'empty', + loading: true, + networkStatus: NetworkStatus.refetch, + partial: true, + }); + + await expect(stream.takeNext()).resolves.toEqual({ + data: data2, + dataState: 'complete', + loading: false, + networkStatus: NetworkStatus.ready, + partial: false, + }); + + await expect(stream).not.toEmitAnything(); + }); }); describe('query()', () => { @@ -166,7 +171,7 @@ describe('Apollo', () => { const apollo = new Apollo(ngZone); apollo.create({ - link: mockSingleLink(), + link: new MockLink([]), cache: new InMemoryCache(), }); @@ -195,13 +200,13 @@ describe('Apollo', () => { const apollo = new Apollo(ngZone); apollo.create({ - link: mockSingleLink(), + link: new MockLink([]), cache: new InMemoryCache(), }); const client = apollo.client; - client.query = vi.fn(options => { + client.query = vi.fn((options: { used: boolean }) => { if (options.used) { throw new Error('options was reused'); } @@ -235,7 +240,7 @@ describe('Apollo', () => { const apollo = new Apollo(ngZone); apollo.create({ - link: mockSingleLink(), + link: new MockLink([]), cache: new InMemoryCache(), }); @@ -254,52 +259,6 @@ describe('Apollo', () => { }, }); })); - - test('should NOT useInitialLoading by default', () => - new Promise(done => { - expect.assertions(2); - const apollo = testBed.inject(Apollo); - const query = gql` - query heroes { - heroes { - name - __typename - } - } - `; - const data = { - heroes: [ - { - name: 'Superman', - __typename: 'Hero', - }, - ], - }; - - // create - apollo.create({ - link: mockSingleLink({ request: { query }, result: { data } }), - cache: new InMemoryCache(), - }); - - // query - apollo - .query({ - query, - }) - .subscribe({ - next: result => { - expect(result.loading).toBe(false); - expect(result.data).toMatchObject(data); - setTimeout(() => { - return done(); - }, 3000); - }, - error: e => { - throw e; - }, - }); - })); }); describe('mutate()', () => { @@ -309,7 +268,7 @@ describe('Apollo', () => { const apollo = new Apollo(ngZone); apollo.create({ - link: mockSingleLink(), + link: new MockLink([]), cache: new InMemoryCache(), }); @@ -353,13 +312,13 @@ describe('Apollo', () => { const apollo = new Apollo(ngZone); apollo.create({ - link: mockSingleLink(), + link: new MockLink([]), cache: new InMemoryCache(), }); const client = apollo.client; - client.mutate = vi.fn(options => { + client.mutate = vi.fn((options: { used: boolean }) => { if (options.used) { throw new Error('options was reused'); } @@ -393,7 +352,7 @@ describe('Apollo', () => { const apollo = new Apollo(ngZone); apollo.create({ - link: mockSingleLink(), + link: new MockLink([]), cache: new InMemoryCache(), }); @@ -440,7 +399,7 @@ describe('Apollo', () => { }; apollo.create({ - link: mockSingleLink( + link: new MockLink([ { request: op1, result: { data: data1 }, @@ -449,7 +408,7 @@ describe('Apollo', () => { request: op2, result: { data: data2 }, }, - ), + ]), cache: new InMemoryCache(), }); @@ -472,101 +431,79 @@ describe('Apollo', () => { }); })); - test('should NOT useMutationLoading by default', () => - new Promise(done => { - expect.assertions(2); - const apollo = testBed.inject(Apollo); - const query = gql` - mutation addRandomHero { - addRandomHero { - name - __typename - } + test('should NOT useMutationLoading by default', async () => { + const apollo = testBed.inject(Apollo); + const query = gql` + mutation addRandomHero { + addRandomHero { + name + __typename } - `; - const data = { - addRandomHero: { - name: 'Superman', - __typename: 'Hero', - }, - }; + } + `; + const data = { + addRandomHero: { + name: 'Superman', + __typename: 'Hero', + }, + }; - // create - apollo.create({ - link: mockSingleLink({ request: { query }, result: { data } }), - cache: new InMemoryCache(), - }); + apollo.create({ + link: new MockLink([{ request: { query }, result: { data } }]), + cache: new InMemoryCache(), + }); - // mutation - apollo - .mutate({ - mutation: query, - }) - .subscribe({ - next: result => { - expect(result.loading).toBe(false); - expect(result.data).toMatchObject(data); - setTimeout(() => { - return done(); - }, 3000); - }, - error: e => { - throw e; - }, - }); - })); + const stream = new ObservableStream(apollo.mutate({ mutation: query })); - test('should useMutationLoading on demand', () => - new Promise(done => { - expect.assertions(3); - const apollo = testBed.inject(Apollo); - const query = gql` - mutation addRandomHero { - addRandomHero { - name - __typename - } + await expect(stream.takeNext()).resolves.toEqual({ + data, + loading: false, + }); + + await expect(stream).not.toEmitAnything(); + }); + + test('should useMutationLoading on demand', async () => { + const apollo = testBed.inject(Apollo); + const query = gql` + mutation addRandomHero { + addRandomHero { + name + __typename } - `; - const data = { - addRandomHero: { - name: 'Superman', - __typename: 'Hero', - }, - }; + } + `; + const data = { + addRandomHero: { + name: 'Superman', + __typename: 'Hero', + }, + }; - let alreadyCalled = false; + apollo.create({ + link: new MockLink([{ request: { query }, result: { data } }]), + cache: new InMemoryCache(), + }); - // create - apollo.create({ - link: mockSingleLink({ request: { query }, result: { data } }), - cache: new InMemoryCache(), - }); + const stream = new ObservableStream( + apollo.mutate({ + mutation: query, + useMutationLoading: true, + }), + ); - // mutation - apollo - .mutate({ - mutation: query, - useMutationLoading: true, - }) - .subscribe({ - next: result => { - if (alreadyCalled) { - expect(result.loading).toBe(false); - expect(result.data).toMatchObject(data); - setTimeout(() => { - return done(); - }, 3000); - } else { - expect(result.loading).toBe(true); - alreadyCalled = true; - } - }, - error: e => { - throw e; - }, - }); - })); + await expect(stream.takeNext()).resolves.toEqual({ + data: undefined, + loading: true, + }); + + await expect(stream.takeNext()).resolves.toEqual({ + data, + loading: false, + }); + + await expect(stream).not.toEmitAnything(); + }); }); describe('subscribe', () => { @@ -576,7 +513,7 @@ describe('Apollo', () => { const apollo = new Apollo(ngZone); apollo.create({ - link: mockSingleLink(), + link: new MockLink([]), cache: new InMemoryCache(), }); @@ -609,7 +546,7 @@ describe('Apollo', () => { const apollo = new Apollo(ngZone); apollo.create({ - link: mockSingleLink(), + link: new MockLink([]), cache: new InMemoryCache(), }); @@ -628,7 +565,7 @@ describe('Apollo', () => { const apollo = new Apollo(ngZone); apollo.create({ - link: mockSingleLink(), + link: new MockLink([]), cache: new InMemoryCache(), }); @@ -637,7 +574,7 @@ describe('Apollo', () => { client.subscribe = vi.fn().mockReturnValue(['subscription']); - const obs = apollo.subscribe(options, { useZone: false }); + const obs = apollo.subscribe({ ...options, useZone: false }); const operator = (obs as any).operator; expect(operator).toBeUndefined(); @@ -649,7 +586,7 @@ describe('Apollo', () => { const apollo = new Apollo(ngZone); apollo.create({ - link: mockSingleLink(), + link: new MockLink([]), cache: new InMemoryCache(), }); @@ -677,166 +614,197 @@ describe('Apollo', () => { }); describe('query updates', () => { - test('should update a query after mutation', () => - new Promise(done => { - expect.assertions(3); - const query = gql` - query heroes { - allHeroes { - name - __typename - } + test('should update a query after mutation', async () => { + const query = gql` + query heroes { + allHeroes { + name + __typename } - `; - const mutation = gql` - mutation addHero($name: String!) { - addHero(name: $name) { - name - __typename - } + } + `; + const mutation = gql` + mutation addHero($name: String!) { + addHero(name: $name) { + name + __typename } - `; - const variables = { name: 'Bar' }; - // tslint:disable:variable-name - const __typename = 'Hero'; + } + `; + const variables = { name: 'Bar' }; + // tslint:disable:variable-name + const __typename = 'Hero'; - const FooHero = { name: 'Foo', __typename }; - const BarHero = { name: 'Bar', __typename }; + const FooHero = { name: 'Foo', __typename }; + const BarHero = { name: 'Bar', __typename }; - const data1 = { allHeroes: [FooHero] }; - const dataMutation = { addHero: BarHero }; - const data2 = { allHeroes: [FooHero, BarHero] }; + const data1 = { allHeroes: [FooHero] }; + const dataMutation = { addHero: BarHero }; + const data2 = { allHeroes: [FooHero, BarHero] }; - const link = mockSingleLink( - { - request: { query }, - result: { data: data1 }, - }, - { - request: { query: mutation, variables }, - result: { data: dataMutation }, + const link = new MockLink([ + { + request: { query }, + result: { data: data1 }, + }, + { + request: { query: mutation, variables }, + result: { data: dataMutation }, + }, + ]); + const apollo = mockApollo(link, ngZone); + + const obs = apollo.watchQuery({ query }); + const stream = new ObservableStream(obs.valueChanges); + + await expect(stream.takeNext()).resolves.toEqual({ + data: undefined, + dataState: 'empty', + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + + await expect(stream.takeNext()).resolves.toEqual({ + data: data1, + dataState: 'complete', + loading: false, + networkStatus: NetworkStatus.ready, + partial: false, + }); + + const mutationStream = new ObservableStream( + apollo.mutate({ + mutation, + variables, + updateQueries: { + heroes: (prev: any, { mutationResult }: any) => { + return { + allHeroes: [...prev.allHeroes, mutationResult.data.addHero], + }; + }, }, - ); - const apollo = mockApollo(link, ngZone); - - const obs = apollo.watchQuery({ query }); - - let calls = 0; - obs.valueChanges.subscribe(({ data }) => { - calls++; - - if (calls === 1) { - expect(data).toMatchObject(data1); - - apollo - .mutate({ - mutation, - variables, - updateQueries: { - heroes: (prev: any, { mutationResult }: any) => { - return { - allHeroes: [...prev.allHeroes, mutationResult.data.addHero], - }; - }, - }, - }) - .subscribe({ - next: result => { - expect(result.data.addHero).toMatchObject(BarHero); - }, - error(error) { - throw error.message; - }, - }); - } else if (calls === 2) { - expect(data).toMatchObject(data2); - done(); - } - }); - })); + }), + ); - test('should update a query with Optimistic Response after mutation', () => - new Promise(done => { - expect.assertions(3); - const query = gql` - query heroes { - allHeroes { - id - name - __typename - } + await expect(mutationStream.takeNext()).resolves.toEqual({ + data: { addHero: BarHero }, + loading: false, + }); + + await expect(stream.takeNext()).resolves.toEqual({ + data: data2, + dataState: 'complete', + loading: false, + networkStatus: NetworkStatus.ready, + partial: false, + }); + + await expect(stream).not.toEmitAnything(); + }); + + test('should update a query with Optimistic Response after mutation', async () => { + const query = gql` + query heroes { + allHeroes { + id + name + __typename } - `; - const mutation = gql` - mutation addHero($name: String!) { - addHero(name: $name) { - id - name - __typename - } + } + `; + const mutation = gql` + mutation addHero($name: String!) { + addHero(name: $name) { + id + name + __typename } - `; - const variables = { name: 'Bar' }; - const __typename = 'Hero'; + } + `; + const variables = { name: 'Bar' }; + const __typename = 'Hero'; - const FooHero = { id: 1, name: 'Foo', __typename }; - const BarHero = { id: 2, name: 'Bar', __typename }; - const OptimisticHero = { id: null, name: 'Temp', __typename }; + const FooHero = { id: 1, name: 'Foo', __typename }; + const BarHero = { id: 2, name: 'Bar', __typename }; + const OptimisticHero = { id: null, name: 'Temp', __typename }; - const data1 = { allHeroes: [FooHero] }; - const dataMutation = { addHero: BarHero }; - const data2 = { allHeroes: [FooHero, OptimisticHero] }; - const data3 = { allHeroes: [FooHero, BarHero] }; + const data1 = { allHeroes: [FooHero] }; + const dataMutation = { addHero: BarHero }; + const data2 = { allHeroes: [FooHero, OptimisticHero] }; + const data3 = { allHeroes: [FooHero, BarHero] }; - const link = mockSingleLink( - { - request: { query }, - result: { data: data1 }, + const link = new MockLink([ + { + request: { query }, + result: { data: data1 }, + }, + { + request: { query: mutation, variables }, + result: { data: dataMutation }, + }, + ]); + const apollo = mockApollo(link, ngZone); + + const obs = apollo.watchQuery({ query }); + const stream = new ObservableStream(obs.valueChanges); + + await expect(stream.takeNext()).resolves.toEqual({ + data: undefined, + dataState: 'empty', + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + + await expect(stream.takeNext()).resolves.toEqual({ + data: data1, + dataState: 'complete', + loading: false, + networkStatus: NetworkStatus.ready, + partial: false, + }); + + apollo + .mutate({ + mutation, + variables, + optimisticResponse: { + addHero: OptimisticHero, + }, + updateQueries: { + heroes: (prev: any, { mutationResult }: any) => { + return { + allHeroes: [...prev.allHeroes, mutationResult.data.addHero], + }; + }, }, - { - request: { query: mutation, variables }, - result: { data: dataMutation }, + }) + .subscribe({ + error(error) { + throw error.message; }, - ); - const apollo = mockApollo(link, ngZone); - - const obs = apollo.watchQuery({ query }); - - let calls = 0; - obs.valueChanges.subscribe(({ data }) => { - calls++; - - if (calls === 1) { - expect(data).toMatchObject(data1); - - apollo - .mutate({ - mutation, - variables, - optimisticResponse: { - addHero: OptimisticHero, - }, - updateQueries: { - heroes: (prev: any, { mutationResult }: any) => { - return { - allHeroes: [...prev.allHeroes, mutationResult.data.addHero], - }; - }, - }, - }) - .subscribe({ - error(error) { - throw error.message; - }, - }); - } else if (calls === 2) { - expect(data).toMatchObject(data2); - } else if (calls === 3) { - expect(data).toMatchObject(data3); - done(); - } }); - })); + + // optimistic response + await expect(stream.takeNext()).resolves.toEqual({ + data: data2, + dataState: 'complete', + loading: false, + networkStatus: NetworkStatus.ready, + partial: false, + }); + + await expect(stream.takeNext()).resolves.toEqual({ + data: data3, + dataState: 'complete', + loading: false, + networkStatus: NetworkStatus.ready, + partial: false, + }); + + await expect(stream).not.toEmitAnything(); + }); }); test('should use HttpClient', () => @@ -864,8 +832,8 @@ describe('Apollo', () => { }; // create - apollo.create({ - link: mockSingleLink({ request: op, result: { data } }), + apollo.create({ + link: new MockLink([{ request: op, result: { data } }]), cache: new InMemoryCache(), }); @@ -881,123 +849,6 @@ describe('Apollo', () => { }); })); - test('should useInitialLoading', () => - new Promise(done => { - expect.assertions(3); - const apollo = testBed.inject(Apollo); - const query = gql` - query heroes { - heroes { - name - __typename - } - } - `; - const data = { - heroes: [ - { - name: 'Superman', - __typename: 'Hero', - }, - ], - }; - - let alreadyCalled = false; - - // create - apollo.create({ - link: mockSingleLink({ request: { query }, result: { data } }), - cache: new InMemoryCache(), - }); - - // query - apollo - .watchQuery({ - query, - useInitialLoading: true, - }) - .valueChanges.subscribe({ - next: result => { - if (alreadyCalled) { - expect(result.data).toMatchObject(data); - setTimeout(() => { - return done(); - }, 3000); - } else { - expect(result.loading).toBe(true); - expect(result.networkStatus).toBe(NetworkStatus.loading); - alreadyCalled = true; - } - }, - error: e => { - throw e; - }, - }); - })); - - test('useInitialLoading should emit false once when data is already available', () => - new Promise(done => { - expect.assertions(4); - const apollo = testBed.inject(Apollo); - const query = gql` - query heroes { - heroes { - name - __typename - } - } - `; - const data = { - heroes: [ - { - name: 'Superman', - __typename: 'Hero', - }, - ], - }; - - let calls = 0; - - const cache = new InMemoryCache(); - - cache.writeQuery({ - query: query, - data, - }); - - // create - apollo.create({ - link: mockSingleLink({ request: { query }, result: { data } }), - cache, - }); - - // query - apollo - .watchQuery({ - query, - notifyOnNetworkStatusChange: true, - useInitialLoading: true, - }) - .valueChanges.subscribe({ - next: result => { - calls++; - - if (calls === 1) { - setTimeout(() => { - expect(calls).toEqual(1); - expect(result.loading).toEqual(false); - expect(result.networkStatus).toEqual(NetworkStatus.ready); - expect(result.data).toEqual(data); - return done(); - }, 3000); - } - }, - error: e => { - throw e; - }, - }); - })); - test('should emit cached result only once', () => new Promise(done => { expect.assertions(3); @@ -1029,8 +880,8 @@ describe('Apollo', () => { }); // create - apollo.create({ - link: mockSingleLink({ request: { query }, result: { data } }), + apollo.create({ + link: new MockLink([{ request: { query }, result: { data } }]), cache, }); @@ -1061,11 +912,11 @@ describe('Apollo', () => { test('should create default client with named options', () => { const apollo = new Apollo(ngZone, undefined, { default: { - link: mockSingleLink(), + link: new MockLink([]), cache: new InMemoryCache(), }, test: { - link: mockSingleLink(), + link: new MockLink([]), cache: new InMemoryCache(), }, }); @@ -1075,7 +926,7 @@ describe('Apollo', () => { }); test('should remove default client', () => { - const apollo = mockApollo(mockSingleLink(), ngZone); + const apollo = mockApollo(new MockLink([]), ngZone); expect(apollo.client).toBeDefined(); @@ -1085,10 +936,10 @@ describe('Apollo', () => { }); test('should remove named client', () => { - const apollo = mockApollo(mockSingleLink(), ngZone); + const apollo = mockApollo(new MockLink([]), ngZone); apollo.createNamed('test', { - link: mockSingleLink(), + link: new MockLink([]), cache: new InMemoryCache(), }); diff --git a/packages/apollo-angular/tests/Mutation.spec.ts b/packages/apollo-angular/tests/Mutation.spec.ts index 76a51ac1c..523396534 100644 --- a/packages/apollo-angular/tests/Mutation.spec.ts +++ b/packages/apollo-angular/tests/Mutation.spec.ts @@ -61,7 +61,7 @@ describe('Mutation', () => { }); test('should pass variables to Apollo.mutate', () => { - addHero.mutate({ foo: 1 }); + addHero.mutate({ variables: { foo: 1 } }); expect(apolloMock.mutate).toBeCalled(); expect(apolloMock.mutate.mock.calls[0][0]).toMatchObject({ @@ -70,7 +70,7 @@ describe('Mutation', () => { }); test('should pass options to Apollo.mutate', () => { - addHero.mutate({}, { fetchPolicy: 'no-cache' }); + addHero.mutate({ fetchPolicy: 'no-cache' }); expect(apolloMock.mutate).toBeCalled(); expect(apolloMock.mutate.mock.calls[0][0]).toMatchObject({ @@ -79,7 +79,7 @@ describe('Mutation', () => { }); test('should not overwrite query when options object is provided', () => { - addHero.mutate({}, { query: 'asd', fetchPolicy: 'cache-first' } as any); + addHero.mutate({ mutation: 'asd', fetchPolicy: 'cache-first' } as any); expect(apolloMock.mutate).toBeCalled(); expect(apolloMock.mutate.mock.calls[0][0]).toMatchObject({ diff --git a/packages/apollo-angular/tests/Query.spec.ts b/packages/apollo-angular/tests/Query.spec.ts index 019a1648a..fd0736237 100644 --- a/packages/apollo-angular/tests/Query.spec.ts +++ b/packages/apollo-angular/tests/Query.spec.ts @@ -84,7 +84,7 @@ describe('Query', () => { }); test('should pass variables to Apollo.watchQuery', () => { - heroesQuery.watch({ foo: 1 }); + heroesQuery.watch({ variables: { foo: 1 } }); expect(apolloMock.watchQuery).toBeCalled(); expect(apolloMock.watchQuery.mock.calls[0][0]).toMatchObject({ @@ -93,7 +93,7 @@ describe('Query', () => { }); test('should pass options to Apollo.watchQuery', () => { - heroesQuery.watch({}, { fetchPolicy: 'network-only' }); + heroesQuery.watch({ fetchPolicy: 'network-only' }); expect(apolloMock.watchQuery).toBeCalled(); expect(apolloMock.watchQuery.mock.calls[0][0]).toMatchObject({ @@ -102,7 +102,7 @@ describe('Query', () => { }); test('should not overwrite query when options object is provided', () => { - heroesQuery.watch({}, { query: 'asd', fetchPolicy: 'cache-first' } as any); + heroesQuery.watch({ query: 'asd', fetchPolicy: 'cache-first' } as any); expect(apolloMock.watchQuery).toBeCalled(); expect(apolloMock.watchQuery.mock.calls[0][0]).toMatchObject({ @@ -133,7 +133,7 @@ describe('Query', () => { }); test('should pass variables to Apollo.query', () => { - heroesQuery.fetch({ foo: 1 }); + heroesQuery.fetch({ variables: { foo: 1 } }); expect(apolloMock.query).toBeCalled(); expect(apolloMock.query.mock.calls[0][0]).toMatchObject({ @@ -142,7 +142,7 @@ describe('Query', () => { }); test('should pass options to Apollo.query', () => { - heroesQuery.fetch({}, { fetchPolicy: 'network-only' }); + heroesQuery.fetch({ fetchPolicy: 'network-only' }); expect(apolloMock.query).toBeCalled(); expect(apolloMock.query.mock.calls[0][0]).toMatchObject({ @@ -151,7 +151,7 @@ describe('Query', () => { }); test('should not overwrite query when options object is provided', () => { - heroesQuery.fetch({}, { query: 'asd', fetchPolicy: 'cache-first' } as any); + heroesQuery.fetch({ query: 'asd', fetchPolicy: 'cache-first' } as any); expect(apolloMock.query).toBeCalled(); expect(apolloMock.query.mock.calls[0][0]).toMatchObject({ diff --git a/packages/apollo-angular/tests/QueryRef.spec.ts b/packages/apollo-angular/tests/QueryRef.spec.ts index ba154f59f..a65efd73e 100644 --- a/packages/apollo-angular/tests/QueryRef.spec.ts +++ b/packages/apollo-angular/tests/QueryRef.spec.ts @@ -2,10 +2,17 @@ import { Subject } from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import { NgZone } from '@angular/core'; -import { ApolloClient, ApolloLink, InMemoryCache, ObservableQuery } from '@apollo/client/core'; -import { mockSingleLink } from '@apollo/client/testing'; +import { + ApolloClient, + ApolloLink, + InMemoryCache, + NetworkStatus, + ObservableQuery, +} from '@apollo/client'; +import { MockLink } from '@apollo/client/testing'; import { gql } from '../src/gql'; import { QueryRef } from '../src/query-ref'; +import { ObservableStream } from '../test-utils/ObservableStream'; const createClient = (link: ApolloLink) => new ApolloClient({ @@ -23,7 +30,6 @@ const heroesOperation = { } `, variables: {}, - operationName: 'allHeroes', }; // tslint:disable:variable-name @@ -40,13 +46,13 @@ const Batman = { describe('QueryRef', () => { let ngZone: NgZone; - let client: ApolloClient; + let client: ApolloClient; let obsQuery: ObservableQuery; let queryRef: QueryRef; beforeEach(() => { ngZone = { run: vi.fn(cb => cb()) } as any; - const mockedLink = mockSingleLink( + const mockedLink = new MockLink([ { request: heroesOperation, result: { data: { heroes: [Superman] } }, @@ -55,25 +61,21 @@ describe('QueryRef', () => { request: heroesOperation, result: { data: { heroes: [Superman, Batman] } }, }, - ); + ]); client = createClient(mockedLink); obsQuery = client.watchQuery(heroesOperation); - queryRef = new QueryRef(obsQuery, ngZone, {} as any); + queryRef = new QueryRef(obsQuery, ngZone); }); - test('should listen to changes', () => - new Promise(done => { - queryRef.valueChanges.subscribe({ - next: result => { - expect(result.data).toBeDefined(); - done(); - }, - error: e => { - throw e; - }, - }); - })); + test('should listen to changes', async () => { + const stream = new ObservableStream(queryRef.valueChanges); + + await expect(stream.takeNext()).resolves.toMatchObject({ loading: true }); + + const result = await stream.takeNext(); + expect(result.data).toBeDefined(); + }); test('should be able to call refetch', () => { const mockCallback = vi.fn(); @@ -84,62 +86,59 @@ describe('QueryRef', () => { expect(mockCallback.mock.calls.length).toBe(1); }); - test('should be able refetch and receive new results', () => - new Promise(done => { - let calls = 0; - - queryRef.valueChanges.subscribe({ - next: result => { - calls++; - - expect(result.data).toBeDefined(); - - if (calls === 2) { - done(); - } - }, - error: e => { - throw e; - }, - complete: () => { - throw 'Should not be here'; - }, - }); + test('should be able refetch and receive new results', async () => { + const stream = new ObservableStream(queryRef.valueChanges); + + await expect(stream.takeNext()).resolves.toEqual({ + data: undefined, + dataState: 'empty', + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + + await expect(stream.takeNext()).resolves.toEqual({ + data: { heroes: [Superman] }, + dataState: 'complete', + loading: false, + networkStatus: NetworkStatus.ready, + partial: false, + }); - setTimeout(() => { - queryRef.refetch(); - }, 200); - })); + queryRef.refetch(); - test('should be able refetch and receive new results after using rxjs operator', () => - new Promise(done => { - let calls = 0; - const obs = queryRef.valueChanges; + await expect(stream.takeNext()).resolves.toEqual({ + data: { heroes: [Superman] }, + dataState: 'complete', + loading: true, + networkStatus: NetworkStatus.refetch, + partial: false, + }); + + await expect(stream.takeNext()).resolves.toEqual({ + data: { heroes: [Superman, Batman] }, + dataState: 'complete', + loading: false, + networkStatus: NetworkStatus.ready, + partial: false, + }); + + await expect(stream).not.toEmitAnything(); + }); - obs.pipe(map(result => result.data)).subscribe({ - next: result => { - calls++; + test('should be able refetch and receive new results after using rxjs operator', async () => { + const obs = queryRef.valueChanges.pipe(map(result => result.data)); + const stream = new ObservableStream(obs); - if (calls === 1) { - expect(result.heroes.length).toBe(1); - } else if (calls === 2) { - expect(result.heroes.length).toBe(2); + await expect(stream.takeNext()).resolves.toBeUndefined(); + await expect(stream.takeNext()).resolves.toEqual({ heroes: [Superman] }); - done(); - } - }, - error: e => { - throw e; - }, - complete: () => { - throw 'Should not be here'; - }, - }); + queryRef.refetch(); - setTimeout(() => { - queryRef.refetch(); - }, 200); - })); + await expect(stream.takeNext()).resolves.toEqual({ heroes: [Superman] }); + await expect(stream.takeNext()).resolves.toEqual({ heroes: [Superman, Batman] }); + await expect(stream).not.toEmitAnything(); + }); test('should be able to call updateQuery()', () => { const mockCallback = vi.fn(); @@ -152,72 +151,68 @@ describe('QueryRef', () => { expect(mockCallback.mock.calls[0][0]).toBe(mapFn); }); - test('should be able to call result()', () => { - const mockCallback = vi.fn(); - obsQuery.result = mockCallback.mockReturnValue('expected'); + test('should be able to call getCurrentResult() and get updated results', async () => { + const stream = new ObservableStream(queryRef.valueChanges); - const result = queryRef.result(); + { + const result = await stream.takeNext(); + const currentResult = queryRef.getCurrentResult(); - expect(result).toBe('expected'); - expect(mockCallback.mock.calls.length).toBe(1); - }); - - test('should be able to call getCurrentResult() and get updated results', () => - new Promise(done => { - let calls = 0; - const obs = queryRef.valueChanges; - - obs.pipe(map(result => result.data)).subscribe({ - next: result => { - calls++; - const currentResult = queryRef.getCurrentResult(); - expect(currentResult.data.heroes.length).toBe(result.heroes.length); - - if (calls === 2) { - done(); - } - }, - error: e => { - throw e; - }, - complete: () => { - throw 'Should not be here'; - }, + expect(currentResult).toEqual(result); + expect(currentResult).toEqual({ + data: undefined, + dataState: 'empty', + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, }); + } - setTimeout(() => { - queryRef.refetch(); - }, 200); - })); - - test('should be able to call getLastResult()', () => { - const mockCallback = vi.fn(); - obsQuery.getLastResult = mockCallback.mockReturnValue('expected'); - - const result = queryRef.getLastResult(); - - expect(result).toBe('expected'); - expect(mockCallback.mock.calls.length).toBe(1); - }); - - test('should be able to call getLastError()', () => { - const mockCallback = vi.fn(); - obsQuery.getLastError = mockCallback.mockReturnValue('expected'); - - const result = queryRef.getLastError(); + { + const result = await stream.takeNext(); + const currentResult = queryRef.getCurrentResult(); + + expect(currentResult).toEqual(result); + expect(currentResult).toEqual({ + data: { heroes: [Superman] }, + dataState: 'complete', + loading: false, + networkStatus: NetworkStatus.ready, + partial: false, + }); + } - expect(result).toBe('expected'); - expect(mockCallback.mock.calls.length).toBe(1); - }); + queryRef.refetch(); - test('should be able to call resetLastResults()', () => { - const mockCallback = vi.fn(); - obsQuery.resetLastResults = mockCallback.mockReturnValue('expected'); + { + const result = await stream.takeNext(); + const currentResult = queryRef.getCurrentResult(); + + expect(currentResult).toEqual(result); + expect(currentResult).toEqual({ + data: { heroes: [Superman] }, + dataState: 'complete', + loading: true, + networkStatus: NetworkStatus.refetch, + partial: false, + }); + } - const result = queryRef.resetLastResults(); + { + const result = await stream.takeNext(); + const currentResult = queryRef.getCurrentResult(); + + expect(currentResult).toEqual(result); + expect(currentResult).toEqual({ + data: { heroes: [Superman, Batman] }, + dataState: 'complete', + loading: false, + networkStatus: NetworkStatus.ready, + partial: false, + }); + } - expect(result).toBe('expected'); - expect(mockCallback.mock.calls.length).toBe(1); + await expect(stream).not.toEmitAnything(); }); test('should be able to call fetchMore()', () => { @@ -262,18 +257,6 @@ describe('QueryRef', () => { expect(mockCallback.mock.calls[0][0]).toBe(3000); }); - test('should be able to call setOptions()', () => { - const mockCallback = vi.fn(); - const opts = {}; - obsQuery.setOptions = mockCallback.mockReturnValue('expected'); - - const result = queryRef.setOptions(opts); - - expect(result).toBe('expected'); - expect(mockCallback.mock.calls.length).toBe(1); - expect(mockCallback.mock.calls[0][0]).toBe(opts); - }); - test('should be able to call setVariables()', () => { const mockCallback = vi.fn(); const variables = {}; @@ -300,7 +283,8 @@ describe('QueryRef', () => { next: result => { calls.first++; - expect(result.data).toBeDefined(); + // Initial loading state + expect(result.data).not.toBeDefined(); }, error: e => { throw e; @@ -314,7 +298,8 @@ describe('QueryRef', () => { next: result => { calls.second++; - expect(result.data).toBeDefined(); + // Initial loading state + expect(result.data).not.toBeDefined(); setTimeout(() => { subSecond.unsubscribe(); @@ -346,17 +331,16 @@ describe('QueryRef', () => { test('should unsubscribe', () => new Promise(done => { const obs = queryRef.valueChanges; - const id = queryRef.queryId; const sub = obs.subscribe(() => { // }); - expect(client['queryManager'].queries.get(id)).toBeDefined(); + expect(client.getObservableQueries().size).toBe(1); setTimeout(() => { sub.unsubscribe(); - expect(client['queryManager'].queries.get(id)).toBeUndefined(); + expect(client.getObservableQueries().size).toBe(0); done(); }); })); @@ -365,17 +349,16 @@ describe('QueryRef', () => { new Promise(done => { const gate = new Subject(); const obs = queryRef.valueChanges.pipe(takeUntil(gate)); - const id = queryRef.queryId; obs.subscribe(() => { // }); - expect(client['queryManager'].queries.get(id)).toBeDefined(); + expect(client.getObservableQueries().size).toBe(1); gate.next(); - expect(client['queryManager'].queries.get(id)).toBeUndefined(); + expect(client.getObservableQueries().size).toBe(0); done(); })); }); diff --git a/packages/apollo-angular/tests/Subscription.spec.ts b/packages/apollo-angular/tests/Subscription.spec.ts index 4559b6f44..64903d1f5 100644 --- a/packages/apollo-angular/tests/Subscription.spec.ts +++ b/packages/apollo-angular/tests/Subscription.spec.ts @@ -60,7 +60,7 @@ describe('Subscription', () => { }); test('should pass variables to Apollo.subscribe', () => { - heroes.subscribe({ foo: 1 }); + heroes.subscribe({ variables: { foo: 1 } }); expect(apolloMock.subscribe).toBeCalled(); expect(apolloMock.subscribe.mock.calls[0][0]).toMatchObject({ @@ -69,7 +69,7 @@ describe('Subscription', () => { }); test('should pass options to Apollo.subscribe', () => { - heroes.subscribe({}, { fetchPolicy: 'network-only' }); + heroes.subscribe({ fetchPolicy: 'network-only' }); expect(apolloMock.subscribe).toBeCalled(); expect(apolloMock.subscribe.mock.calls[0][0]).toMatchObject({ @@ -78,7 +78,7 @@ describe('Subscription', () => { }); test('should not overwrite query when options object is provided', () => { - heroes.subscribe({}, { query: 'asd', fetchPolicy: 'cache-first' } as any); + heroes.subscribe({ query: 'asd', fetchPolicy: 'cache-first' } as any); expect(apolloMock.subscribe).toBeCalled(); expect(apolloMock.subscribe.mock.calls[0][0]).toMatchObject({ diff --git a/packages/apollo-angular/tests/integration.spec.ts b/packages/apollo-angular/tests/integration.spec.ts index 88170abe0..b07264c32 100644 --- a/packages/apollo-angular/tests/integration.spec.ts +++ b/packages/apollo-angular/tests/integration.spec.ts @@ -1,8 +1,8 @@ import { beforeEach, describe, expect, test } from 'vitest'; import { provideHttpClient } from '@angular/common/http'; import { TestBed } from '@angular/core/testing'; -import { InMemoryCache } from '@apollo/client/core'; -import { mockSingleLink } from '@apollo/client/testing'; +import { InMemoryCache } from '@apollo/client'; +import { MockLink } from '@apollo/client/testing'; import { Apollo, provideApollo } from '../src'; describe('Integration', () => { @@ -13,7 +13,7 @@ describe('Integration', () => { provideHttpClient(), provideApollo(() => { return { - link: mockSingleLink(), + link: new MockLink([]), cache: new InMemoryCache(), }; }), diff --git a/packages/apollo-angular/tests/test-setup.ts b/packages/apollo-angular/tests/test-setup.ts index b28aa1910..2e2d02241 100644 --- a/packages/apollo-angular/tests/test-setup.ts +++ b/packages/apollo-angular/tests/test-setup.ts @@ -1,4 +1,5 @@ import '@analogjs/vitest-angular/setup-zone'; +import '../test-utils/matchers'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, diff --git a/packages/apollo-angular/tests/vitest.d.ts b/packages/apollo-angular/tests/vitest.d.ts new file mode 100644 index 000000000..de05459d6 --- /dev/null +++ b/packages/apollo-angular/tests/vitest.d.ts @@ -0,0 +1,11 @@ +import 'vitest'; +import type { TakeOptions } from '../test-utils/ObservableStream'; + +interface CustomMatchers { + toEmitAnything: (options?: TakeOptions) => Promise; +} + +declare module 'vitest' { + interface Assertion extends CustomMatchers {} + interface AsymmetricMatchersContaining extends CustomMatchers {} +} diff --git a/packages/apollo-angular/tsconfig.json b/packages/apollo-angular/tsconfig.json index 8bc35a98b..ddb43ee39 100644 --- a/packages/apollo-angular/tsconfig.json +++ b/packages/apollo-angular/tsconfig.json @@ -3,10 +3,8 @@ "compilerOptions": { "rootDir": ".", "baseUrl": ".", - "outDir": "build" - }, - "angularCompilerOptions": { - "enableIvy": false + "outDir": "build", + "inlineSources": true }, "include": [ "headers/**/*.ts", @@ -14,6 +12,7 @@ "persisted-queries/**/*.ts", "src/**/*.ts", "testing/**/*.ts", - "tests/**/*.ts" + "tests/**/*.ts", + "test-utils/**/*.ts" ] } diff --git a/packages/demo/src/app/app.config.ts b/packages/demo/src/app/app.config.ts index 8dec6e286..03ce48c66 100644 --- a/packages/demo/src/app/app.config.ts +++ b/packages/demo/src/app/app.config.ts @@ -3,7 +3,7 @@ import { HttpLink } from 'apollo-angular/http'; import { provideHttpClient } from '@angular/common/http'; import { ApplicationConfig, inject, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; -import { InMemoryCache } from '@apollo/client/core'; +import { InMemoryCache } from '@apollo/client'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { diff --git a/packages/demo/src/app/pages/movie/movie-page.component.ts b/packages/demo/src/app/pages/movie/movie-page.component.ts index 59c7c7291..813a183a9 100644 --- a/packages/demo/src/app/pages/movie/movie-page.component.ts +++ b/packages/demo/src/app/pages/movie/movie-page.component.ts @@ -1,8 +1,8 @@ import { Apollo, gql } from 'apollo-angular'; import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { filter, map } from 'rxjs/operators'; import { AsyncPipe } from '@angular/common'; -import { Component, OnInit } from '@angular/core'; +import { Component, inject, OnInit } from '@angular/core'; import { ActivatedRoute, RouterLink } from '@angular/router'; interface Character { @@ -21,6 +21,10 @@ interface Query { film: Film; } +interface Variables { + id: string; +} + @Component({ selector: 'author-page', template: ` @@ -43,21 +47,13 @@ interface Query { }) export class MoviePageComponent implements OnInit { film$!: Observable; - - constructor( - private readonly apollo: Apollo, - private readonly route: ActivatedRoute, - ) {} + private readonly apollo = inject(Apollo); + private readonly route = inject(ActivatedRoute); ngOnInit() { this.film$ = this.apollo - .watchQuery< - Query, - { - id: string; - } - >({ - query: gql` + .watchQuery({ + query: gql` query FilmCharacters($id: ID) { film(id: $id) { title @@ -74,6 +70,9 @@ export class MoviePageComponent implements OnInit { id: this.route.snapshot.paramMap.get('id')!, }, }) - .valueChanges.pipe(map(result => result.data.film)); + .valueChanges.pipe( + map(result => (result.dataState === 'complete' ? result.data.film : null)), + filter(Boolean), + ); } } diff --git a/packages/demo/src/app/pages/movies/movies-page.component.ts b/packages/demo/src/app/pages/movies/movies-page.component.ts index 86bb00e83..e7e3e3e9d 100644 --- a/packages/demo/src/app/pages/movies/movies-page.component.ts +++ b/packages/demo/src/app/pages/movies/movies-page.component.ts @@ -1,8 +1,8 @@ import { Apollo, gql } from 'apollo-angular'; import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { filter, map } from 'rxjs/operators'; import { AsyncPipe } from '@angular/common'; -import { Component, OnInit } from '@angular/core'; +import { Component, inject, OnInit } from '@angular/core'; import { RouterLink } from '@angular/router'; interface Film { @@ -16,6 +16,8 @@ interface Query { allFilms: { films: Film[] }; } +type Variables = Record; + @Component({ selector: 'movies-page', template: ` @@ -37,13 +39,12 @@ interface Query { }) export class MoviesPageComponent implements OnInit { films$!: Observable; - - constructor(private readonly apollo: Apollo) {} + private readonly apollo = inject(Apollo); ngOnInit() { this.films$ = this.apollo - .watchQuery({ - query: gql` + .watchQuery({ + query: gql` query AllFilms { allFilms { films { @@ -56,6 +57,9 @@ export class MoviesPageComponent implements OnInit { } `, }) - .valueChanges.pipe(map(result => result.data.allFilms.films)) as any; + .valueChanges.pipe( + map(result => (result.dataState == 'complete' ? result.data.allFilms.films : null)), + filter(Boolean), + ); } } diff --git a/scripts/prepare-e2e.js b/scripts/prepare-e2e.js index 290aefe43..d5994d1f1 100755 --- a/scripts/prepare-e2e.js +++ b/scripts/prepare-e2e.js @@ -7,18 +7,20 @@ const cwd = process.cwd(); const [, , name, version] = process.argv; function updateComponent() { - let filepath = - [ - path.join(cwd, `./${name}/src/app/app.component.ts`), - path.join(cwd, `./${name}/src/app/app.ts`), - ].find(path => fs.existsSync(path)); + let filepath = path.join(cwd, `./${name}/src/app/app.component.ts`); + let suffix = 'Component'; + + if (!fs.existsSync(filepath)) { + filepath = path.join(cwd, `./${name}/src/app/app.ts`); + suffix = ''; + } const code = `import { Apollo } from 'apollo-angular';\n` + `import { versionInfo } from 'graphql';\n` + fs .readFileSync(filepath, 'utf8') - .replace('AppComponent {', 'AppComponent { constructor(private readonly apollo: Apollo) {}') + + .replace(`App${suffix} {`, `App${suffix} { constructor(private readonly apollo: Apollo) {}`) + `\n (window as any).GRAPHQL_VERSION = versionInfo.major;`; fs.writeFileSync(filepath, code, 'utf8'); diff --git a/tsconfig.json b/tsconfig.json index 13df0b58d..77d1bc456 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,27 +2,23 @@ "compileOnSave": false, "compilerOptions": { "target": "ES2022", - "module": "esnext", - "moduleResolution": "node", + "module": "preserve", "importHelpers": true, - "inlineSources": true, "sourceMap": true, "declaration": true, "outDir": "build", - "lib": ["es6", "dom", "esnext.asynciterable"], - "downlevelIteration": true, "experimentalDecorators": true, - "noImplicitAny": true, + "noImplicitOverride": true, + "noImplicitReturns": true, "noUnusedLocals": true, "noUnusedParameters": true, - "strictNullChecks": true, "strict": true }, "angularCompilerOptions": { - "enableResourceInlining": true, - "fullTemplateTypeCheck": true, - "skipTemplateCodegen": true, "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "typeCheckHostBindings": true, + "strictTemplates": true, "strictMetadataEmit": true } } diff --git a/website/src/pages/docs/caching/advanced-topics.mdx b/website/src/pages/docs/caching/advanced-topics.mdx index 41dc021e5..f781d8c5f 100644 --- a/website/src/pages/docs/caching/advanced-topics.mdx +++ b/website/src/pages/docs/caching/advanced-topics.mdx @@ -314,7 +314,7 @@ a different query, Apollo Client doesn't know that. To tell Apollo Client where existing `book` data, we can define a field policy `read` function for the `book` field: ```ts -import { InMemoryCache } from '@apollo/client/core'; +import { InMemoryCache } from '@apollo/client'; const cache = new InMemoryCache({ typePolicies: { @@ -383,7 +383,7 @@ write to the cache with a short configurable debounce interval. ```ts import { LocalStorageWrapper, persistCacheSync } from 'apollo3-cache-persist'; -import { InMemoryCache } from '@apollo/client/core'; +import { InMemoryCache } from '@apollo/client'; const cache = new InMemoryCache(); diff --git a/website/src/pages/docs/caching/configuration.mdx b/website/src/pages/docs/caching/configuration.mdx index 1dcb2a3b6..5caca1672 100644 --- a/website/src/pages/docs/caching/configuration.mdx +++ b/website/src/pages/docs/caching/configuration.mdx @@ -28,7 +28,7 @@ Create an `InMemoryCache` object and provide to Apollo options, like so: import { provideApollo } from 'apollo-angular'; import { HttpLink } from 'apollo-angular/http'; import { inject } from '@angular/core'; -import { InMemoryCache } from '@apollo/client/core'; +import { InMemoryCache } from '@apollo/client'; provideApollo(() => { const httpLink = inject(HttpLink); @@ -61,7 +61,6 @@ object supports the following fields: | Name | Type | Description | | ----------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `addTypename` | boolean | If `true`, the cache automatically adds `__typename` fields to all outgoing queries, removing the need to add them manually.
Default: `true` | | `resultCaching` | boolean | If `true`, the cache returns an identical (`===`) response object for every execution of the same query, as long as the underlying data remains unchanged. This makes it easier to detect changes to a query's result.
Default: `true` | | `possibleTypes` | `{ [supertype: string]: string[] }` | Include this object to define polymorphic relationships between your schema's types. Doing so enables you to look up cached data by interface or by union.
The key for each entry is the `__typename` of an interface or union, and the value is an array of the `__typename`s of the types that either belong to the corresponding union or implement the corresponding interface. | | `typePolicies` | `{ [typename: string]: TypePolicy }` | Include this object to customize the cache's behavior on a type-by-type basis.
The key for each entry is a type's `__typename`. For details, see [`TypePolicy` fields](#typepolicy-fields). | @@ -168,7 +167,7 @@ If you need to define a single fallback `keyFields` function that isn't specific `__typename`, you can use the `dataIdFromObject` function that was introduced in Apollo Client 2.x: ```ts -import { defaultDataIdFromObject } from '@apollo/client/core'; +import { defaultDataIdFromObject } from '@apollo/client'; const cache = new InMemoryCache({ dataIdFromObject(responseObject) { diff --git a/website/src/pages/docs/data/mutations.mdx b/website/src/pages/docs/data/mutations.mdx index 1a37c5e72..c477146c6 100644 --- a/website/src/pages/docs/data/mutations.mdx +++ b/website/src/pages/docs/data/mutations.mdx @@ -258,7 +258,7 @@ can enable `useMutationLoading` flag in configuration. import { provideApollo } from 'apollo-angular'; import { HttpLink } from 'apollo-angular/http'; import { inject } from '@angular/core'; -import { InMemoryCache } from '@apollo/client/core'; +import { InMemoryCache } from '@apollo/client'; provideApollo( () => { diff --git a/website/src/pages/docs/data/network.mdx b/website/src/pages/docs/data/network.mdx index 4afcf37f1..429bf1520 100644 --- a/website/src/pages/docs/data/network.mdx +++ b/website/src/pages/docs/data/network.mdx @@ -24,7 +24,7 @@ easier testing. import { provideApollo } from 'apollo-angular'; import { HttpLink } from 'apollo-angular/http'; import { inject } from '@angular/core'; -import { InMemoryCache } from '@apollo/client/core'; +import { InMemoryCache } from '@apollo/client'; provideApollo(() => { const httpLink = inject(HttpLink); @@ -96,7 +96,7 @@ apollo.query({ import { provideApollo } from 'apollo-angular'; import { HttpLink } from 'apollo-angular/http'; import { inject } from '@angular/core'; -import { InMemoryCache } from '@apollo/client/core'; +import { InMemoryCache } from '@apollo/client'; provideApollo(() => { const httpLink = inject(HttpLink); @@ -128,7 +128,7 @@ import { HttpLink } from 'apollo-angular/http'; import extractFiles from 'extract-files/extractFiles.mjs'; import isExtractableFile from 'extract-files/isExtractableFile.mjs'; import { inject } from '@angular/core'; -import { InMemoryCache } from '@apollo/client/core'; +import { InMemoryCache } from '@apollo/client'; provideApollo(() => { const httpLink = inject(HttpLink); @@ -162,7 +162,7 @@ import { provideApollo } from 'apollo-angular'; import { HttpLink } from 'apollo-angular/http'; import { HttpHeaders } from '@angular/common/http'; import { inject } from '@angular/core'; -import { ApolloLink, InMemoryCache } from '@apollo/client/core'; +import { ApolloLink, InMemoryCache } from '@apollo/client'; provideApollo(() => { const httpLink = inject(HttpLink); @@ -194,7 +194,7 @@ provideApollo(() => { import { provideApollo } from 'apollo-angular'; import { HttpLink } from 'apollo-angular/http'; import { inject } from '@angular/core'; -import { InMemoryCache } from '@apollo/client/core'; +import { InMemoryCache } from '@apollo/client'; import { onError } from '@apollo/client/link/error'; provideApollo(() => { @@ -226,7 +226,7 @@ An Apollo Link to combine multiple GraphQL operations into single HTTP request. import { provideApollo } from 'apollo-angular'; import { HttpBatchLink } from 'apollo-angular/http'; import { inject } from '@angular/core'; -import { InMemoryCache } from '@apollo/client/core'; +import { InMemoryCache } from '@apollo/client'; provideApollo(() => { const httpBatchLink = inject(HttpBatchLink); diff --git a/website/src/pages/docs/data/pagination.mdx b/website/src/pages/docs/data/pagination.mdx index 176e0b7f5..251f4cb15 100644 --- a/website/src/pages/docs/data/pagination.mdx +++ b/website/src/pages/docs/data/pagination.mdx @@ -34,7 +34,7 @@ field policy for every relevant list field. This example uses `offsetLimitPagination` to generate a field policy for `Query.posts`: ```typescript -import { InMemoryCache } from '@apollo/client/core'; +import { InMemoryCache } from '@apollo/client'; import { offsetLimitPagination } from '@apollo/client/utilities'; const cache = new InMemoryCache({ @@ -197,7 +197,7 @@ class FeedComponent { ``` ```ts -import { InMemoryCache } from '@apollo/client/core'; +import { InMemoryCache } from '@apollo/client'; const cache = new InMemoryCache({ typePolicies: { diff --git a/website/src/pages/docs/data/queries.mdx b/website/src/pages/docs/data/queries.mdx index ae7c044fd..105bd0273 100644 --- a/website/src/pages/docs/data/queries.mdx +++ b/website/src/pages/docs/data/queries.mdx @@ -369,30 +369,31 @@ to render partial results. ## Loading State -Every response you get from `Apollo.watchQuery()` contains `loading` property. By default, it's -always `false` and the first result is emitted with the response from the ApolloLink execution -chain. In order to correct it you can enable `useInitialLoading` flag in configuration. +Every response you get from `Apollo.watchQuery()` contains the `loading` property. The option +[`notifyOnNetworkStatusChange`](https://www.apollographql.com/docs/react/data/queries#queryhookoptions-interface-notifyonnetworkstatuschange) +defaults to `true`. That means the observable will emit immediately and synchronously with +`loading: true` and `data: undefined`. When the XHR is completed, the observable will emit with +`loading: false` and with a defined `result`. + +If you prefer to be notified only when result is available, and you are not interested in loading +state, then you can configure `notifyOnNetworkStatusChange` to `false`, globally when constructing +`ApolloClient` of for a specific query only. Something like this: ```ts filename="app.config.ts" import { provideApollo } from 'apollo-angular'; import { HttpLink } from 'apollo-angular/http'; import { inject } from '@angular/core'; -import { InMemoryCache } from '@apollo/client/core'; - -provideApollo( - () => { - return { - link: inject(HttpLink).create({ uri: '/graphql' }), - cache: new InMemoryCache(), - }; - }, - { - useInitialLoading: true, // enable it here - }, -); +import { InMemoryCache } from '@apollo/client'; + +provideApollo(() => { + return { + link: inject(HttpLink).create({ uri: '/graphql' }), + cache: new InMemoryCache(), + defaultOptions: { + watchQuery: { + notifyOnNetworkStatusChange: false, + }, + }, + }; +}); ``` - - - `useInitialLoading` is disabled to avoid any breaking changes, this may be enabled in next major - version. - diff --git a/website/src/pages/docs/data/subscriptions.mdx b/website/src/pages/docs/data/subscriptions.mdx index ed4abf7e1..92f2d6999 100644 --- a/website/src/pages/docs/data/subscriptions.mdx +++ b/website/src/pages/docs/data/subscriptions.mdx @@ -105,7 +105,7 @@ import { HttpLink } from 'apollo-angular/http'; import { Kind, OperationTypeNode } from 'graphql'; import { createClient } from 'graphql-ws'; import { inject } from '@angular/core'; -import { InMemoryCache, split } from '@apollo/client/core'; +import { InMemoryCache, split } from '@apollo/client'; import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; import { getMainDefinition } from '@apollo/client/utilities'; diff --git a/website/src/pages/docs/development-and-testing/client-schema-mocking.mdx b/website/src/pages/docs/development-and-testing/client-schema-mocking.mdx index b43290ca3..dd992fe9b 100644 --- a/website/src/pages/docs/development-and-testing/client-schema-mocking.mdx +++ b/website/src/pages/docs/development-and-testing/client-schema-mocking.mdx @@ -30,7 +30,7 @@ data: import { provideApollo } from 'apollo-angular'; import { HttpLink } from 'apollo-angular/http'; import { inject } from '@angular/core'; -import { InMemoryCache } from '@apollo/client/core'; +import { InMemoryCache } from '@apollo/client'; const typeDefs = gql` extend type Rocket { diff --git a/website/src/pages/docs/development-and-testing/testing.mdx b/website/src/pages/docs/development-and-testing/testing.mdx index f12d5f2e2..d49060021 100644 --- a/website/src/pages/docs/development-and-testing/testing.mdx +++ b/website/src/pages/docs/development-and-testing/testing.mdx @@ -292,18 +292,13 @@ test('expect to call clientA', () => { ## Using a custom cache -By default, every ApolloCache is created with these options: - -```json -{ - "addTypename": false -} -``` - -If you would like to change it in the default client, do the following: +A `InMemoryCache` is provided by default. If you would like to change it in the default client, you +can provide the `APOLLO_TESTING_CACHE` token: ```ts -import { APOLLO_TESTING_CACHE } from 'apollo-angular/testing'; +import { APOLLO_TESTING_CACHE, ApolloTestingModule } from 'apollo-angular/testing'; +import { TestBed } from '@angular/core/testing'; +import { InMemoryCache } from '@apollo/client'; beforeEach(() => { TestBed.configureTestingModule({ @@ -311,9 +306,9 @@ beforeEach(() => { providers: [ { provide: APOLLO_TESTING_CACHE, - useValue: { - addTypename: true, - }, + useValue: new InMemoryCache({ + // Custom options here... + }), }, ], }); @@ -322,10 +317,12 @@ beforeEach(() => { }); ``` -For named clients: +And for named clients: ```ts -import { APOLLO_TESTING_NAMED_CACHE } from 'apollo-angular/testing'; +import { APOLLO_TESTING_NAMED_CACHE, ApolloTestingModule } from 'apollo-angular/testing'; +import { TestBed } from '@angular/core/testing'; +import { InMemoryCache } from '@apollo/client'; beforeEach(() => { TestBed.configureTestingModule({ @@ -334,12 +331,12 @@ beforeEach(() => { { provide: APOLLO_TESTING_NAMED_CACHE, useValue: { - clientA: { - addTypename: true, - }, - clientB: { - addTypename: true, - }, + clientA: new InMemoryCache({ + // Custom options for client A here... + }), + clientB: new InMemoryCache({ + // Custom options for client B here... + }), }, }, ], diff --git a/website/src/pages/docs/get-started.mdx b/website/src/pages/docs/get-started.mdx index 78c9358a6..99f018c1b 100644 --- a/website/src/pages/docs/get-started.mdx +++ b/website/src/pages/docs/get-started.mdx @@ -33,18 +33,6 @@ npm i apollo-angular @apollo/client graphql - `apollo-angular`: Bridge between Angular and Apollo Client - `graphql`: Second most important package -The `@apollo/client` package requires `AsyncIterable` so make sure your `tsconfig.json` includes -`ES2020` or later: - -```jsonc filename="tsconfig.json" /"es2020"/ -{ - "compilerOptions": { - // ... - "lib": ["es2020", "dom"], - }, -} -``` - Great, now that you have all the dependencies you need, let's create your first Apollo Client. In `app.config.ts` file, provide Apollo with some options: @@ -54,7 +42,7 @@ import { provideApollo } from 'apollo-angular'; import { HttpLink } from 'apollo-angular/http'; import { provideHttpClient } from '@angular/common/http'; import { ApplicationConfig, inject } from '@angular/core'; -import { InMemoryCache } from '@apollo/client/core'; +import { InMemoryCache } from '@apollo/client'; export const appConfig: ApplicationConfig = { providers: [ @@ -76,7 +64,7 @@ Take a closer look what we did there: 1. With `apollo-angular/http` and `HttpLink` service we connect our client to an external GraphQL Server -1. Thanks to `@apollo/client/core` and `InMemoryCache` we have a place to store data in +1. Thanks to `@apollo/client` and `InMemoryCache` we have a place to store data in ## Links and Cache diff --git a/website/src/pages/docs/performance/server-side-rendering.mdx b/website/src/pages/docs/performance/server-side-rendering.mdx index 8a75a494b..ad5dd3499 100644 --- a/website/src/pages/docs/performance/server-side-rendering.mdx +++ b/website/src/pages/docs/performance/server-side-rendering.mdx @@ -28,7 +28,7 @@ import { provideApollo } from 'apollo-angular'; import { HttpLink } from 'apollo-angular/http'; import { provideHttpClient } from '@angular/common/http'; import { ApplicationConfig, inject, InjectionToken } from '@angular/core'; -import { InMemoryCache } from '@apollo/client/core'; +import { InMemoryCache } from '@apollo/client'; const MY_APOLLO_CACHE = new InjectionToken('apollo-cache'); @@ -84,7 +84,7 @@ import { makeStateKey, TransferState, } from '@angular/core'; -import { InMemoryCache, NormalizedCacheObject } from '@apollo/client/core'; +import { InMemoryCache, NormalizedCacheObject } from '@apollo/client'; const MY_APOLLO_CACHE = new InjectionToken('apollo-cache'); const STATE_KEY = makeStateKey('apollo.state'); diff --git a/website/src/pages/docs/recipes/authentication.mdx b/website/src/pages/docs/recipes/authentication.mdx index 3ca48b72b..f5800684a 100644 --- a/website/src/pages/docs/recipes/authentication.mdx +++ b/website/src/pages/docs/recipes/authentication.mdx @@ -24,7 +24,7 @@ backend, it is very easy to tell your network interface to send the cookie along import { provideApollo } from 'apollo-angular'; import { HttpLink } from 'apollo-angular/http'; import { inject } from '@angular/core'; -import { InMemoryCache } from '@apollo/client/core'; +import { InMemoryCache } from '@apollo/client'; provideApollo(() => { const httpLink = inject(HttpLink); @@ -60,7 +60,7 @@ pull the login token from `localStorage` every time a request is sent. import { provideApollo } from 'apollo-angular'; import { HttpLink } from 'apollo-angular/http'; import { inject } from '@angular/core'; -import { ApolloLink, InMemoryCache } from '@apollo/client/core'; +import { ApolloLink, InMemoryCache } from '@apollo/client'; import { setContext } from '@apollo/client/link/context'; provideApollo(() => { diff --git a/website/src/pages/docs/recipes/automatic-persisted-queries.mdx b/website/src/pages/docs/recipes/automatic-persisted-queries.mdx index 7dcea5c8e..5ea4a2105 100644 --- a/website/src/pages/docs/recipes/automatic-persisted-queries.mdx +++ b/website/src/pages/docs/recipes/automatic-persisted-queries.mdx @@ -58,7 +58,7 @@ import { HttpLink } from 'apollo-angular/http'; import { createPersistedQueryLink } from 'apollo-angular/persisted-queries'; import { sha256 } from 'crypto-hash'; import { inject } from '@angular/core'; -import { InMemoryCache } from '@apollo/client/core'; +import { InMemoryCache } from '@apollo/client'; provideApollo(() => { const httpLink = inject(HttpLink); diff --git a/website/src/pages/docs/recipes/multiple-clients.mdx b/website/src/pages/docs/recipes/multiple-clients.mdx index e7a98c890..c0dc8c074 100644 --- a/website/src/pages/docs/recipes/multiple-clients.mdx +++ b/website/src/pages/docs/recipes/multiple-clients.mdx @@ -58,7 +58,7 @@ In our `app.config.ts` file use `provideNamedApollo()` token to configure Apollo import { provideNamedApollo } from 'apollo-angular'; import { HttpLink } from 'apollo-angular/http'; import { inject } from '@angular/core'; -import { InMemoryCache } from '@apollo/client/core'; +import { InMemoryCache } from '@apollo/client'; provideNamedApollo(() => { const httpLink = inject(HttpLink); diff --git a/website/src/pages/docs/recipes/nativescript.mdx b/website/src/pages/docs/recipes/nativescript.mdx index b77d651ce..9a1f0a9cf 100644 --- a/website/src/pages/docs/recipes/nativescript.mdx +++ b/website/src/pages/docs/recipes/nativescript.mdx @@ -27,7 +27,7 @@ import { HttpLink } from 'apollo-angular/http'; import { NativeScriptHttpClientModule } from 'nativescript-angular/http-client'; import { NativeScriptModule } from 'nativescript-angular/nativescript.module'; import { ApplicationConfig, importProvidersFrom, inject } from '@angular/core'; -import { InMemoryCache } from '@apollo/client/core'; +import { InMemoryCache } from '@apollo/client'; export const appConfig: ApplicationConfig = { providers: [ diff --git a/yarn.lock b/yarn.lock index 892bbd425..d653fb408 100644 --- a/yarn.lock +++ b/yarn.lock @@ -264,24 +264,18 @@ dependencies: tslib "^2.3.0" -"@apollo/client@^3.13.1": - version "3.13.1" - resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.13.1.tgz#c0633c69c5446967b0e517a590595eeea61dd176" - integrity sha512-HaAt62h3jNUXpJ1v5HNgUiCzPP1c5zc2Q/FeTb2cTk/v09YlhoqKKHQFJI7St50VCJ5q8JVIc03I5bRcBrQxsg== +"@apollo/client@4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@apollo/client/-/client-4.0.1.tgz#00fe65c518de241126b3664307ccb26bc5314abc" + integrity sha512-qBW2d6++wmNeVkYuCfcw9vQi2kG007wdwdohLc+NXs2Ojz3XPiTb4r9gPTAjwAF9JXN9i7WSQ45fdcN9JAom8Q== dependencies: "@graphql-typed-document-node/core" "^3.1.1" "@wry/caches" "^1.0.0" "@wry/equality" "^0.5.6" "@wry/trie" "^0.5.0" graphql-tag "^2.12.6" - hoist-non-react-statics "^3.3.2" optimism "^0.18.0" - prop-types "^15.7.2" - rehackt "^0.1.0" - symbol-observable "^4.0.0" - ts-invariant "^0.10.3" tslib "^2.3.0" - zen-observable-ts "^1.2.5" "@asamuzakjp/css-color@^2.8.2": version "2.8.3" @@ -6561,13 +6555,6 @@ heap@^0.2.6: resolved "https://registry.yarnpkg.com/heap/-/heap-0.2.7.tgz#1e6adf711d3f27ce35a81fe3b7bd576c2260a8fc" integrity sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg== -hoist-non-react-statics@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" - integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== - dependencies: - react-is "^16.7.0" - hosted-git-info@^7.0.0: version "7.0.2" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-7.0.2.tgz#9b751acac097757667f30114607ef7b661ff4f17" @@ -10115,7 +10102,7 @@ react-fast-compare@^3.0.1: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49" integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== -react-is@^16.13.1, react-is@^16.7.0: +react-is@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -10261,11 +10248,6 @@ regjsparser@^0.9.1: dependencies: jsesc "~0.5.0" -rehackt@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/rehackt/-/rehackt-0.1.0.tgz#a7c5e289c87345f70da8728a7eb878e5d03c696b" - integrity sha512-7kRDOuLHB87D/JESKxQoRwv4DzbIdwkAGQ7p6QKGdVlY1IZheUnVhlk/4UZlNUVxdAXpyxikE3URsG067ybVzw== - rehype-katex@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/rehype-katex/-/rehype-katex-7.0.0.tgz#f5e9e2825981175a7b0a4d58ed9816c33576dfed" @@ -11376,7 +11358,7 @@ svgo@^3.2.0: csso "^5.0.5" picocolors "^1.0.0" -symbol-observable@4.0.0, symbol-observable@^4.0.0: +symbol-observable@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205" integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ== @@ -11611,13 +11593,6 @@ ts-interface-checker@^0.1.9: resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== -ts-invariant@^0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.10.3.tgz#3e048ff96e91459ffca01304dbc7f61c1f642f6c" - integrity sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ== - dependencies: - tslib "^2.1.0" - ts-morph@^21.0.0: version "21.0.1" resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-21.0.1.tgz#712302a0f6e9dbf1aa8d9cf33a4386c4b18c2006" @@ -12479,18 +12454,6 @@ yocto-queue@^1.0.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.1.1.tgz#fef65ce3ac9f8a32ceac5a634f74e17e5b232110" integrity sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g== -zen-observable-ts@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz#6c6d9ea3d3a842812c6e9519209365a122ba8b58" - integrity sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg== - dependencies: - zen-observable "0.8.15" - -zen-observable@0.8.15: - version "0.8.15" - resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15" - integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ== - zod-validation-error@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-1.5.0.tgz#2b355007a1c3b7fb04fa476bfad4e7b3fd5491e3"