diff --git a/docs/fetching/queries.md b/docs/fetching/queries.md index 015e5b7..673cf12 100644 --- a/docs/fetching/queries.md +++ b/docs/fetching/queries.md @@ -206,6 +206,10 @@ Glimmer Apollo supports SSR with [FastBoot](http://ember-fastboot.com/) by defau The `ssr` option allows disabling fetching of the query when running in SSR with FastBoot. It will skip the execution entirely in FastBoot but will execute when running in the Browser. This feature is useful if you are fetching secondary data to the page and can wait to be fetched. +### `skip` + +The query will skip the execution while the `skip` option is true. + ### `clientId` This option specifies which Apollo Client should be used for the given query. Glimmer Apollo supports defining multiple Apollo Clients that are distinguished by a custom identifier while setting the client to Glimmer Apollo. diff --git a/packages/glimmer-apollo/src/-private/query.ts b/packages/glimmer-apollo/src/-private/query.ts index 22bcda7..7edbf63 100644 --- a/packages/glimmer-apollo/src/-private/query.ts +++ b/packages/glimmer-apollo/src/-private/query.ts @@ -22,6 +22,7 @@ import type { TemplateArgs } from './types'; export interface QueryOptions extends Omit, 'query'> { + skip?: boolean; ssr?: boolean; clientId?: string; onComplete?: (data: TData | undefined) => void; @@ -41,7 +42,7 @@ export class QueryResource< TVariables, TemplateArgs> > { - @tracked loading = true; + @tracked loading = false; @tracked error?: ApolloError; @tracked data: TData | undefined; @tracked networkStatus: NetworkStatus = NetworkStatus.loading; @@ -55,22 +56,39 @@ export class QueryResource< /** @internal */ async setup(): Promise { this.#previousPositionalArgs = this.args.positional; - const [query, options] = this.args.positional; - const client = getClient(this, options?.clientId); + const [query, options = {}] = this.args.positional; + const client = getClient(this, options.clientId); - this.loading = true; const fastboot = getFastboot(this); - if (fastboot && fastboot.isFastBoot && options && options.ssr === false) { + if ( + fastboot && + fastboot.isFastBoot && + (options.ssr === false || options.skip === true) + ) { return; } let [promise, firstResolve, firstReject] = createPromise(); // eslint-disable-line prefer-const this.#firstPromiseReject = firstReject; this.promise = promise; + + if (options.skip) { + options.fetchPolicy = 'standby'; + } + + if (options.fetchPolicy === 'standby') { + if (firstResolve) { + firstResolve(); + firstResolve = undefined; + } + } else { + this.loading = true; + } + const observable = client.watchQuery({ query, - ...(options || {}) + ...options }); this._setObservable(observable); diff --git a/packages/test-app/app/components/playground/experiment.hbs b/packages/test-app/app/components/playground/experiment.hbs index 522ea79..6478d8e 100644 --- a/packages/test-app/app/components/playground/experiment.hbs +++ b/packages/test-app/app/components/playground/experiment.hbs @@ -16,6 +16,25 @@ firstName: refetch +
+

+ User Info w/ skip +

+ +loading: +{{this.userInfoWithSkip.loading}} +
+firstName: +{{this.userInfoWithSkip.data.user.firstName}} + +{{#if this.userInfoWithSkip.error}} + Error: + {{this.userInfoWithSkip.error.message}} +{{/if}} + +

Login diff --git a/packages/test-app/app/components/playground/experiment.ts b/packages/test-app/app/components/playground/experiment.ts index 9017996..8c25070 100644 --- a/packages/test-app/app/components/playground/experiment.ts +++ b/packages/test-app/app/components/playground/experiment.ts @@ -29,6 +29,16 @@ export default class PlaygroundExperiment extends Component { } ]); + userInfoWithSkip = useQuery(this, () => [ + USER_INFO, + { + variables: { id: '1-with-delay' }, + errorPolicy: 'all', + notifyOnNetworkStatusChange: true, + skip: true + } + ]); + login = useMutation(this, () => [ LOGIN, { diff --git a/packages/test-app/tests/unit/query-test.ts b/packages/test-app/tests/unit/query-test.ts index 33c242e..f508e4b 100644 --- a/packages/test-app/tests/unit/query-test.ts +++ b/packages/test-app/tests/unit/query-test.ts @@ -7,6 +7,7 @@ import { ApolloClient, ApolloError, InMemoryCache, + WatchQueryFetchPolicy, createHttpLink } from '@apollo/client/core'; import { @@ -14,6 +15,7 @@ import { UserInfoQueryVariables } from '../../app/mocks/handlers'; import sinon from 'sinon'; +import { waitUntil } from '@ember/test-helpers'; const USER_INFO = gql` query UserInfo($id: ID!) { @@ -29,11 +31,13 @@ module('useQuery', function (hooks) { let ctx = {}; const owner = {}; + const link = createHttpLink({ + uri: '/graphql' + }); + const client = new ApolloClient({ cache: new InMemoryCache(), - link: createHttpLink({ - uri: '/graphql' - }) + link }); hooks.beforeEach(() => { @@ -141,6 +145,26 @@ module('useQuery', function (hooks) { assert.deepEqual(onCompleteCalled, expectedData); }); + test('it does not call onCompleted if skip is true', async function (assert) { + const sandbox = sinon.createSandbox(); + const onCompleteCallback = sandbox.fake(); + const query = useQuery(ctx, () => [ + USER_INFO, + { + variables: { id: '2' }, + skip: true, + onComplete: onCompleteCallback + } + ]); + + await query.settled(); + assert.equal(query.loading, false); + assert.equal(query.data, undefined); + assert.equal(onCompleteCallback.callCount, 0); + + sandbox.restore(); + }); + test('it calls onError', async function (assert) { let onErrorCalled: ApolloError; const query = useQuery(ctx, () => [ @@ -267,4 +291,155 @@ module('useQuery', function (hooks) { sandbox.restore(); }); + + test('it skips running a query when skip is true', async function (assert) { + class Obj { + @tracked skip = true; + } + const options = new Obj(); + + const query = useQuery(ctx, () => [ + USER_INFO, + { + variables: { id: '1' }, + skip: options.skip + } + ]); + + assert.equal(query.loading, false); + assert.equal(query.data, undefined); + + options.skip = false; + + assert.equal(query.loading, true); + assert.equal(query.data, undefined); + await query.settled(); + assert.equal(query.loading, false); + assert.deepEqual(query.data, { + user: { + __typename: 'User', + firstName: 'Cathaline', + id: '1', + lastName: 'McCoy' + } + }); + }); + + test('it does not make network requests when skip is true', async function (assert) { + const sandbox = sinon.createSandbox(); + + const requestSpy = sandbox.fake(link.request); + sandbox.replace(link, 'request', requestSpy); + + class Obj { + @tracked skip = false; + @tracked id = '1'; + } + const options = new Obj(); + + const query = useQuery(ctx, () => [ + USER_INFO, + { + variables: { id: options.id }, + skip: options.skip + } + ]); + + assert.equal(query.loading, true); + assert.equal(query.data, undefined); + + await query.settled(); + assert.equal(query.loading, false); + assert.deepEqual(query.data, { + user: { + __typename: 'User', + firstName: 'Cathaline', + id: '1', + lastName: 'McCoy' + } + }); + + options.skip = true; + options.id = '2'; + + await query.settled(); + assert.equal(query.loading, false); + assert.deepEqual(query.data, { + user: { + __typename: 'User', + firstName: 'Cathaline', + id: '1', + lastName: 'McCoy' + } + }); + assert.equal(requestSpy.callCount, 1); + + sandbox.restore(); + }); + + test('it treats fetchPolicy standby like skip', async function (assert) { + class Obj { + @tracked fetchPolicy: WatchQueryFetchPolicy = 'standby'; + } + const options = new Obj(); + + const query = useQuery(ctx, () => [ + USER_INFO, + { + variables: { id: '1' }, + fetchPolicy: options.fetchPolicy + } + ]); + + assert.equal(query.loading, false); + assert.equal(query.data, undefined); + + await query.settled(); + + options.fetchPolicy = 'cache-first'; + assert.equal(query.data, undefined); + + await query.settled(); + assert.deepEqual(query.data, { + user: { + __typename: 'User', + firstName: 'Cathaline', + id: '1', + lastName: 'McCoy' + } + }); + }); + + test('it refetches the query when skip is true', async function (assert) { + const sandbox = sinon.createSandbox(); + + const requestSpy = sandbox.fake(link.request); + sandbox.replace(link, 'request', requestSpy); + + const query = useQuery(ctx, () => [ + USER_INFO, + { + variables: { id: '1' }, + skip: true + } + ]); + + query.refetch(); + + assert.equal(query.loading, false); + await waitUntil(() => query.data !== undefined); + assert.ok(requestSpy.calledOnce); + assert.equal(query.loading, false); + assert.equal(query.networkStatus, 7); + assert.deepEqual(query.data, { + user: { + __typename: 'User', + firstName: 'Cathaline', + id: '1', + lastName: 'McCoy' + } + }); + + sandbox.restore(); + }); });