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

(feat) - schema awareness #58

Merged
merged 34 commits into from
Sep 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
0119aaf
(chore) - document idea's
JoviDeCroock Sep 3, 2019
b061b33
(docs) - further document thought process
JoviDeCroock Sep 3, 2019
cc46fd5
(chore) - document implementation details
JoviDeCroock Sep 3, 2019
3984db6
(chore) - remove schemaFetching
JoviDeCroock Sep 3, 2019
c20324d
(docs) - add wall of text about offline and partials
JoviDeCroock Sep 3, 2019
5e82e73
(feat) - find the rootFields in the schema or use sensible defaults
JoviDeCroock Sep 3, 2019
58d9aa0
(chore) - start adding a test and define schema in our iterator
JoviDeCroock Sep 3, 2019
3ca4bd4
(feat) - see if a field is nullable
JoviDeCroock Sep 3, 2019
0def52c
(feat) - add first implementation to exchange.ts
JoviDeCroock Sep 3, 2019
c5c48ea
(refactor) - move to separate schemaPredicate class
JoviDeCroock Sep 3, 2019
68a7d50
(refactor) - add second argument to our query and write methods
JoviDeCroock Sep 3, 2019
feb2dc4
(feat) - trigger heuristic matching when we are handling the no schem…
JoviDeCroock Sep 4, 2019
097460c
(feat) - implement heuristic/schema fragment matching and nullability…
JoviDeCroock Sep 4, 2019
3c3b7c8
(tests) - add tests for our new SchemaPredicates class
JoviDeCroock Sep 4, 2019
a159cf0
(chore) - update paths for new schema
JoviDeCroock Sep 4, 2019
1a205f6
(chore) - update simpleSchema to include a union (this also updates s…
JoviDeCroock Sep 4, 2019
9b8006a
(tests) - test reexecution of operation on partial queryResult
JoviDeCroock Sep 4, 2019
fd6aa93
(chore) - resolve typing issue and convert some Query ref to the root…
JoviDeCroock Sep 4, 2019
2e2e34a
(refactor) - move rootKey logic to store
JoviDeCroock Sep 4, 2019
6e97a98
(refactor) - use graphql-js internals
JoviDeCroock Sep 4, 2019
92cc54b
(chore) - remove comment blocks
JoviDeCroock Sep 4, 2019
bd4851f
(refactor) - cleanup some unreachable code paths
JoviDeCroock Sep 4, 2019
64cc41e
(chore) - remove redundant comments
JoviDeCroock Sep 4, 2019
517c33d
Add improved warning and invariant to SchemaPredicates.isFieldNullable
kitten Sep 4, 2019
01b1303
Add improved invariant to SchemaPredicates.isInterfaceOfType
kitten Sep 4, 2019
d737398
Fix case for null type condition
kitten Sep 4, 2019
821989e
Fix some types in schemaPredicates
kitten Sep 4, 2019
bece505
Add warning on heuristic fragment matching
kitten Sep 4, 2019
487b8ff
Add new partial handling to exchange
kitten Sep 4, 2019
77bdf38
Fix partial query behaviour
kitten Sep 4, 2019
422c0ed
Fix exchange outcome marking and tests
kitten Sep 4, 2019
d16c4a7
Add missing hasFields setter for invalid entities
kitten Sep 4, 2019
4184506
Add limit for partial Query result to partial results
kitten Sep 4, 2019
bcfa475
(tests) - update assertions of query.test.ts
JoviDeCroock Sep 4, 2019
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
12 changes: 0 additions & 12 deletions src/ast/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
SelectionSetNode,
InlineFragmentNode,
FieldNode,
OperationDefinitionNode,
FragmentDefinitionNode,
Kind,
} from 'graphql';
Expand All @@ -15,17 +14,6 @@ import { SelectionSet } from '../types';
/** Returns the name of a given node */
export const getName = (node: { name: NameNode }): string => node.name.value;

export const getOperationName = (node: OperationDefinitionNode) => {
switch (node.operation) {
case 'query':
return 'Query';
case 'mutation':
return 'Mutation';
case 'subscription':
return 'Subscription';
}
};

export const getFragmentTypeName = (node: FragmentDefinitionNode): string =>
node.typeCondition.name.value;

Expand Down
32 changes: 32 additions & 0 deletions src/ast/schemaPredicates.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { SchemaPredicates } from './schemaPredicates';

