Skip to content
This repository has been archived by the owner on Jul 6, 2020. It is now read-only.

Refactor queries to allow for cascaded partial results #59

Merged
merged 18 commits into from
Sep 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 49 additions & 30 deletions src/ast/schemaPredicates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import warning from 'warning';
import {
buildClientSchema,
isNullableType,
isListType,
isNonNullType,
GraphQLSchema,
GraphQLAbstractType,
GraphQLObjectType,
Expand All @@ -19,39 +21,18 @@ export class SchemaPredicates {
}

isFieldNullable(typename: string, fieldName: string): boolean {
const type = this.schema.getType(typename);
expectObjectType(type, typename);

const object = type as GraphQLObjectType;
if (object === undefined) {
warning(
false,
'Invalid type: The type `%s` is not a type in the defined schema, ' +
'but the GraphQL document expects it to exist.\n' +
'Traversal will continue, however this may lead to undefined behavior!',
typename
);

return false;
}

const field = object.getFields()[fieldName];
if (field === undefined) {
warning(
false,
'Invalid field: The field `%s` does not exist on `%s`, ' +
'but the GraphQL document expects it to exist.\n' +
'Traversal will continue, however this may lead to undefined behavior!',
fieldName,
typename
);

return false;
}

const field = getField(this.schema, typename, fieldName);
if (field === undefined) return false;
return isNullableType(field.type);
}

isListNullable(typename: string, fieldName: string): boolean {
const field = getField(this.schema, typename, fieldName);
if (field === undefined) return false;
const ofType = isNonNullType(field.type) ? field.type.ofType : field.type;
return isListType(ofType) && isNullableType(ofType.ofType);
}

isInterfaceOfType(
typeCondition: null | string,
typename: string | void
Expand All @@ -70,6 +51,44 @@ export class SchemaPredicates {
}
}

const getField = (
schema: GraphQLSchema,
typename: string,
fieldName: string
) => {
const type = schema.getType(typename);
expectObjectType(type, typename);

const object = type as GraphQLObjectType;
if (object === undefined) {
warning(
false,
'Invalid type: The type `%s` is not a type in the defined schema, ' +
'but the GraphQL document expects it to exist.\n' +
'Traversal will continue, however this may lead to undefined behavior!',
typename
);

return undefined;
}

const field = object.getFields()[fieldName];
if (field === undefined) {
warning(
false,
'Invalid field: The field `%s` does not exist on `%s`, ' +
'but the GraphQL document expects it to exist.\n' +
'Traversal will continue, however this may lead to undefined behavior!',
fieldName,
typename
);

return undefined;
}

return field;
};

const expectObjectType = (type: any, typename: string) => {
invariant(
type instanceof GraphQLObjectType,
Expand Down
32 changes: 23 additions & 9 deletions src/exchange.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,7 @@ it('follows nested resolvers for mutations', () => {
{
__typename: 'Author',
id: '123',
book: null,
name: '[REDACTED ONLINE]',
},
{
Expand All @@ -519,6 +520,7 @@ it('follows nested resolvers for mutations', () => {
__typename: 'Author',
id: '123',
name: '[REDACTED ONLINE]',
book: null,
},
{
__typename: 'Author',
Expand All @@ -537,8 +539,7 @@ it('follows nested resolvers for mutations', () => {
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
return { operation: queryOperation, data: queryData };
}
if (forwardOp.key === 2) {
} else if (forwardOp.key === 2) {
return { operation: mutationOperation, data: mutationData };
}

Expand Down Expand Up @@ -623,7 +624,7 @@ it('follows nested resolvers for mutations', () => {
]);
});

it.only('reexecutes query and returns data on partial result', () => {
it('reexecutes query and returns data on partial result', () => {
jest.useFakeTimers();
const client = createClient({ url: '' });
const [ops$, next] = makeSubject<Operation>();
Expand All @@ -633,6 +634,16 @@ it.only('reexecutes query and returns data on partial result', () => {
// partial data.
.mockImplementation(() => {});

const initialQuery = gql`
query {
todos {
id
text
__typename
}
}
`;

const query = gql`
query {
todos {
Expand All @@ -649,8 +660,13 @@ it.only('reexecutes query and returns data on partial result', () => {
}
`;

const queryOperation = client.createRequestOperation('query', {
const initialQueryOperation = client.createRequestOperation('query', {
key: 1,
query: initialQuery,
});

const queryOperation = client.createRequestOperation('query', {
key: 2,
query,
});

Expand All @@ -673,6 +689,8 @@ it.only('reexecutes query and returns data on partial result', () => {
const response = jest.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
return { operation: initialQueryOperation, data: queryData };
} else if (forwardOp.key === 2) {
return { operation: queryOperation, data: queryData };
}

Expand All @@ -697,7 +715,7 @@ it.only('reexecutes query and returns data on partial result', () => {
publish
);

next(queryOperation);
next(initialQueryOperation);
jest.runAllTimers();
expect(response).toHaveBeenCalledTimes(1);
expect(reexec).toHaveBeenCalledTimes(0);
Expand All @@ -706,15 +724,11 @@ it.only('reexecutes query and returns data on partial result', () => {
todos: [
{
__typename: 'Todo',
author: null,
complete: null,
id: '123',
text: 'Learn',
},
{
__typename: 'Todo',
author: null,
complete: null,
id: '456',
text: 'Teach',
},
Expand Down
28 changes: 10 additions & 18 deletions src/exchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
} from 'urql';

import { filter, map, merge, pipe, share, tap } from 'wonka';
import { query, write, writeOptimistic, readOperation } from './operations';
import { query, write, writeOptimistic } from './operations';
import { SchemaPredicates } from './ast/schemaPredicates';
import { Store } from './store';

import {
Expand All @@ -17,7 +18,6 @@ import {
OptimisticMutationConfig,
KeyingConfig,
} from './types';
import { SchemaPredicates } from './ast/schemaPredicates';

type OperationResultWithMeta = OperationResult & {
outcome: CacheOutcome;
Expand Down Expand Up @@ -96,13 +96,8 @@ export const cacheExchange = (opts?: CacheExchangeOpts): Exchange => ({
}) => {
if (!opts) opts = {};

let schemaPredicates;
if (opts.schema) {
schemaPredicates = new SchemaPredicates(opts.schema);
}

const store = new Store(
schemaPredicates,
opts.schema ? new SchemaPredicates(opts.schema) : undefined,
opts.resolvers,
opts.updates,
opts.optimistic,
Expand Down Expand Up @@ -175,22 +170,19 @@ export const cacheExchange = (opts?: CacheExchangeOpts): Exchange => ({
operation: Operation
): OperationResultWithMeta => {
const policy = getRequestPolicy(operation);
const { data, dependencies, completeness } = query(store, operation);
const { data, dependencies, partial } = query(store, operation);
let cacheOutcome: CacheOutcome;

if (completeness === 'FULL' || policy === 'cache-only') {
updateDependencies(operation, dependencies);
cacheOutcome = 'hit';
} else if (completeness === 'PARTIAL') {
updateDependencies(operation, dependencies);
cacheOutcome = 'partial';
} else {
if (data === null) {
cacheOutcome = 'miss';
} else {
updateDependencies(operation, dependencies);
cacheOutcome = !partial || policy === 'cache-only' ? 'hit' : 'partial';
}

return {
operation,
outcome: cacheOutcome,
operation,
data,
};
};
Expand All @@ -217,7 +209,7 @@ export const cacheExchange = (opts?: CacheExchangeOpts): Exchange => ({
data = queryResult.data;
queryDependencies = queryResult.dependencies;
} else {
data = readOperation(store, operation, data).data;
data = query(store, operation, data).data;
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/operations/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { query, readOperation } from './query';
export { query, read } from './query';
export { write, writeOptimistic, writeFragment } from './write';
2 changes: 1 addition & 1 deletion src/operations/query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe('Query', () => {

it('test partial results', () => {
const result = query(store, { query: TODO_QUERY });
expect(result.completeness).toEqual('PARTIAL');
expect(result.partial).toBe(true);
expect(result.data).toEqual({
__typename: 'Query',
todos: [
Expand Down
Loading