diff --git a/.changeset/twenty-olives-applaud.md b/.changeset/twenty-olives-applaud.md new file mode 100644 index 0000000000..3a70c3eb31 --- /dev/null +++ b/.changeset/twenty-olives-applaud.md @@ -0,0 +1,20 @@ +--- +'@urql/exchange-auth': patch +'@urql/exchange-execute': patch +'@urql/exchange-graphcache': patch +'@urql/exchange-multipart-fetch': patch +'@urql/exchange-persisted-fetch': patch +'@urql/exchange-populate': patch +'@urql/exchange-refocus': patch +'@urql/exchange-retry': patch +'@urql/exchange-suspense': patch +'@urql/core': minor +'urql': patch +--- + +Deprecate the `Operation.operationName` property in favor of `Operation.kind`. This name was +previously confusing as `operationName` was effectively referring to two different things. You can +safely upgrade to this new version, however to mute all deprecation warnings you will have to +**upgrade** all `urql` packages you use. If you have custom exchanges that spread operations, please +use [the new `makeOperation` helper +function](https://formidable.com/open-source/urql/docs/api/core/#makeoperation) instead. diff --git a/docs/advanced/authentication.md b/docs/advanced/authentication.md index 38dfc325f4..49b3ef4e0c 100644 --- a/docs/advanced/authentication.md +++ b/docs/advanced/authentication.md @@ -82,7 +82,7 @@ const getAuth = async ({ authState }) => { } return null; -} +}; ``` We check that the `authState` doesn't already exist (this indicates that it is the first time this exchange is executed and not an auth failure) and fetch the auth state from @@ -104,7 +104,7 @@ const getAuth = async ({ authState, mutate }) => { } return null; -} +}; ``` ### Configuring `addAuthToOperation` @@ -113,10 +113,9 @@ The purpose of `addAuthToOperation` is to take apply your auth state to each req you've returned from `getAuth` and not at all constrained by the exchange: ```js -const addAuthToOperation = ({ - authState, - operation, -}) => { +import { makeOperation } from '@urql/core'; + +const addAuthToOperation = ({ authState, operation }) => { if (!authState || !authState.token) { return operation; } @@ -126,20 +125,17 @@ const addAuthToOperation = ({ ? operation.context.fetchOptions() : operation.context.fetchOptions || {}; - return { - ...operation, - context: { - ...operation.context, - fetchOptions: { - ...fetchOptions, - headers: { - ...fetchOptions.headers, - "Authorization": authState.token, - }, + return makeOperation(operation.kind, operation, { + ...operation.context, + fetchOptions: { + ...fetchOptions, + headers: { + ...fetchOptions.headers, + Authorization: authState.token, }, }, - }; -} + }); +}; ``` First we check that we have an `authState` and a `token`. Then we apply it to the request `fetchOptions` as an `Authorization` header. @@ -174,10 +170,8 @@ is the recommended approach. We'll be able to determine whether any of the Graph ```js const didAuthError = ({ error }) => { - return error.graphQLErrors.some( - e => e.extensions?.code === 'FORBIDDEN', - ); -} + return error.graphQLErrors.some(e => e.extensions?.code === 'FORBIDDEN'); +}; ``` For some GraphQL APIs, the auth error is communicated via an 401 HTTP response as is common in RESTful APIs: @@ -225,7 +219,7 @@ const getAuth = async ({ authState }) => { logout(); return null; -} +}; ``` Here, `logout()` is a placeholder that is called when we got an error, so that we can redirect to a login page again and clear our tokens from local storage or otherwise. @@ -299,14 +293,12 @@ const client = createClient({ cacheExchange, errorExchange({ onError: error => { - const isAuthError = error.graphQLErrors.some( - e => e.extensions?.code === 'FORBIDDEN', - ); + const isAuthError = error.graphQLErrors.some(e => e.extensions?.code === 'FORBIDDEN'); if (isAuthError) { logout(); } - } + }, }), authExchange({ /* config */ @@ -331,14 +323,14 @@ const App = ({ isLoggedIn }: { isLoggedIn: boolean | null }) => { if (isLoggedIn === null) { return null; } - + return createClient({ /* config */ }); }, [isLoggedIn]); if (!client) { return null; } - + return { {/* app content */} diff --git a/docs/advanced/testing.md b/docs/advanced/testing.md index 4d90001b90..ddfdaa19e6 100644 --- a/docs/advanced/testing.md +++ b/docs/advanced/testing.md @@ -191,17 +191,15 @@ If you prefer to have more control on when the new data is arriving you can use Here's an example of testing a list component which uses a subscription. ```tsx +import { OperationContext, makeOperation } from '@urql/core'; + const mockClient = { executeSubscription: jest.fn(query => pipe( interval(200), map((i: number) => ({ // To mock a full result, we need to pass a mock operation back as well - operation: { - operationName: 'subscription, - context: {}, - ...query, - }, + operation: makeOperation('subscription', query, {} as OperationContext), data: { posts: { id: i, title: 'Post title', content: 'This is a post' } }, })) ) diff --git a/docs/api/core.md b/docs/api/core.md index ffc9101a5a..d3bb80d8e6 100644 --- a/docs/api/core.md +++ b/docs/api/core.md @@ -37,7 +37,7 @@ Accepts a [`GraphQLRequest`](#graphqlrequest) and optionally `Partial`](#operationresult) — a stream of query results that can be subscribed to. Internally, subscribing to the returned source will create an [`Operation`](#operation), with -`operationName` set to `'query'`, and dispatch it on the +`kind` set to `'query'`, and dispatch it on the exchanges pipeline. If no subscribers are listening to this operation anymore and unsubscribe from the query sources, the `Client` will dispatch a "teardown" operation. @@ -53,12 +53,12 @@ repeatedly in the interval you pass. ### client.executeSubscription This is functionally the same as `client.executeQuery`, but creates operations for subscriptions -instead, with `operationName` set to `'subscription'`. +instead, with `kind` set to `'subscription'`. ### client.executeMutation This is functionally the same as `client.executeQuery`, but creates operations for mutations -instead, with `operationName` set to `'mutation'`. +instead, with `kind` set to `'mutation'`. A mutation source is always guaranteed to only respond with a single [`OperationResult`](#operationresult) and then complete. @@ -168,16 +168,15 @@ received. ### Operation The input for every exchange that informs GraphQL requests. -It extends the [GraphQLRequest](#graphqlrequest) type and contains these additional properties: +It extends the [`GraphQLRequest` type](#graphqlrequest) and contains these additional properties: -| Prop | Type | Description | -| --------------- | ------------------ | --------------------------------------------- | -| `operationName` | `OperationType` | The type of GraphQL operation being executed. | -| `context` | `OperationContext` | Additional metadata passed to exchange. | +| Prop | Type | Description | +| --------- | ------------------ | --------------------------------------------- | +| `kind` | `OperationType` | The type of GraphQL operation being executed. | +| `context` | `OperationContext` | Additional metadata passed to exchange. | -> **Note:** In `urql` the `operationName` on the `Operation` isn't the actual name of an operation -> and derived from the GraphQL `DocumentNode`, but instead a type of operation, like `'query'` or -> `'teardown'` +An `Operation` also contains the `operationName` property, which is a deprecated alias of the `kind` +property and outputs a deprecation warning if it's used. ### RequestPolicy @@ -379,6 +378,36 @@ Additionally, this utility will ensure that the `query` reference will remain st that if the same `query` will be passed in as a string or as a fresh `DocumentNode`, then the output will always have the same `DocumentNode` reference. +### makeOperation + +This utility is used to either turn a [`GraphQLRequest` object](#graphqlrequest) into a new +[`Operation` object](#operation) or to copy an `Operation`. It adds the `kind` property and the +`operationName` alias that outputs a deprecation warning. + +It accepts three arguments: + +- An `Operation`'s `kind` (See [`OperationType`](#operationtype) +- A [`GraphQLRequest` object](#graphqlrequest) or another [`Operation`](#operation) that should be + copied. +- and; optionally a [partial `OperationContext` object.](#operationcontext). This argument may be + left out if the context is to be copied from the operation that may be passed as a second argument. + +Hence some valid uses of the utility are: + +```js +// Create a new operation from scratch +makeOperation('query', createRequest(query, variables), client.createOperationContext(opts)); + +// Turn an operation into a 'teardown' operation +makeOperation('teardown', operation); + +// Copy an existing operation while modifying its context +makeOperation(operation.kind, operation, { + ...operation.context, + preferGetMethod: true, +}); +``` + ### makeResult This is a helper function that converts a GraphQL API result to an diff --git a/docs/common-questions.md b/docs/common-questions.md index 3b4c29b808..bb59211b69 100644 --- a/docs/common-questions.md +++ b/docs/common-questions.md @@ -10,6 +10,8 @@ order: 6 If you need `async fetchOptions` you can add an exchange that looks like this: ```js +import { makeOperation } from '@urql/core'; + export const fetchOptionsExchange = (fn: any): Exchange => ({ forward }) => ops$ => { return pipe( ops$, @@ -17,10 +19,12 @@ export const fetchOptionsExchange = (fn: any): Exchange => ({ forward }) => ops$ const result = fn(operation.context.fetchOptions); return pipe( typeof result.then === 'function' ? fromPromise(result) : fromValue(result), - map((fetchOptions: RequestInit | (() => RequestInit)) => ({ - ...operation, - context: { ...operation.context, fetchOptions }, - })) + map((fetchOptions: RequestInit | (() => RequestInit)) => { + return makeOperation(operation.kind, operation, { + ...operation.context, + fetchOptions, + }); + }) ); }), forward diff --git a/docs/concepts/exchanges.md b/docs/concepts/exchanges.md index 9e21501c3e..ddbb2a95aa 100644 --- a/docs/concepts/exchanges.md +++ b/docs/concepts/exchanges.md @@ -190,11 +190,11 @@ import { pipe, filter, merge, share } from 'wonka'; // <-- The ExchangeIO function (inline) const queries = pipe( operations$, - filter(op => op.operationName === 'query') + filter(op => op.kind === 'query') ); const others = pipe( operations$, - filter(op => op.operationName !== 'query') + filter(op => op.kind !== 'query') ); return forward(merge([queries, others])); }; @@ -205,11 +205,11 @@ import { pipe, filter, merge, share } from 'wonka'; const shared = pipe(operations$, share); const queries = pipe( shared, - filter(op => op.operationName === 'query') + filter(op => op.kind === 'query') ); const others = pipe( shared, - filter(op => op.operationName !== 'query') + filter(op => op.kind !== 'query') ); return forward(merge([queries, others])); }; @@ -221,7 +221,7 @@ import { pipe, filter, merge, share } from 'wonka'; pipe( operations$, map(op => { - if (op.operationName === 'query') { + if (op.kind === 'query') { /* ... */ } else { /* ... */ @@ -252,7 +252,7 @@ import { pipe, filter, merge, share } from 'wonka'; // This doesn't handle operations that aren't queries const queries = pipe( operations$, - filter(op => op.operationName === 'query') + filter(op => op.kind === 'query') ); return forward(queries); }; @@ -262,11 +262,11 @@ import { pipe, filter, merge, share } from 'wonka'; const shared = pipe(operations$, share); const queries = pipe( shared, - filter(op => op.operationName === 'query') + filter(op => op.kind === 'query') ); const rest = pipe( shared, - filter(op => op.operationName !== 'query') + filter(op => op.kind !== 'query') ); return forward(merge([queries, rest])); }; diff --git a/exchanges/auth/README.md b/exchanges/auth/README.md index b0bbea4bc0..c21f19832d 100644 --- a/exchanges/auth/README.md +++ b/exchanges/auth/README.md @@ -18,6 +18,7 @@ You'll then need to add the `authExchange`, that this package exposes to your `u ```js import { createClient, dedupExchange, cacheExchange, fetchExchange } from 'urql'; +import { makeOperation } from '@urql/core'; import { authExchange } from '@urql/exchange-auth'; const client = createClient({ @@ -41,9 +42,10 @@ const client = createClient({ ? operation.context.fetchOptions() : operation.context.fetchOptions || {}; - return { - ...operation, - context: { + return makeOperation( + operation.kind, + operation, + { ...operation.context, fetchOptions: { ...fetchOptions, @@ -53,7 +55,7 @@ const client = createClient({ }, }, }, - }; + ); }, willAuthError: ({ authState }) => { if (!authState) return true; diff --git a/exchanges/auth/src/authExchange.ts b/exchanges/auth/src/authExchange.ts index 7fffe541a4..2551dd3532 100644 --- a/exchanges/auth/src/authExchange.ts +++ b/exchanges/auth/src/authExchange.ts @@ -22,6 +22,7 @@ import { CombinedError, Exchange, createRequest, + makeOperation, } from '@urql/core'; import { DocumentNode } from 'graphql'; @@ -57,13 +58,11 @@ export interface AuthConfig { const addAuthAttemptToOperation = ( operation: Operation, hasAttempted: boolean -) => ({ - ...operation, - context: { +) => + makeOperation(operation.kind, operation, { ...operation.context, authAttempt: hasAttempted, - }, -}); + }); export function authExchange({ addAuthToOperation, @@ -131,14 +130,14 @@ export function authExchange({ const teardownOps$ = pipe( sharedOps$, filter((operation: Operation) => { - return operation.operationName === 'teardown'; + return operation.kind === 'teardown'; }) ); const pendingOps$ = pipe( sharedOps$, filter((operation: Operation) => { - return operation.operationName !== 'teardown'; + return operation.kind !== 'teardown'; }) ); @@ -162,9 +161,7 @@ export function authExchange({ const teardown$ = pipe( sharedOps$, filter(op => { - return ( - op.operationName === 'teardown' && op.key === operation.key - ); + return op.kind === 'teardown' && op.key === operation.key; }) ); diff --git a/exchanges/execute/src/execute.test.ts b/exchanges/execute/src/execute.test.ts index f4f46396ad..3fe41a2de3 100644 --- a/exchanges/execute/src/execute.test.ts +++ b/exchanges/execute/src/execute.test.ts @@ -1,7 +1,7 @@ jest.mock('graphql'); import { fetchExchange } from 'urql'; -import { executeExchange, getOperationName } from './execute'; +import { executeExchange } from './execute'; import { execute, print } from 'graphql'; import { pipe, @@ -13,8 +13,9 @@ import { Source, } from 'wonka'; import { mocked } from 'ts-jest/utils'; +import { getOperationName } from '@urql/core/utils'; import { queryOperation } from '@urql/core/test-utils'; -import { makeErrorResult } from '@urql/core'; +import { makeErrorResult, makeOperation } from '@urql/core'; import { Client } from '@urql/core/client'; import { OperationResult } from '@urql/core/types'; @@ -179,10 +180,11 @@ describe('on thrown error', () => { }); describe('on unsupported operation', () => { - const operation = { - ...queryOperation, - operationName: 'teardown', - } as const; + const operation = makeOperation( + 'teardown', + queryOperation, + queryOperation.context + ); it('returns operation result', async () => { const { source, next } = makeSubject(); diff --git a/exchanges/execute/src/execute.ts b/exchanges/execute/src/execute.ts index f146b54348..0ee25fed5d 100644 --- a/exchanges/execute/src/execute.ts +++ b/exchanges/execute/src/execute.ts @@ -10,8 +10,6 @@ import { } from 'wonka'; import { - DocumentNode, - Kind, GraphQLSchema, GraphQLFieldResolver, GraphQLTypeResolver, @@ -19,15 +17,7 @@ import { } from 'graphql'; import { Exchange, makeResult, makeErrorResult, Operation } from '@urql/core'; - -export const getOperationName = (query: DocumentNode): string | undefined => { - for (let i = 0, l = query.definitions.length; i < l; i++) { - const node = query.definitions[i]; - if (node.kind === Kind.OPERATION_DEFINITION && node.name) { - return node.name.value; - } - } -}; +import { getOperationName } from '@urql/core/utils'; interface ExecuteExchangeArgs { schema: GraphQLSchema; @@ -51,16 +41,13 @@ export const executeExchange = ({ const executedOps$ = pipe( sharedOps$, filter((operation: Operation) => { - return ( - operation.operationName === 'query' || - operation.operationName === 'mutation' - ); + return operation.kind === 'query' || operation.kind === 'mutation'; }), mergeMap((operation: Operation) => { const { key } = operation; const teardown$ = pipe( sharedOps$, - filter(op => op.operationName === 'teardown' && op.key === key) + filter(op => op.kind === 'teardown' && op.key === key) ); const calculatedContext = @@ -99,10 +86,7 @@ export const executeExchange = ({ const forwardedOps$ = pipe( sharedOps$, filter((operation: Operation) => { - return ( - operation.operationName !== 'query' && - operation.operationName !== 'mutation' - ); + return operation.kind !== 'query' && operation.kind !== 'mutation'; }), forward ); diff --git a/exchanges/graphcache/src/cacheExchange.test.ts b/exchanges/graphcache/src/cacheExchange.test.ts index d4abed160c..072f8a59c8 100644 --- a/exchanges/graphcache/src/cacheExchange.test.ts +++ b/exchanges/graphcache/src/cacheExchange.test.ts @@ -775,7 +775,7 @@ describe('optimistic updates', () => { pipe( cacheExchange({ optimistic })({ forward, client, dispatchDebug })(ops$), - filter(x => x.operation.operationName === 'mutation'), + filter(x => x.operation.kind === 'mutation'), tap(result), publish ); @@ -862,7 +862,7 @@ describe('optimistic updates', () => { pipe( ops$, delay(1), - filter(x => x.operationName !== 'mutation'), + filter(x => x.kind !== 'mutation'), map(response) ); @@ -1518,7 +1518,7 @@ describe('commutativity', () => { const forward = (ops$: Source): Source => pipe( ops$, - filter(op => op.operationName !== 'teardown'), + filter(op => op.kind !== 'teardown'), mergeMap(result) ); @@ -1597,7 +1597,7 @@ describe('commutativity', () => { pipe( cacheExchange({ optimistic })({ forward, client, dispatchDebug })(ops$), tap(result => { - if (result.operation.operationName === 'query') { + if (result.operation.kind === 'query') { data = result.data; } }), @@ -1691,7 +1691,7 @@ describe('commutativity', () => { pipe( cacheExchange()({ forward, client, dispatchDebug })(ops$), tap(result => { - if (result.operation.operationName === 'query') { + if (result.operation.kind === 'query') { data = result.data; } }), @@ -1803,7 +1803,7 @@ describe('commutativity', () => { pipe( cacheExchange({ optimistic })({ forward, client, dispatchDebug })(ops$), tap(result => { - if (result.operation.operationName === 'query') { + if (result.operation.kind === 'query') { data = result.data; } }), @@ -1888,7 +1888,7 @@ describe('commutativity', () => { pipe( cacheExchange()({ forward, client, dispatchDebug })(ops$), tap(result => { - if (result.operation.operationName === 'query') { + if (result.operation.kind === 'query') { data = result.data; } }), @@ -1979,7 +1979,7 @@ describe('commutativity', () => { pipe( cacheExchange()({ forward, client, dispatchDebug })(ops$), tap(result => { - if (result.operation.operationName === 'query') { + if (result.operation.kind === 'query') { data = result.data; } }), diff --git a/exchanges/graphcache/src/cacheExchange.ts b/exchanges/graphcache/src/cacheExchange.ts index 598ea059dc..d25f1f9382 100644 --- a/exchanges/graphcache/src/cacheExchange.ts +++ b/exchanges/graphcache/src/cacheExchange.ts @@ -3,6 +3,7 @@ import { IntrospectionQuery } from 'graphql'; import { Exchange, formatDocument, + makeOperation, Operation, OperationResult, RequestPolicy, @@ -127,16 +128,16 @@ export const cacheExchange = (opts?: CacheExchangeOpts): Exchange => ({ // This registers queries with the data layer to ensure commutativity const prepareForwardedOperation = (operation: Operation) => { - if (operation.operationName === 'query') { + if (operation.kind === 'query') { // Pre-reserve the position of the result layer reserveLayer(store.data, operation.key); - } else if (operation.operationName === 'teardown') { + } else if (operation.kind === 'teardown') { // Delete reference to operation if any exists to release it ops.delete(operation.key); // Mark operation layer as done noopDataState(store.data, operation.key); } else if ( - operation.operationName === 'mutation' && + operation.kind === 'mutation' && operation.context.requestPolicy !== 'network-only' ) { // This executes an optimistic update for mutations and registers it if necessary @@ -157,16 +158,20 @@ export const cacheExchange = (opts?: CacheExchangeOpts): Exchange => ({ } } - return { - ...operation, - variables: operation.variables - ? filterVariables( - getMainOperation(operation.query), - operation.variables - ) - : operation.variables, - query: formatDocument(operation.query), - }; + return makeOperation( + operation.kind, + { + key: operation.key, + query: formatDocument(operation.query), + variables: operation.variables + ? filterVariables( + getMainOperation(operation.query), + operation.variables + ) + : operation.variables, + }, + operation.context + ); }; // This updates the known dependencies for the passed operation @@ -207,7 +212,7 @@ export const cacheExchange = (opts?: CacheExchangeOpts): Exchange => ({ const { operation, error, extensions } = result; const { key } = operation; - if (operation.operationName === 'mutation') { + if (operation.kind === 'mutation') { // Collect previous dependencies that have been written for optimistic updates const dependencies = optimisticKeysToDependencies.get(key); collectPendingOperations(pendingOperations, dependencies); @@ -226,7 +231,7 @@ export const cacheExchange = (opts?: CacheExchangeOpts): Exchange => ({ const queryResult = query(store, operation, result.data); result.data = queryResult.data; - if (operation.operationName === 'query') { + if (operation.kind === 'query') { // Collect the query's dependencies for future pending operation updates queryDependencies = queryResult.dependencies; collectPendingOperations(pendingOperations, queryDependencies); @@ -269,8 +274,7 @@ export const cacheExchange = (opts?: CacheExchangeOpts): Exchange => ({ inputOps$, filter(op => { return ( - op.operationName === 'query' && - op.context.requestPolicy !== 'network-only' + op.kind === 'query' && op.context.requestPolicy !== 'network-only' ); }), map(operationResultFromCache), @@ -281,8 +285,7 @@ export const cacheExchange = (opts?: CacheExchangeOpts): Exchange => ({ inputOps$, filter(op => { return ( - op.operationName !== 'query' || - op.context.requestPolicy === 'network-only' + op.kind !== 'query' || op.context.requestPolicy === 'network-only' ); }) ); diff --git a/exchanges/graphcache/src/helpers/operation.ts b/exchanges/graphcache/src/helpers/operation.ts index 27dcc32c14..995f427246 100644 --- a/exchanges/graphcache/src/helpers/operation.ts +++ b/exchanges/graphcache/src/helpers/operation.ts @@ -1,4 +1,9 @@ -import { Operation, RequestPolicy, CacheOutcome } from '@urql/core'; +import { + Operation, + RequestPolicy, + CacheOutcome, + makeOperation, +} from '@urql/core'; // Returns the given operation result with added cacheOutcome meta field export const addCacheOutcome = ( @@ -19,10 +24,9 @@ export const addCacheOutcome = ( export const toRequestPolicy = ( operation: Operation, requestPolicy: RequestPolicy -): Operation => ({ - ...operation, - context: { +): Operation => { + return makeOperation(operation.kind, operation, { ...operation.context, requestPolicy, - }, -}); + }); +}; diff --git a/exchanges/graphcache/src/offlineExchange.ts b/exchanges/graphcache/src/offlineExchange.ts index 181a79f33c..a3596212a2 100644 --- a/exchanges/graphcache/src/offlineExchange.ts +++ b/exchanges/graphcache/src/offlineExchange.ts @@ -7,6 +7,7 @@ import { ExchangeIO, CombinedError, createRequest, + makeOperation, } from '@urql/core'; import { @@ -81,7 +82,7 @@ export const offlineExchange = (opts: CacheExchangeOpts): Exchange => input => { const requests: SerializedRequest[] = []; for (let i = 0; i < failedQueue.length; i++) { const operation = failedQueue[i]; - if (operation.operationName === 'mutation') { + if (operation.kind === 'mutation') { requests.push({ query: print(operation.query), variables: operation.variables, @@ -98,8 +99,8 @@ export const offlineExchange = (opts: CacheExchangeOpts): Exchange => input => { for (let i = 0; i < failedQueue.length; i++) { const operation = failedQueue[i]; - if (operation.operationName === 'mutation') { - next({ ...operation, operationName: 'teardown' }); + if (operation.kind === 'mutation') { + next(makeOperation('teardown', operation)); } } @@ -117,7 +118,7 @@ export const offlineExchange = (opts: CacheExchangeOpts): Exchange => input => { outerForward(ops$), filter(res => { if ( - res.operation.operationName === 'mutation' && + res.operation.kind === 'mutation' && isOfflineError(res.error) && isOptimisticMutation(optimisticMutations, res.operation) ) { @@ -160,10 +161,7 @@ export const offlineExchange = (opts: CacheExchangeOpts): Exchange => input => { return pipe( cacheResults$(opsAndRebound$), filter(res => { - if ( - res.operation.operationName === 'query' && - isOfflineError(res.error) - ) { + if (res.operation.kind === 'query' && isOfflineError(res.error)) { next(toRequestPolicy(res.operation, 'cache-only')); failedQueue.push(res.operation); return false; diff --git a/exchanges/multipart-fetch/src/__snapshots__/multipartFetchExchange.test.ts.snap b/exchanges/multipart-fetch/src/__snapshots__/multipartFetchExchange.test.ts.snap index 07c048cc73..751c635652 100644 --- a/exchanges/multipart-fetch/src/__snapshots__/multipartFetchExchange.test.ts.snap +++ b/exchanges/multipart-fetch/src/__snapshots__/multipartFetchExchange.test.ts.snap @@ -14,6 +14,7 @@ Object { "url": "http://localhost:3000/graphql", }, "key": 2, + "kind": "query", "operationName": "query", "query": Object { "definitions": Array [ @@ -153,6 +154,7 @@ Object { "url": "http://localhost:3000/graphql", }, "key": 2, + "kind": "query", "operationName": "query", "query": Object { "definitions": Array [ @@ -294,6 +296,7 @@ Object { "url": "http://localhost:3000/graphql", }, "key": 2, + "kind": "query", "operationName": "query", "query": Object { "definitions": Array [ @@ -437,6 +440,7 @@ Object { "url": "http://localhost:3000/graphql", }, "key": 3, + "kind": "mutation", "operationName": "mutation", "query": Object { "definitions": Array [ @@ -560,6 +564,7 @@ Object { "url": "http://localhost:3000/graphql", }, "key": 3, + "kind": "mutation", "operationName": "mutation", "query": Object { "definitions": Array [ diff --git a/exchanges/multipart-fetch/src/multipartFetchExchange.test.ts b/exchanges/multipart-fetch/src/multipartFetchExchange.test.ts index 87f9da0725..4895c5d7d4 100644 --- a/exchanges/multipart-fetch/src/multipartFetchExchange.test.ts +++ b/exchanges/multipart-fetch/src/multipartFetchExchange.test.ts @@ -1,4 +1,4 @@ -import { Client, OperationResult, OperationType } from '@urql/core'; +import { Client, OperationResult, makeOperation } from '@urql/core'; import { empty, fromValue, pipe, Source, subscribe, toPromise } from 'wonka'; import { multipartFetchExchange } from './multipartFetchExchange'; @@ -202,10 +202,9 @@ describe('on teardown', () => { it('does not call the query', () => { pipe( - fromValue({ - ...queryOperation, - operationName: 'teardown' as OperationType, - }), + fromValue( + makeOperation('teardown', queryOperation, queryOperation.context) + ), multipartFetchExchange(exchangeArgs), subscribe(fail) ); diff --git a/exchanges/multipart-fetch/src/multipartFetchExchange.ts b/exchanges/multipart-fetch/src/multipartFetchExchange.ts index 188efcf729..bc27b46eb2 100644 --- a/exchanges/multipart-fetch/src/multipartFetchExchange.ts +++ b/exchanges/multipart-fetch/src/multipartFetchExchange.ts @@ -17,17 +17,12 @@ export const multipartFetchExchange: Exchange = ({ const fetchResults$ = pipe( sharedOps$, filter(operation => { - return ( - operation.operationName === 'query' || - operation.operationName === 'mutation' - ); + return operation.kind === 'query' || operation.kind === 'mutation'; }), mergeMap(operation => { const teardown$ = pipe( sharedOps$, - filter( - op => op.operationName === 'teardown' && op.key === operation.key - ) + filter(op => op.kind === 'teardown' && op.key === operation.key) ); // Spreading operation.variables here in case someone made a variables with Object.create(null). @@ -102,10 +97,7 @@ export const multipartFetchExchange: Exchange = ({ const forward$ = pipe( sharedOps$, filter(operation => { - return ( - operation.operationName !== 'query' && - operation.operationName !== 'mutation' - ); + return operation.kind !== 'query' && operation.kind !== 'mutation'; }), forward ); diff --git a/exchanges/multipart-fetch/src/test-utils.ts b/exchanges/multipart-fetch/src/test-utils.ts index 909b459734..6b075b7e2d 100644 --- a/exchanges/multipart-fetch/src/test-utils.ts +++ b/exchanges/multipart-fetch/src/test-utils.ts @@ -1,4 +1,9 @@ -import { GraphQLRequest, OperationContext, Operation } from '@urql/core'; +import { + GraphQLRequest, + OperationContext, + Operation, + makeOperation, +} from '@urql/core'; import gql from 'graphql-tag'; const context: OperationContext = { @@ -60,20 +65,20 @@ const uploads: GraphQLRequest = { }, }; -export const uploadOperation: Operation = { - ...upload, - operationName: 'mutation', - context, -}; +export const uploadOperation: Operation = makeOperation( + 'mutation', + upload, + context +); -export const multipleUploadOperation: Operation = { - ...uploads, - operationName: 'mutation', - context, -}; +export const multipleUploadOperation: Operation = makeOperation( + 'mutation', + uploads, + context +); -export const queryOperation: Operation = { - ...queryGql, - operationName: 'query', - context, -}; +export const queryOperation: Operation = makeOperation( + 'query', + queryGql, + context +); diff --git a/exchanges/persisted-fetch/src/persistedFetchExchange.ts b/exchanges/persisted-fetch/src/persistedFetchExchange.ts index 6f6f7b3540..1ad95bb55d 100644 --- a/exchanges/persisted-fetch/src/persistedFetchExchange.ts +++ b/exchanges/persisted-fetch/src/persistedFetchExchange.ts @@ -13,6 +13,7 @@ import { } from 'wonka'; import { + makeOperation, CombinedError, ExchangeInput, Exchange, @@ -48,12 +49,12 @@ export const persistedFetchExchange = ( const sharedOps$ = share(ops$); const fetchResults$ = pipe( sharedOps$, - filter(operation => operation.operationName === 'query'), + filter(operation => operation.kind === 'query'), mergeMap(operation => { const { key } = operation; const teardown$ = pipe( sharedOps$, - filter(op => op.operationName === 'teardown' && op.key === key) + filter(op => op.kind === 'teardown' && op.key === key) ); const body = makeFetchBody(operation); @@ -125,7 +126,7 @@ export const persistedFetchExchange = ( const forward$ = pipe( sharedOps$, - filter(operation => operation.operationName !== 'query'), + filter(operation => operation.kind !== 'query'), forward ); @@ -139,17 +140,16 @@ const makePersistedFetchSource = ( dispatchDebug: ExchangeInput['dispatchDebug'], useGet: boolean ): Source => { - const newOperation = { - ...operation, - context: { - ...operation.context, - preferGetMethod: useGet || operation.context.preferGetMethod, - }, - }; + const newOperation = makeOperation(operation.kind, operation, { + ...operation.context, + preferGetMethod: useGet || operation.context.preferGetMethod, + }); + const url = makeFetchURL( newOperation, body.query ? body : { ...body, query: '' } ); + const fetchOptions = makeFetchOptions(newOperation, body); dispatchDebug({ diff --git a/exchanges/persisted-fetch/src/test-utils.ts b/exchanges/persisted-fetch/src/test-utils.ts index e7e7b101a0..b084a3a110 100644 --- a/exchanges/persisted-fetch/src/test-utils.ts +++ b/exchanges/persisted-fetch/src/test-utils.ts @@ -1,4 +1,9 @@ -import { GraphQLRequest, OperationContext, Operation } from '@urql/core'; +import { + GraphQLRequest, + OperationContext, + Operation, + makeOperation, +} from '@urql/core'; import gql from 'graphql-tag'; const context: OperationContext = { @@ -39,14 +44,14 @@ export const mutationGql: GraphQLRequest = { }, }; -export const queryOperation: Operation = { - ...queryGql, - operationName: 'query', - context, -}; +export const queryOperation: Operation = makeOperation( + 'query', + queryGql, + context +); -export const mutationOperation: Operation = { - ...mutationGql, - operationName: 'mutation', - context, -}; +export const mutationOperation: Operation = makeOperation( + 'mutation', + mutationGql, + context +); diff --git a/exchanges/populate/src/populateExchange.test.ts b/exchanges/populate/src/populateExchange.test.ts index a81bfbe16c..30321c387a 100644 --- a/exchanges/populate/src/populateExchange.test.ts +++ b/exchanges/populate/src/populateExchange.test.ts @@ -9,7 +9,7 @@ import { import gql from 'graphql-tag'; import { fromValue, pipe, fromArray, toArray } from 'wonka'; -import { Client, Operation } from '@urql/core'; +import { Client, Operation, OperationContext, makeOperation } from '@urql/core'; import { populateExchange } from './populateExchange'; @@ -85,6 +85,8 @@ const schemaDef = ` } `; +const context = {} as OperationContext; + const getNodesByType = ( query: DocumentNode, type: T @@ -110,15 +112,18 @@ const exchangeArgs = { }; describe('on mutation', () => { - const operation = { - key: 1234, - operationName: 'mutation', - query: gql` - mutation MyMutation { - addTodo @populate - } - `, - } as Operation; + const operation = makeOperation( + 'mutation', + { + key: 1234, + query: gql` + mutation MyMutation { + addTodo @populate + } + `, + }, + context + ); describe('mutation query', () => { it('matches snapshot', async () => { @@ -140,37 +145,43 @@ describe('on mutation', () => { }); describe('on query -> mutation', () => { - const queryOp = { - key: 1234, - operationName: 'query', - query: gql` - query { - todos { - id - text - creator { - id - name - } - } - users { + const queryOp = makeOperation( + 'query', + { + key: 1234, + query: gql` + query { todos { + id text + creator { + id + name + } + } + users { + todos { + text + } } } - } - `, - } as Operation; - - const mutationOp = { - key: 5678, - operationName: 'mutation', - query: gql` - mutation MyMutation { - addTodo @populate - } - `, - } as Operation; + `, + }, + context + ); + + const mutationOp = makeOperation( + 'mutation', + { + key: 5678, + query: gql` + mutation MyMutation { + addTodo @populate + } + `, + }, + context + ); describe('mutation query', () => { it('matches snapshot', async () => { @@ -207,47 +218,53 @@ describe('on query -> mutation', () => { }); describe('on (query w/ fragment) -> mutation', () => { - const queryOp = { - key: 1234, - operationName: 'query', - query: gql` - query { - todos { - ...TodoFragment - creator { - ...CreatorFragment + const queryOp = makeOperation( + 'query', + { + key: 1234, + query: gql` + query { + todos { + ...TodoFragment + creator { + ...CreatorFragment + } } } - } - fragment TodoFragment on Todo { - id - text - } + fragment TodoFragment on Todo { + id + text + } - fragment CreatorFragment on User { - id - name - } - `, - } as Operation; - - const mutationOp = { - key: 5678, - operationName: 'mutation', - query: gql` - mutation MyMutation { - addTodo @populate { - ...TodoFragment + fragment CreatorFragment on User { + id + name + } + `, + }, + context + ); + + const mutationOp = makeOperation( + 'mutation', + { + key: 5678, + query: gql` + mutation MyMutation { + addTodo @populate { + ...TodoFragment + } } - } - fragment TodoFragment on Todo { - id - text - } - `, - } as Operation; + fragment TodoFragment on Todo { + id + text + } + `, + }, + context + ); describe('mutation query', () => { it('matches snapshot', async () => { @@ -301,36 +318,42 @@ describe('on (query w/ fragment) -> mutation', () => { }); describe('on (query w/ unused fragment) -> mutation', () => { - const queryOp = { - key: 1234, - operationName: 'query', - query: gql` - query { - todos { - id - text - } - users { - ...UserFragment + const queryOp = makeOperation( + 'query', + { + key: 1234, + query: gql` + query { + todos { + id + text + } + users { + ...UserFragment + } } - } - fragment UserFragment on User { - id - name - } - `, - } as Operation; - - const mutationOp = { - key: 5678, - operationName: 'mutation', - query: gql` - mutation MyMutation { - addTodo @populate - } - `, - } as Operation; + fragment UserFragment on User { + id + name + } + `, + }, + context + ); + + const mutationOp = makeOperation( + 'mutation', + { + key: 5678, + query: gql` + mutation MyMutation { + addTodo @populate + } + `, + }, + context + ); describe('mutation query', () => { it('matches snapshot', async () => { @@ -371,32 +394,38 @@ describe('on (query w/ unused fragment) -> mutation', () => { }); describe('on query -> (mutation w/ interface return type)', () => { - const queryOp = { - key: 1234, - operationName: 'query', - query: gql` - query { - todos { - id - name - } - users { - id - text + const queryOp = makeOperation( + 'query', + { + key: 1234, + query: gql` + query { + todos { + id + name + } + users { + id + text + } } - } - `, - } as Operation; - - const mutationOp = { - key: 5678, - operationName: 'mutation', - query: gql` - mutation MyMutation { - removeTodo @populate - } - `, - } as Operation; + `, + }, + context + ); + + const mutationOp = makeOperation( + 'mutation', + { + key: 5678, + query: gql` + mutation MyMutation { + removeTodo @populate + } + `, + }, + context + ); describe('mutation query', () => { it('matches snapshot', async () => { @@ -430,32 +459,38 @@ describe('on query -> (mutation w/ interface return type)', () => { }); describe('on query -> (mutation w/ union return type)', () => { - const queryOp = { - key: 1234, - operationName: 'query', - query: gql` - query { - todos { - id - name - } - users { - id - text + const queryOp = makeOperation( + 'query', + { + key: 1234, + query: gql` + query { + todos { + id + name + } + users { + id + text + } } - } - `, - } as Operation; - - const mutationOp = { - key: 5678, - operationName: 'mutation', - query: gql` - mutation MyMutation { - updateTodo @populate - } - `, - } as Operation; + `, + }, + context + ); + + const mutationOp = makeOperation( + 'mutation', + { + key: 5678, + query: gql` + mutation MyMutation { + updateTodo @populate + } + `, + }, + context + ); describe('mutation query', () => { it('matches snapshot', async () => { @@ -489,33 +524,36 @@ describe('on query -> (mutation w/ union return type)', () => { }); describe('on query -> teardown -> mutation', () => { - const queryOp = { - key: 1234, - operationName: 'query', - query: gql` - query { - todos { - id - text + const queryOp = makeOperation( + 'query', + { + key: 1234, + query: gql` + query { + todos { + id + text + } } - } - `, - } as Operation; - - const teardownOp = { - key: queryOp.key, - operationName: 'teardown', - } as Operation; - - const mutationOp = { - key: 5678, - operationName: 'mutation', - query: gql` - mutation MyMutation { - addTodo @populate - } - `, - } as Operation; + `, + }, + context + ); + + const teardownOp = makeOperation('teardown', queryOp, context); + + const mutationOp = makeOperation( + 'mutation', + { + key: 5678, + query: gql` + mutation MyMutation { + addTodo @populate + } + `, + }, + context + ); describe('mutation query', () => { it('matches snapshot', async () => { @@ -549,30 +587,36 @@ describe('on query -> teardown -> mutation', () => { }); describe('interface returned in mutation', () => { - const queryOp = { - key: 1234, - operationName: 'query', - query: gql` - query { - products { - id - text - price - tax + const queryOp = makeOperation( + 'query', + { + key: 1234, + query: gql` + query { + products { + id + text + price + tax + } } - } - `, - } as Operation; - - const mutationOp = { - key: 5678, - operationName: 'mutation', - query: gql` - mutation MyMutation { - addProduct @populate - } - `, - } as Operation; + `, + }, + context + ); + + const mutationOp = makeOperation( + 'mutation', + { + key: 5678, + query: gql` + mutation MyMutation { + addProduct @populate + } + `, + }, + context + ); it('should correctly make the inline-fragments', () => { const response = pipe( @@ -605,36 +649,42 @@ describe('interface returned in mutation', () => { }); describe('nested interfaces', () => { - const queryOp = { - key: 1234, - operationName: 'query', - query: gql` - query { - products { - id - text - price - tax - store { + const queryOp = makeOperation( + 'query', + { + key: 1234, + query: gql` + query { + products { id - name - address - website + text + price + tax + store { + id + name + address + website + } } } - } - `, - } as Operation; - - const mutationOp = { - key: 5678, - operationName: 'mutation', - query: gql` - mutation MyMutation { - addProduct @populate - } - `, - } as Operation; + `, + }, + context + ); + + const mutationOp = makeOperation( + 'mutation', + { + key: 5678, + query: gql` + mutation MyMutation { + addProduct @populate + } + `, + }, + context + ); it('should correctly make the inline-fragments', () => { const response = pipe( diff --git a/exchanges/populate/src/populateExchange.ts b/exchanges/populate/src/populateExchange.ts index 9de68cd39b..26009717ea 100644 --- a/exchanges/populate/src/populateExchange.ts +++ b/exchanges/populate/src/populateExchange.ts @@ -13,7 +13,7 @@ import { SelectionNode, } from 'graphql'; import { pipe, tap, map } from 'wonka'; -import { Exchange, Operation } from '@urql/core'; +import { makeOperation, Exchange, Operation } from '@urql/core'; import { warn } from './helpers/help'; import { @@ -50,7 +50,7 @@ export const populateExchange = ({ /** Handle mutation and inject selections + fragments. */ const handleIncomingMutation = (op: Operation) => { - if (op.operationName !== 'mutation') { + if (op.kind !== 'mutation') { return op; } @@ -61,20 +61,20 @@ export const populateExchange = ({ ); } - return { - ...op, - query: addFragmentsToQuery( - schema, - op.query, - activeSelections, - userFragments - ), - }; + const newOperation = makeOperation(op.kind, op); + newOperation.query = addFragmentsToQuery( + schema, + op.query, + activeSelections, + userFragments + ); + + return newOperation; }; /** Handle query and extract fragments. */ - const handleIncomingQuery = ({ key, operationName, query }: Operation) => { - if (operationName !== 'query') { + const handleIncomingQuery = ({ key, kind, query }: Operation) => { + if (kind !== 'query') { return; } @@ -106,8 +106,8 @@ export const populateExchange = ({ } }; - const handleIncomingTeardown = ({ key, operationName }: Operation) => { - if (operationName === 'teardown') { + const handleIncomingTeardown = ({ key, kind }: Operation) => { + if (kind === 'teardown') { activeOperations.delete(key); } }; diff --git a/exchanges/refocus/src/refocusExchange.test.ts b/exchanges/refocus/src/refocusExchange.test.ts index c4de8c9bb1..1be6be6c48 100644 --- a/exchanges/refocus/src/refocusExchange.test.ts +++ b/exchanges/refocus/src/refocusExchange.test.ts @@ -87,6 +87,9 @@ it(`attaches a listener and redispatches queries on call`, () => { context: expect.anything(), key: 1, query: queryOne, + kind: 'query', + + // TODO: Remove this when the deprecated "operationName" property is removed operationName: 'query', }); }); diff --git a/exchanges/refocus/src/refocusExchange.ts b/exchanges/refocus/src/refocusExchange.ts index 653cf0554b..96f511e2b3 100644 --- a/exchanges/refocus/src/refocusExchange.ts +++ b/exchanges/refocus/src/refocusExchange.ts @@ -22,12 +22,12 @@ export const refocusExchange = (): Exchange => { }); const processIncomingOperation = (op: Operation) => { - if (op.operationName === 'query' && !observedOperations.has(op.key)) { + if (op.kind === 'query' && !observedOperations.has(op.key)) { observedOperations.set(op.key, 1); watchedOperations.set(op.key, op); } - if (op.operationName === 'teardown' && observedOperations.has(op.key)) { + if (op.kind === 'teardown' && observedOperations.has(op.key)) { observedOperations.delete(op.key); watchedOperations.delete(op.key); } diff --git a/exchanges/request-policy/src/requestPolicyExchange.ts b/exchanges/request-policy/src/requestPolicyExchange.ts index 33bed8900f..7078fab0df 100644 --- a/exchanges/request-policy/src/requestPolicyExchange.ts +++ b/exchanges/request-policy/src/requestPolicyExchange.ts @@ -1,4 +1,4 @@ -import { Operation, Exchange } from '@urql/core'; +import { makeOperation, Operation, Exchange } from '@urql/core'; import { pipe, map } from 'wonka'; const defaultTTL = 5 * 60 * 1000; @@ -33,13 +33,10 @@ export const requestPolicyExchange = (options: Options): Exchange => ({ (options.shouldUpgrade ? options.shouldUpgrade(operation) : true) ) { operations.set(operation.key, new Date()); - return { - ...operation, - context: { - ...operation.context, - requestPolicy: 'cache-and-network', - }, - }; + return makeOperation(operation.kind, operation, { + ...operation.context, + requestPolicy: 'cache-and-network', + }); } return operation; diff --git a/exchanges/retry/src/retryExchange.ts b/exchanges/retry/src/retryExchange.ts index 681d05b27d..3daf89eb83 100644 --- a/exchanges/retry/src/retryExchange.ts +++ b/exchanges/retry/src/retryExchange.ts @@ -10,6 +10,7 @@ import { takeUntil, } from 'wonka'; import { + makeOperation, Exchange, Operation, CombinedError, @@ -68,9 +69,7 @@ export const retryExchange = ({ sharedOps$, filter(op => { return ( - (op.operationName === 'query' || - op.operationName === 'teardown') && - op.key === key + (op.kind === 'query' || op.kind === 'teardown') && op.key === key ); }) ); @@ -86,14 +85,13 @@ export const retryExchange = ({ // Add new retryDelay and retryCount to operation return pipe( - fromValue({ - ...op, - context: { + fromValue( + makeOperation(op.kind, op, { ...op.context, retryDelay: delayAmount, retryCount, - }, - }), + }) + ), delay(delayAmount), // Stop retry if a teardown comes in takeUntil(teardown$) diff --git a/exchanges/suspense/src/suspenseExchange.ts b/exchanges/suspense/src/suspenseExchange.ts index a010d31759..f7f7f50f45 100644 --- a/exchanges/suspense/src/suspenseExchange.ts +++ b/exchanges/suspense/src/suspenseExchange.ts @@ -32,9 +32,9 @@ export const suspenseExchange: Exchange = ({ client, forward }) => { // Every uncached operation that isn't skipped will be marked as immediate and forwarded const forwardResults$ = pipe( sharedOps$, - filter(op => op.operationName !== 'query' || !isOperationCached(op)), + filter(op => op.kind !== 'query' || !isOperationCached(op)), onPush(op => { - if (op.operationName === 'query') keys.add(op.key); + if (op.kind === 'query') keys.add(op.key); }), forward, share @@ -43,7 +43,7 @@ export const suspenseExchange: Exchange = ({ client, forward }) => { // Results that are skipped by suspense (mutations) const ignoredResults$ = pipe( forwardResults$, - filter(res => res.operation.operationName !== 'query') + filter(res => res.operation.kind !== 'query') ); // Results that may have suspended since they did not resolve synchronously @@ -51,8 +51,7 @@ export const suspenseExchange: Exchange = ({ client, forward }) => { forwardResults$, filter(res => { return ( - res.operation.operationName === 'query' && - !isOperationCached(res.operation) + res.operation.kind === 'query' && !isOperationCached(res.operation) ); }), onPush((res: OperationResult) => { @@ -70,9 +69,9 @@ export const suspenseExchange: Exchange = ({ client, forward }) => { // deferredResults$ ignores it const immediateResults$ = pipe( sharedOps$, - filter(op => op.operationName === 'query' && !isOperationCached(op)), + filter(op => op.kind === 'query' && !isOperationCached(op)), onPush(op => { - if (op.operationName === 'query') keys.delete(op.key); + if (op.kind === 'query') keys.delete(op.key); }), filter(() => false) ); @@ -81,7 +80,7 @@ export const suspenseExchange: Exchange = ({ client, forward }) => { // by the suspenseExchange, and will be deleted from the cache immediately after const cachedResults$ = pipe( sharedOps$, - filter(op => op.operationName === 'query' && isOperationCached(op)), + filter(op => op.kind === 'query' && isOperationCached(op)), map(op => { const { key } = op; const result = cache.get(key) as OperationResult; diff --git a/packages/core/src/client.test.ts b/packages/core/src/client.test.ts index 89caa95dd8..71128ef25c 100755 --- a/packages/core/src/client.test.ts +++ b/packages/core/src/client.test.ts @@ -3,7 +3,7 @@ import gql from 'graphql-tag'; /** NOTE: Testing in this file is designed to test both the client and its interaction with default Exchanges */ -import { map, pipe, subscribe, filter, toArray, tap } from 'wonka'; +import { Source, map, pipe, subscribe, filter, toArray, tap } from 'wonka'; import { Exchange, Operation, OperationResult } from './types'; import { createClient } from './client'; import { queryOperation } from './test-utils'; @@ -32,9 +32,9 @@ const query = { variables: { example: 1234 }, }; -let receivedOps: any[] = []; +let receivedOps: Operation[] = []; let client = createClient({ url: '1234' }); -const receiveMock = jest.fn(s => +const receiveMock = jest.fn((s: Source) => pipe( s, tap(op => (receivedOps = [...receivedOps, op])), @@ -86,7 +86,7 @@ describe('promisified methods', () => { expect(print(received.query)).toEqual(print(query.query)); expect(received.key).toBeDefined(); expect(received.variables).toEqual({ example: 1234 }); - expect(received.operationName).toEqual('query'); + expect(received.kind).toEqual('query'); expect(received.context).toEqual({ url: 'https://hostname.com', requestPolicy: 'cache-only', @@ -116,7 +116,7 @@ describe('promisified methods', () => { expect(print(received.query)).toEqual(print(query.query)); expect(received.key).toBeDefined(); expect(received.variables).toEqual({ example: 1234 }); - expect(received.operationName).toEqual('mutation'); + expect(received.kind).toEqual('mutation'); expect(received.context).toEqual({ url: 'https://hostname.com', requestPolicy: 'cache-and-network', @@ -142,13 +142,16 @@ describe('synchronous methods', () => { ); expect(receivedOps.length).toBe(2); - expect(receivedOps[0].operationName).toBe('query'); - expect(receivedOps[1].operationName).toBe('teardown'); + expect(receivedOps[0].kind).toBe('query'); + expect(receivedOps[1].kind).toBe('teardown'); expect(result).toEqual({ operation: { ...query, context: expect.anything(), key: expect.any(Number), + kind: 'query', + + // TODO: Remove this when the deprecated `operationName` property is removed operationName: 'query', }, }); @@ -199,13 +202,13 @@ describe('executeQuery', () => { ); }); - it('passes operationName type to exchange', () => { + it('passes kind type to exchange', () => { pipe( client.executeQuery(query), subscribe(x => x) ); - expect(receivedOps[0]).toHaveProperty('operationName', 'query'); + expect(receivedOps[0]).toHaveProperty('kind', 'query'); }); it('passes url (from context) to exchange', () => { @@ -227,11 +230,11 @@ describe('executeQuery', () => { expect(receivedOps.length).toEqual(1); jest.advanceTimersByTime(200); expect(receivedOps.length).toEqual(5); - expect(receivedOps[0].operationName).toEqual('query'); - expect(receivedOps[1].operationName).toEqual('teardown'); - expect(receivedOps[2].operationName).toEqual('query'); - expect(receivedOps[3].operationName).toEqual('teardown'); - expect(receivedOps[4].operationName).toEqual('query'); + expect(receivedOps[0].kind).toEqual('query'); + expect(receivedOps[1].kind).toEqual('teardown'); + expect(receivedOps[2].kind).toEqual('query'); + expect(receivedOps[3].kind).toEqual('teardown'); + expect(receivedOps[4].kind).toEqual('query'); unsubscribe(); }); }); @@ -256,13 +259,13 @@ describe('executeMutation', () => { expect(receivedOps[0]).toHaveProperty('variables', query.variables); }); - it('passes operationName type to exchange', () => { + it('passes kind type to exchange', () => { pipe( client.executeMutation(query), subscribe(x => x) ); - expect(receivedOps[0]).toHaveProperty('operationName', 'mutation'); + expect(receivedOps[0]).toHaveProperty('kind', 'mutation'); }); it('passes url (from context) to exchange', () => { @@ -295,13 +298,13 @@ describe('executeSubscription', () => { expect(receivedOps[0]).toHaveProperty('variables', query.variables); }); - it('passes operationName type to exchange', () => { + it('passes kind type to exchange', () => { pipe( client.executeSubscription(query), subscribe(x => x) ); - expect(receivedOps[0]).toHaveProperty('operationName', 'subscription'); + expect(receivedOps[0]).toHaveProperty('kind', 'subscription'); }); }); @@ -312,7 +315,7 @@ describe('queuing behavior', () => { const exchange: Exchange = ({ client }) => ops$ => { return pipe( ops$, - filter(op => op.operationName !== 'teardown'), + filter(op => op.kind !== 'teardown'), tap(op => { output.push(op); if ( diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index b3c6dda85d..8ac039d61b 100755 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -42,6 +42,7 @@ import { withPromise, maskTypename, noop, + makeOperation, } from './utils'; import { DocumentNode } from 'graphql'; @@ -136,7 +137,7 @@ export class Client { // Reexecute operation only if any subscribers are still subscribed to the // operation's exchange results if ( - operation.operationName === 'mutation' || + operation.kind === 'mutation' || (this.activeOperations[operation.key] || 0) > 0 ) { this.queue.push(operation); @@ -169,7 +170,7 @@ export class Client { pipe(this.results$, publish); } - private createOperationContext = ( + createOperationContext = ( opts?: Partial ): OperationContext => ({ url: this.url, @@ -181,16 +182,11 @@ export class Client { }); createRequestOperation = ( - type: OperationType, + kind: OperationType, request: GraphQLRequest, opts?: Partial - ): Operation => ({ - key: request.key, - query: request.query, - variables: request.variables, - operationName: type, - context: this.createOperationContext(opts), - }); + ): Operation => + makeOperation(kind, request, this.createOperationContext(opts)); /** Counts up the active operation key and dispatches the operation */ private onOperationStart(operation: Operation) { @@ -207,13 +203,15 @@ export class Client { prevActive <= 0 ? 0 : prevActive - 1); if (newActive <= 0) { - this.dispatchOperation({ ...operation, operationName: 'teardown' }); + this.dispatchOperation( + makeOperation('teardown', operation, operation.context) + ); } } /** Executes an Operation by sending it through the exchange pipeline It returns an observable that emits all related exchange results and keeps track of this observable's subscribers. A teardown signal will be emitted when no subscribers are listening anymore. */ executeRequestOperation(operation: Operation): Source { - const { key, operationName } = operation; + const { key, kind } = operation; let operationResults$ = pipe( this.results$, filter((res: OperationResult) => res.operation.key === key) @@ -229,7 +227,7 @@ export class Client { ); } - if (operationName === 'mutation') { + if (kind === 'mutation') { // A mutation is always limited to just a single result and is never shared return pipe( operationResults$, @@ -240,9 +238,7 @@ export class Client { const teardown$ = pipe( this.operations$, - filter( - (op: Operation) => op.operationName === 'teardown' && op.key === key - ) + filter((op: Operation) => op.kind === 'teardown' && op.key === key) ); const result$ = pipe( @@ -258,7 +254,7 @@ export class Client { return operation.context.suspense !== false && this.suspense && - operationName === 'query' + kind === 'query' ? toSuspenseSource(result$ as Source) : (result$ as Source); } diff --git a/packages/core/src/exchanges/__snapshots__/fetch.test.ts.snap b/packages/core/src/exchanges/__snapshots__/fetch.test.ts.snap index afac1ebe20..dac2c06046 100644 --- a/packages/core/src/exchanges/__snapshots__/fetch.test.ts.snap +++ b/packages/core/src/exchanges/__snapshots__/fetch.test.ts.snap @@ -14,6 +14,7 @@ Object { "url": "http://localhost:3000/graphql", }, "key": 2, + "kind": "query", "operationName": "query", "query": Object { "definitions": Array [ @@ -153,6 +154,7 @@ Object { "url": "http://localhost:3000/graphql", }, "key": 2, + "kind": "query", "operationName": "query", "query": Object { "definitions": Array [ @@ -294,6 +296,7 @@ Object { "url": "http://localhost:3000/graphql", }, "key": 2, + "kind": "query", "operationName": "query", "query": Object { "definitions": Array [ diff --git a/packages/core/src/exchanges/__snapshots__/subscription.test.ts.snap b/packages/core/src/exchanges/__snapshots__/subscription.test.ts.snap index 546cc7ca03..91058c3795 100644 --- a/packages/core/src/exchanges/__snapshots__/subscription.test.ts.snap +++ b/packages/core/src/exchanges/__snapshots__/subscription.test.ts.snap @@ -14,6 +14,7 @@ Object { "url": "http://localhost:3000/graphql", }, "key": 4, + "kind": "subscription", "operationName": "subscription", "query": Object { "definitions": Array [ diff --git a/packages/core/src/exchanges/cache.ts b/packages/core/src/exchanges/cache.ts index 8df20c2e10..a3328910b7 100755 --- a/packages/core/src/exchanges/cache.ts +++ b/packages/core/src/exchanges/cache.ts @@ -3,7 +3,9 @@ import { filter, map, merge, pipe, share, tap } from 'wonka'; import { Client } from '../client'; import { Exchange, Operation, OperationResult, ExchangeInput } from '../types'; + import { + makeOperation, addMetadata, collectTypesFromResponse, formatDocument, @@ -15,18 +17,19 @@ interface OperationCache { [key: string]: Set; } -const shouldSkip = ({ operationName }: Operation) => - operationName !== 'mutation' && operationName !== 'query'; +const shouldSkip = ({ kind }: Operation) => + kind !== 'mutation' && kind !== 'query'; export const cacheExchange: Exchange = ({ forward, client, dispatchDebug }) => { const resultCache = new Map() as ResultCache; const operationCache = Object.create(null) as OperationCache; // Adds unique typenames to query (for invalidating cache entries) - const mapTypeNames = (operation: Operation): Operation => ({ - ...operation, - query: formatDocument(operation.query), - }); + const mapTypeNames = (operation: Operation): Operation => { + const formattedOperation = makeOperation(operation.kind, operation); + formattedOperation.query = formatDocument(operation.query); + return formattedOperation; + }; const handleAfterMutation = afterMutation( resultCache, @@ -37,14 +40,14 @@ export const cacheExchange: Exchange = ({ forward, client, dispatchDebug }) => { const handleAfterQuery = afterQuery(resultCache, operationCache); - const isOperationCached = operation => { + const isOperationCached = (operation: Operation) => { const { key, - operationName, + kind, context: { requestPolicy }, } = operation; return ( - operationName === 'query' && + kind === 'query' && requestPolicy !== 'network-only' && (requestPolicy === 'cache-only' || resultCache.has(key)) ); @@ -102,21 +105,13 @@ export const cacheExchange: Exchange = ({ forward, client, dispatchDebug }) => { ]), map(op => addMetadata(op, { cacheOutcome: 'miss' })), filter( - op => - op.operationName !== 'query' || - op.context.requestPolicy !== 'cache-only' + op => op.kind !== 'query' || op.context.requestPolicy !== 'cache-only' ), forward, tap(response => { - if ( - response.operation && - response.operation.operationName === 'mutation' - ) { + if (response.operation && response.operation.kind === 'mutation') { handleAfterMutation(response); - } else if ( - response.operation && - response.operation.operationName === 'query' - ) { + } else if (response.operation && response.operation.kind === 'query') { handleAfterQuery(response); } }) @@ -128,13 +123,12 @@ export const cacheExchange: Exchange = ({ forward, client, dispatchDebug }) => { // Reexecutes a given operation with the default requestPolicy const reexecuteOperation = (client: Client, operation: Operation) => { - return client.reexecuteOperation({ - ...operation, - context: { + return client.reexecuteOperation( + makeOperation(operation.kind, operation, { ...operation.context, requestPolicy: 'network-only', - }, - }); + }) + ); }; // Invalidates the cache given a mutation's response diff --git a/packages/core/src/exchanges/dedup.test.ts b/packages/core/src/exchanges/dedup.test.ts index c59606bba9..c11773a91b 100644 --- a/packages/core/src/exchanges/dedup.test.ts +++ b/packages/core/src/exchanges/dedup.test.ts @@ -14,6 +14,7 @@ import { } from '../test-utils'; import { Operation } from '../types'; import { dedupExchange } from './dedup'; +import { makeOperation } from '../utils'; const dispatchDebug = jest.fn(); let shouldRespond = false; @@ -82,7 +83,7 @@ it('forwards duplicate query operations after one was torn down', async () => { publish(exchange); next(queryOperation); - next({ ...queryOperation, operationName: 'teardown' }); + next(makeOperation('teardown', queryOperation, queryOperation.context)); next(queryOperation); complete(); expect(forwardedOperations.length).toBe(3); diff --git a/packages/core/src/exchanges/dedup.ts b/packages/core/src/exchanges/dedup.ts index f55ad44bbe..c1e701cdd5 100644 --- a/packages/core/src/exchanges/dedup.ts +++ b/packages/core/src/exchanges/dedup.ts @@ -6,13 +6,13 @@ export const dedupExchange: Exchange = ({ forward, dispatchDebug }) => { const inFlightKeys = new Set(); const filterIncomingOperation = (operation: Operation) => { - const { key, operationName } = operation; - if (operationName === 'teardown') { + const { key, kind } = operation; + if (kind === 'teardown') { inFlightKeys.delete(key); return true; } - if (operationName !== 'query' && operationName !== 'subscription') { + if (kind !== 'query' && kind !== 'subscription') { return true; } diff --git a/packages/core/src/exchanges/fallback.ts b/packages/core/src/exchanges/fallback.ts index 8be1c2b9c4..0e95cd3f40 100644 --- a/packages/core/src/exchanges/fallback.ts +++ b/packages/core/src/exchanges/fallback.ts @@ -12,10 +12,10 @@ export const fallbackExchange: ({ ops$, tap(operation => { if ( - operation.operationName !== 'teardown' && + operation.kind !== 'teardown' && process.env.NODE_ENV !== 'production' ) { - const message = `No exchange has handled operations of type "${operation.operationName}". Check whether you've added an exchange responsible for these operations.`; + const message = `No exchange has handled operations of kind "${operation.kind}". Check whether you've added an exchange responsible for these operations.`; dispatchDebug({ type: 'fallbackCatch', diff --git a/packages/core/src/exchanges/fetch.test.ts b/packages/core/src/exchanges/fetch.test.ts index b965c84425..5320af06d9 100755 --- a/packages/core/src/exchanges/fetch.test.ts +++ b/packages/core/src/exchanges/fetch.test.ts @@ -1,8 +1,9 @@ import { empty, fromValue, pipe, Source, subscribe, toPromise } from 'wonka'; import { Client } from '../client'; +import { makeOperation } from '../utils'; import { queryOperation } from '../test-utils'; -import { OperationResult, OperationType } from '../types'; +import { OperationResult } from '../types'; import { fetchExchange } from './fetch'; const fetch = (global as any).fetch as jest.Mock; @@ -160,10 +161,9 @@ describe('on teardown', () => { it('does not call the query', () => { pipe( - fromValue({ - ...queryOperation, - operationName: 'teardown' as OperationType, - }), + fromValue( + makeOperation('teardown', queryOperation, queryOperation.context) + ), fetchExchange(exchangeArgs), subscribe(fail) ); diff --git a/packages/core/src/exchanges/fetch.ts b/packages/core/src/exchanges/fetch.ts index aef6e4bd78..9e4f17823c 100755 --- a/packages/core/src/exchanges/fetch.ts +++ b/packages/core/src/exchanges/fetch.ts @@ -16,16 +16,13 @@ export const fetchExchange: Exchange = ({ forward, dispatchDebug }) => { const fetchResults$ = pipe( sharedOps$, filter(operation => { - return ( - operation.operationName === 'query' || - operation.operationName === 'mutation' - ); + return operation.kind === 'query' || operation.kind === 'mutation'; }), mergeMap(operation => { const { key } = operation; const teardown$ = pipe( sharedOps$, - filter(op => op.operationName === 'teardown' && op.key === key) + filter(op => op.kind === 'teardown' && op.key === key) ); const body = makeFetchBody(operation); @@ -68,10 +65,7 @@ export const fetchExchange: Exchange = ({ forward, dispatchDebug }) => { const forward$ = pipe( sharedOps$, filter(operation => { - return ( - operation.operationName !== 'query' && - operation.operationName !== 'mutation' - ); + return operation.kind !== 'query' && operation.kind !== 'mutation'; }), forward ); diff --git a/packages/core/src/exchanges/ssr.ts b/packages/core/src/exchanges/ssr.ts index edb21646e2..7fda8aff3a 100644 --- a/packages/core/src/exchanges/ssr.ts +++ b/packages/core/src/exchanges/ssr.ts @@ -27,8 +27,8 @@ export interface SSRExchange extends Exchange { extractData(): SSRData; } -const shouldSkip = ({ operationName }: Operation) => - operationName !== 'subscription' && operationName !== 'query'; +const shouldSkip = ({ kind }: Operation) => + kind !== 'subscription' && kind !== 'query'; /** Serialize an OperationResult to plain JSON */ const serializeResult = ({ diff --git a/packages/core/src/exchanges/subscription.ts b/packages/core/src/exchanges/subscription.ts index 0378a0adae..2b900a11f9 100644 --- a/packages/core/src/exchanges/subscription.ts +++ b/packages/core/src/exchanges/subscription.ts @@ -11,7 +11,7 @@ import { takeUntil, } from 'wonka'; -import { makeResult, makeErrorResult } from '../utils'; +import { makeResult, makeErrorResult, makeOperation } from '../utils'; import { Exchange, @@ -85,11 +85,10 @@ export const subscriptionExchange = ({ complete: () => { if (!isComplete) { isComplete = true; - if (operation.operationName === 'subscription') { - client.reexecuteOperation({ - ...operation, - operationName: 'teardown', - }); + if (operation.kind === 'subscription') { + client.reexecuteOperation( + makeOperation('teardown', operation, operation.context) + ); } complete(); @@ -106,11 +105,10 @@ export const subscriptionExchange = ({ }; const isSubscriptionOperation = (operation: Operation): boolean => { - const { operationName } = operation; + const { kind } = operation; return ( - operationName === 'subscription' || - (!!enableAllOperations && - (operationName === 'query' || operationName === 'mutation')) + kind === 'subscription' || + (!!enableAllOperations && (kind === 'query' || kind === 'mutation')) ); }; @@ -123,7 +121,7 @@ export const subscriptionExchange = ({ const { key } = operation; const teardown$ = pipe( sharedOps$, - filter(op => op.operationName === 'teardown' && op.key === key) + filter(op => op.kind === 'teardown' && op.key === key) ); return pipe(createSubscriptionSource(operation), takeUntil(teardown$)); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 0c631ea4db..11ee0ea712 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -10,4 +10,5 @@ export { makeErrorResult, formatDocument, maskTypename, + makeOperation, } from './utils'; diff --git a/packages/core/src/internal/__snapshots__/fetchSource.test.ts.snap b/packages/core/src/internal/__snapshots__/fetchSource.test.ts.snap index fc902b5973..f7319ebe96 100644 --- a/packages/core/src/internal/__snapshots__/fetchSource.test.ts.snap +++ b/packages/core/src/internal/__snapshots__/fetchSource.test.ts.snap @@ -14,6 +14,7 @@ Object { "url": "http://localhost:3000/graphql", }, "key": 2, + "kind": "query", "operationName": "query", "query": Object { "definitions": Array [ @@ -143,6 +144,7 @@ Object { "url": "http://localhost:3000/graphql", }, "key": 2, + "kind": "query", "operationName": "query", "query": Object { "definitions": Array [ @@ -272,6 +274,7 @@ Object { "url": "http://localhost:3000/graphql", }, "key": 2, + "kind": "query", "operationName": "query", "query": Object { "definitions": Array [ @@ -405,6 +408,7 @@ Object { "url": "http://localhost:3000/graphql", }, "key": 2, + "kind": "query", "operationName": "query", "query": Object { "definitions": Array [ @@ -554,6 +558,7 @@ Object { "url": "http://localhost:3000/graphql", }, "key": 2, + "kind": "query", "operationName": "query", "query": Object { "definitions": Array [ diff --git a/packages/core/src/internal/fetchOptions.ts b/packages/core/src/internal/fetchOptions.ts index 244f3592b4..54bc7760a2 100644 --- a/packages/core/src/internal/fetchOptions.ts +++ b/packages/core/src/internal/fetchOptions.ts @@ -1,6 +1,6 @@ -import { Kind, print, DocumentNode } from 'graphql'; +import { DocumentNode, print } from 'graphql'; -import { stringifyVariables } from '../utils'; +import { getOperationName, stringifyVariables } from '../utils'; import { Operation } from '../types'; export interface FetchBody { @@ -10,19 +10,8 @@ export interface FetchBody { extensions: undefined | Record; } -const getOperationName = (query: DocumentNode): string | undefined => { - for (let i = 0, l = query.definitions.length; i < l; i++) { - const node = query.definitions[i]; - if (node.kind === Kind.OPERATION_DEFINITION && node.name) { - return node.name.value; - } - } -}; - const shouldUseGet = (operation: Operation): boolean => { - return ( - operation.operationName === 'query' && !!operation.context.preferGetMethod - ); + return operation.kind === 'query' && !!operation.context.preferGetMethod; }; export const makeFetchBody = (request: { diff --git a/packages/core/src/test-utils/samples.ts b/packages/core/src/test-utils/samples.ts index 63fb69434f..947a7b7c46 100644 --- a/packages/core/src/test-utils/samples.ts +++ b/packages/core/src/test-utils/samples.ts @@ -7,6 +7,7 @@ import { OperationContext, OperationResult, } from '../types'; +import { makeOperation } from '../utils'; const context: OperationContext = { fetchOptions: { @@ -60,37 +61,45 @@ export const subscriptionGql: GraphQLRequest = { }, }; -export const teardownOperation: Operation = { - query: queryGql.query, - variables: queryGql.variables, - key: queryGql.key, - operationName: 'teardown', - context, -}; +export const queryOperation: Operation = makeOperation( + 'query', + { + query: queryGql.query, + variables: queryGql.variables, + key: queryGql.key, + }, + context +); -export const queryOperation: Operation = { - query: teardownOperation.query, - variables: teardownOperation.variables, - key: teardownOperation.key, - operationName: 'query', - context, -}; +export const teardownOperation: Operation = makeOperation( + 'teardown', + { + query: queryOperation.query, + variables: queryOperation.variables, + key: queryOperation.key, + }, + context +); -export const mutationOperation: Operation = { - query: mutationGql.query, - variables: mutationGql.variables, - key: mutationGql.key, - operationName: 'mutation', - context, -}; +export const mutationOperation: Operation = makeOperation( + 'mutation', + { + query: mutationGql.query, + variables: mutationGql.variables, + key: mutationGql.key, + }, + context +); -export const subscriptionOperation: Operation = { - query: subscriptionGql.query, - variables: subscriptionGql.variables, - key: subscriptionGql.key, - operationName: 'subscription', - context, -}; +export const subscriptionOperation: Operation = makeOperation( + 'subscription', + { + query: subscriptionGql.query, + variables: subscriptionGql.variables, + key: subscriptionGql.key, + }, + context +); export const undefinedQueryResponse: OperationResult = { operation: queryOperation, diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index b690cf58a5..f9b5a9b7ae 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -54,8 +54,11 @@ export interface OperationContext { /** A [query]{@link Query} or [mutation]{@link Mutation} with additional metadata for use during transmission. */ export interface Operation extends GraphQLRequest { - operationName: OperationType; + readonly kind: OperationType; context: OperationContext; + + /** @deprecated use Operation.kind instead */ + readonly operationName: OperationType; } /** Resulting data from an [operation]{@link Operation}. */ diff --git a/packages/core/src/utils/deprecation.test.ts b/packages/core/src/utils/deprecation.test.ts new file mode 100644 index 0000000000..a4fd0383c6 --- /dev/null +++ b/packages/core/src/utils/deprecation.test.ts @@ -0,0 +1,27 @@ +import { _clearWarnings, deprecationWarning } from './deprecation'; + +describe('deprecationWarning()', () => { + let warn: jest.SpyInstance; + + const key = 'deprecation.test'; + const message = 'Test deprecation message.'; + + beforeAll(() => { + warn = jest.spyOn(console, 'warn'); + }); + + afterEach(_clearWarnings); + + afterAll(() => { + warn.mockRestore(); + }); + + it('only calls console.warn once per key', () => { + deprecationWarning({ key, message }); + deprecationWarning({ key, message }); + deprecationWarning({ key, message }); + + expect(warn).toBeCalledTimes(1); + expect(warn).toBeCalledWith(`[WARNING: Deprecated] ${message}`); + }); +}); diff --git a/packages/core/src/utils/deprecation.ts b/packages/core/src/utils/deprecation.ts new file mode 100644 index 0000000000..f1abf4e88e --- /dev/null +++ b/packages/core/src/utils/deprecation.ts @@ -0,0 +1,29 @@ +export interface Warning { + key: string; + message: string; +} + +/** + * Module-scoped state to track if deprecation warnings have already been issued + * for a particular key. + */ +let issuedWarnings: Record = {}; + +/** + * If a deprecation warning has not already been issued, use `console.warn()` to + * issue it with an eye-catching prefix string. + */ +export const deprecationWarning = ({ key, message }: Warning) => { + if (!issuedWarnings[key]) { + console.warn(`[WARNING: Deprecated] ${message}`); + + issuedWarnings[key] = true; + } +}; + +/** + * Clears all issued warnings - intended for use in testing. + */ +export const _clearWarnings = () => { + issuedWarnings = {}; +}; diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index a420bbdd4e..3ab0237f5c 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -6,6 +6,8 @@ export * from './toSuspenseSource'; export * from './stringifyVariables'; export * from './maskTypename'; export * from './withPromise'; +export * from './operation'; +export * from './deprecation'; export const noop = () => { /* noop */ diff --git a/packages/core/src/utils/operation.ts b/packages/core/src/utils/operation.ts new file mode 100644 index 0000000000..40fae033f2 --- /dev/null +++ b/packages/core/src/utils/operation.ts @@ -0,0 +1,61 @@ +import { + GraphQLRequest, + Operation, + OperationContext, + OperationType, +} from '../types'; +import { Warning, deprecationWarning } from './deprecation'; + +// TODO: Remove when the deprecated `operationName` property is removed +const DEPRECATED: Record = { + operationName: { + key: 'Operation.operationName', + message: + 'The "Operation.operationName" property has been deprecated and will be removed in a future release of urql. Use "Operation.kind" instead.', + }, +}; + +function makeOperation( + kind: OperationType, + request: GraphQLRequest, + context: OperationContext +): Operation; +function makeOperation( + kind: OperationType, + request: Operation, + context?: OperationContext +): Operation; + +function makeOperation(kind, request, context) { + if (!context) context = request.context; + + return { + key: request.key, + query: request.query, + variables: request.variables, + kind, + context, + + get operationName(): OperationType { + deprecationWarning(DEPRECATED.operationName); + + return this.kind; + }, + }; +} + +export { makeOperation }; + +/** Spreads the provided metadata to the source operation's meta property in context. */ +export const addMetadata = ( + operation: Operation, + meta: OperationContext['meta'] +) => { + return makeOperation(operation.kind, operation, { + ...operation.context, + meta: { + ...operation.context.meta, + ...meta, + }, + }); +}; diff --git a/packages/core/src/utils/request.ts b/packages/core/src/utils/request.ts index b1813b9313..6fc663dd62 100644 --- a/packages/core/src/utils/request.ts +++ b/packages/core/src/utils/request.ts @@ -1,7 +1,7 @@ -import { DocumentNode, parse, print } from 'graphql'; +import { DocumentNode, Kind, parse, print } from 'graphql'; import { hash, phash } from './hash'; import { stringifyVariables } from './stringifyVariables'; -import { GraphQLRequest, Operation, OperationContext } from '../types'; +import { GraphQLRequest } from '../types'; interface Documents { [key: number]: DocumentNode; @@ -40,17 +40,14 @@ export const createRequest = ( }; }; -/** Spreads the provided metadata to the source operation's meta property in context. */ -export const addMetadata = ( - source: Operation, - meta: Exclude -) => ({ - ...source, - context: { - ...source.context, - meta: { - ...source.context.meta, - ...meta, - }, - }, -}); +/** + * Finds the Name value from the OperationDefinition of a Document + */ +export const getOperationName = (query: DocumentNode): string | undefined => { + for (let i = 0, l = query.definitions.length; i < l; i++) { + const node = query.definitions[i]; + if (node.kind === Kind.OPERATION_DEFINITION && node.name) { + return node.name.value; + } + } +}; diff --git a/packages/react-urql/src/test-utils/ssr.test.tsx b/packages/react-urql/src/test-utils/ssr.test.tsx index 3342429d21..872ef64a1f 100644 --- a/packages/react-urql/src/test-utils/ssr.test.tsx +++ b/packages/react-urql/src/test-utils/ssr.test.tsx @@ -13,6 +13,7 @@ import { GraphQLRequest, Operation, OperationResult, + makeOperation, } from '@urql/core'; import { Provider } from '../context'; @@ -42,21 +43,25 @@ export const queryGql: GraphQLRequest = { }, }; -const teardownOperation: Operation = { - query: queryGql.query, - variables: queryGql.variables, - key: queryGql.key, - operationName: 'teardown', - context, -}; - -const queryOperation: Operation = { - query: teardownOperation.query, - variables: teardownOperation.variables, - key: teardownOperation.key, - operationName: 'query', - context, -}; +const teardownOperation: Operation = makeOperation( + 'teardown', + { + query: queryGql.query, + variables: queryGql.variables, + key: queryGql.key, + }, + context +); + +const queryOperation: Operation = makeOperation( + 'query', + { + query: teardownOperation.query, + variables: teardownOperation.variables, + key: teardownOperation.key, + }, + context +); const queryResponse: OperationResult = { operation: queryOperation, @@ -77,7 +82,7 @@ describe('server-side rendering', () => { const fetchExchange: Exchange = () => ops$ => { return pipe( ops$, - filter(x => x.operationName === 'query'), + filter(x => x.kind === 'query'), delay(100), map(operation => ({ ...queryResponse, operation })) ); diff --git a/scripts/babel/transform-invariant-warning.js b/scripts/babel/transform-invariant-warning.js index 4232a90f47..074b6a162a 100644 --- a/scripts/babel/transform-invariant-warning.js +++ b/scripts/babel/transform-invariant-warning.js @@ -7,16 +7,18 @@ const warningDevCheckTemplate = ` `.trim(); const plugin = ({ template, types: t }) => { - const wrapWithDevCheck = template( - warningDevCheckTemplate, - { placeholderPattern: /^NODE$/ } - ); + const wrapWithDevCheck = template(warningDevCheckTemplate, { + placeholderPattern: /^NODE$/, + }); return { visitor: { CallExpression(path) { const { name } = path.node.callee; - if ((name === 'warn') && !path.node[visited]) { + if ( + (name === 'warn' || name === 'deprecationWarning') && + !path.node[visited] + ) { path.node[visited] = true; // The production-check may be hoisted if the parent @@ -63,10 +65,10 @@ const plugin = ({ template, types: t }) => { ), formerNode, t.stringLiteral('') - ) + ); } - } - } + }, + }, }; };