describe('SchemaPredicates', () => {
let schemaPredicates;

beforeAll(() => {
// eslint-disable-next-line
const schema = require('../test-utils/simple_schema.json');
schemaPredicates = new SchemaPredicates(schema);
});

it('should match fragments by interface/union', () => {
expect(schemaPredicates.isInterfaceOfType('ITodo', 'BigTodo')).toBeTruthy();
expect(
schemaPredicates.isInterfaceOfType('ITodo', 'SmallTodo')
).toBeTruthy();
expect(
schemaPredicates.isInterfaceOfType('Search', 'BigTodo')
).toBeTruthy();
expect(
schemaPredicates.isInterfaceOfType('Search', 'SmallTodo')
).toBeTruthy();
expect(schemaPredicates.isInterfaceOfType('ITodo', 'Todo')).toBeFalsy();
expect(schemaPredicates.isInterfaceOfType('Search', 'Todo')).toBeFalsy();
});

it('should indicate nullability', () => {
expect(schemaPredicates.isFieldNullable('Todo', 'text')).toBeFalsy();
expect(schemaPredicates.isFieldNullable('Todo', 'complete')).toBeTruthy();
expect(schemaPredicates.isFieldNullable('Todo', 'author')).toBeTruthy();
});
});
89 changes: 89 additions & 0 deletions src/ast/schemaPredicates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import invariant from 'invariant';
import warning from 'warning';

import {
buildClientSchema,
isNullableType,
GraphQLSchema,
GraphQLAbstractType,
GraphQLObjectType,
GraphQLInterfaceType,
GraphQLUnionType,
} from 'graphql';

export class SchemaPredicates {
schema: GraphQLSchema;

constructor(schema) {
this.schema = buildClientSchema(schema);
}

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;
}

return isNullableType(field.type);
}

isInterfaceOfType(
typeCondition: null | string,
typename: string | void
): boolean {
if (!typename || !typeCondition) return false;
if (typename === typeCondition) return true;

const abstractType = this.schema.getType(typeCondition);
expectAbstractType(abstractType, typeCondition);
const objectType = this.schema.getType(typename);
expectObjectType(objectType, typename);

const abstractNode = abstractType as GraphQLAbstractType;
const concreteNode = objectType as GraphQLObjectType;
return this.schema.isPossibleType(abstractNode, concreteNode);
}
}

const expectObjectType = (type: any, typename: string) => {
invariant(
type instanceof GraphQLObjectType,
'Invalid type: The type `%s` is not an object in the defined schema, ' +
'but the GraphQL document is traversing it.',
typename
);
};

const expectAbstractType = (type: any, typename: string) => {
invariant(
type instanceof GraphQLInterfaceType || type instanceof GraphQLUnionType,
'Invalid type: The type `%s` is not an Interface or Union type in the defined schema, ' +
'but a fragment in the GraphQL document is using it as a type condition.',
typename
);
};
133 changes: 133 additions & 0 deletions src/exchange.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -622,3 +622,136 @@ it('follows nested resolvers for mutations', () => {
'AwesomeGQL',
]);
});

it.only('reexecutes query and returns data on partial result', () => {
jest.useFakeTimers();
const client = createClient({ url: '' });
const [ops$, next] = makeSubject<Operation>();
const reexec = jest
.spyOn(client, 'reexecuteOperation')
// Empty mock to avoid going in an endless loop, since we would again return
// partial data.
.mockImplementation(() => {});

const query = gql`
query {
todos {
id
text
complete
author {
id
name
__typename
}
__typename
}
}
`;

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

const queryData = {
__typename: 'Query',
todos: [
{
__typename: 'Todo',
id: '123',
text: 'Learn',
},
{
__typename: 'Todo',
id: '456',
text: 'Teach',
},
],
};

const response = jest.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
return { operation: queryOperation, data: queryData };
}

return undefined as any;
}
);

const result = jest.fn();
const forward: ExchangeIO = ops$ =>
pipe(
ops$,
delay(1),
map(response)
);

pipe(
cacheExchange({
// eslint-disable-next-line
schema: require('./test-utils/simple_schema.json'),
})({ forward, client })(ops$),
tap(result),
publish
);

next(queryOperation);
jest.runAllTimers();
expect(response).toHaveBeenCalledTimes(1);
expect(reexec).toHaveBeenCalledTimes(0);
expect(result.mock.calls[0][0].data).toEqual({
__typename: 'Query',
todos: [
{
__typename: 'Todo',
author: null,
complete: null,
id: '123',
text: 'Learn',
},
{
__typename: 'Todo',
author: null,
complete: null,
id: '456',
text: 'Teach',
},
],
});

expect(result.mock.calls[0][0]).toHaveProperty(
'operation.context.meta',
undefined
);

next(queryOperation);
jest.runAllTimers();
expect(result).toHaveBeenCalledTimes(2);
expect(reexec).toHaveBeenCalledTimes(1);
expect(result.mock.calls[1][0].data).toEqual({
__typename: 'Query',
todos: [
{
__typename: 'Todo',
author: null,
complete: null,
id: '123',
text: 'Learn',
},
{
__typename: 'Todo',
author: null,
complete: null,
id: '456',
text: 'Teach',
},
],
});

expect(result.mock.calls[1][0]).toHaveProperty(
'operation.context.meta.cacheOutcome',
'partial'
);
});
Loading