diff --git a/docs/help.md b/docs/help.md
index 72d1601..9657f26 100644
--- a/docs/help.md
+++ b/docs/help.md
@@ -261,11 +261,14 @@ field somewhere, maybe due to a typo.
## (18) Invalid TypeInfo state
-> Invalid TypeInfo state: Found an abstract type when none was expected.
+> Invalid TypeInfo state: Found no flat schema type when one was expected.
When you're using the populate exchange with an introspected schema, it will
start collecting used fragments and selection sets on all of your queries.
-This error may occur if it hits unexpected abstract types when doing so.
+This error may occur if it hits unexpected types or inexistent types when doing so.
+
+Check whether your schema is up-to-date or whether you're using an invalid
+field somewhere, maybe due to a typo.
Please open an issue if it happens on a query that you expect to be supported
by the `populateExchange`.
diff --git a/src/ast/node.ts b/src/ast/node.ts
index 2eaa923..f54d446 100644
--- a/src/ast/node.ts
+++ b/src/ast/node.ts
@@ -6,10 +6,13 @@ import {
InlineFragmentNode,
FieldNode,
FragmentDefinitionNode,
+ GraphQLOutputType,
Kind,
+ isListType,
+ isNonNullType,
} from 'graphql';
-import { SelectionSet } from '../types';
+import { SelectionSet, GraphQLFlatType } from '../types';
/** Returns the name of a given node */
export const getName = (node: { name: NameNode }): string => node.name.value;
@@ -40,3 +43,11 @@ export const isFieldNode = (node: SelectionNode): node is FieldNode =>
export const isInlineFragment = (
node: SelectionNode
): node is InlineFragmentNode => node.kind === Kind.INLINE_FRAGMENT;
+
+export const unwrapType = (
+ type: null | undefined | GraphQLOutputType
+): GraphQLFlatType | null => {
+ return type && (isListType(type) || isNonNullType(type))
+ ? type.ofType
+ : type || null;
+};
diff --git a/src/cacheExchange.ts b/src/cacheExchange.ts
index 07d2113..b7b7136 100644
--- a/src/cacheExchange.ts
+++ b/src/cacheExchange.ts
@@ -7,6 +7,7 @@ import {
CacheOutcome,
} from 'urql';
+import { IntrospectionQuery } from 'graphql';
import { filter, map, merge, pipe, share, tap } from 'wonka';
import { query, write, writeOptimistic } from './operations';
import { SchemaPredicates } from './ast/schemaPredicates';
@@ -85,7 +86,7 @@ export interface CacheExchangeOpts {
resolvers?: ResolverConfig;
optimistic?: OptimisticMutationConfig;
keys?: KeyingConfig;
- schema?: object;
+ schema?: IntrospectionQuery;
}
export const cacheExchange = (opts?: CacheExchangeOpts): Exchange => ({
diff --git a/src/map.ts b/src/helpers/map.ts
similarity index 100%
rename from src/map.ts
rename to src/helpers/map.ts
diff --git a/src/populateExchange.ts b/src/populateExchange.ts
index 6064fa0..f449769 100644
--- a/src/populateExchange.ts
+++ b/src/populateExchange.ts
@@ -1,43 +1,45 @@
-import { pipe, tap, map } from 'wonka';
-import { Exchange, Operation } from 'urql';
import {
DocumentNode,
buildClientSchema,
visitWithTypeInfo,
TypeInfo,
FragmentDefinitionNode,
- SelectionSetNode,
GraphQLSchema,
IntrospectionQuery,
FragmentSpreadNode,
+ NameNode,
ASTNode,
- isNonNullType,
- isListType,
isUnionType,
isInterfaceType,
isCompositeType,
isAbstractType,
+ Kind,
visit,
} from 'graphql';
+
+import { pipe, tap, map } from 'wonka';
+import { Exchange, Operation } from 'urql';
+
+import { getName, getSelectionSet, unwrapType } from './ast';
import { invariant, warn } from './helpers/help';
-interface ExchangeArgs {
+interface PopulateExchangeOpts {
schema: IntrospectionQuery;
}
/** An exchange for auto-populating mutations with a required response body. */
export const populateExchange = ({
schema: ogSchema,
-}: ExchangeArgs): Exchange => ({ forward }) => {
+}: PopulateExchangeOpts): Exchange => ({ forward }) => {
const schema = buildClientSchema(ogSchema);
/** List of operation keys that have already been parsed. */
const parsedOperations = new Set();
/** List of operation keys that have not been torn down. */
const activeOperations = new Set();
/** Collection of fragments used by the user. */
- let userFragments: UserFragmentMap = Object.create(null);
+ const userFragments: UserFragmentMap = Object.create(null);
/** Collection of type fragments. */
- let typeFragments: TypeFragmentMap = Object.create(null);
+ const typeFragments: TypeFragmentMap = Object.create(null);
/** Handle mutation and inject selections + fragments. */
const handleIncomingMutation = (op: Operation) => {
@@ -45,22 +47,21 @@ export const populateExchange = ({
return op;
}
- const activeSelections = Object.keys(typeFragments).reduce(
- (state, key) => ({
- ...state,
- [key]: state[key].filter(s => activeOperations.has(s.key)),
- }),
- typeFragments
- );
+ const activeSelections: TypeFragmentMap = Object.create(null);
+ for (const name in typeFragments) {
+ activeSelections[name] = typeFragments[name].filter(s =>
+ activeOperations.has(s.key)
+ );
+ }
return {
...op,
- query: addFragmentsToQuery({
+ query: addFragmentsToQuery(
schema,
- typeFragments: activeSelections,
- userFragments: userFragments,
- query: op.query,
- }),
+ op.query,
+ activeSelections,
+ userFragments
+ ),
};
};
@@ -71,63 +72,36 @@ export const populateExchange = ({
}
activeOperations.add(key);
-
if (parsedOperations.has(key)) {
return;
}
parsedOperations.add(key);
- const {
- fragments: newFragments,
- selections: newSelections,
- } = extractSelectionsFromQuery({
+ const [extractedFragments, newFragments] = extractSelectionsFromQuery(
schema,
- query,
- });
-
- userFragments = newFragments.reduce(
- (state, fragment) => ({
- ...state,
- [fragment.name.value]: fragment,
- }),
- userFragments
+ query
);
- typeFragments = newSelections.reduce((state, { selections, type }) => {
- const current = state[type] || [];
- const entry: TypeFragment = {
- key,
- fragment: {
- kind: 'FragmentDefinition',
- typeCondition: {
- kind: 'NamedType',
- name: {
- kind: 'Name',
- value: type,
- },
- },
- name: {
- kind: 'Name',
- value: `${type}_PopulateFragment_${current.length}`,
- },
- selectionSet: selections,
- },
- type,
- };
- return {
- ...state,
- [type]: [...current, entry],
- };
- }, typeFragments);
+ for (let i = 0, l = extractedFragments.length; i < l; i++) {
+ const fragment = extractedFragments[i];
+ userFragments[getName(fragment)] = fragment;
+ }
+
+ for (let i = 0, l = newFragments.length; i < l; i++) {
+ const fragment = newFragments[i];
+ const type = getName(fragment.typeCondition);
+ const current = typeFragments[type] || (typeFragments[type] = []);
+
+ (fragment as any).name.value += current.length;
+ current.push({ key, fragment });
+ }
};
const handleIncomingTeardown = ({ key, operationName }: Operation) => {
- if (operationName !== 'teardown') {
- return;
+ if (operationName === 'teardown') {
+ activeOperations.delete(key);
}
-
- activeOperations.delete(key);
};
return ops$ => {
@@ -153,77 +127,77 @@ interface TypeFragment {
key: number;
/** Selection set. */
fragment: FragmentDefinitionNode;
- /** Type of selection. */
- type: string;
-}
-
-interface MakeFragmentsFromQueryArg {
- schema: GraphQLSchema;
- query: DocumentNode;
}
/** Gets typed selection sets and fragments from query */
-export const extractSelectionsFromQuery = ({
- schema,
- query,
-}: MakeFragmentsFromQueryArg) => {
- let selections: { selections: SelectionSetNode; type: string }[] = [];
- let fragments: FragmentDefinitionNode[] = [];
+export const extractSelectionsFromQuery = (
+ schema: GraphQLSchema,
+ query: DocumentNode
+) => {
+ const extractedFragments: FragmentDefinitionNode[] = [];
+ const newFragments: FragmentDefinitionNode[] = [];
const typeInfo = new TypeInfo(schema);
visit(
query,
visitWithTypeInfo(typeInfo, {
Field: node => {
- if (!node.selectionSet) {
- return undefined;
+ if (node.selectionSet) {
+ const type = getTypeName(typeInfo);
+ newFragments.push({
+ kind: Kind.FRAGMENT_DEFINITION,
+ typeCondition: {
+ kind: Kind.NAMED_TYPE,
+ name: nameNode(type),
+ },
+ name: nameNode(`${type}_PopulateFragment_`),
+ selectionSet: node.selectionSet,
+ });
}
-
- const type = getTypeName(typeInfo);
- selections = [...selections, { selections: node.selectionSet, type }];
},
FragmentDefinition: node => {
- fragments = [...fragments, node];
+ extractedFragments.push(node);
},
})
);
- return { selections, fragments };
+ return [extractedFragments, newFragments];
};
-interface AddFragmentsToQuery {
- schema: GraphQLSchema;
- query: DocumentNode;
- typeFragments: Record[]>;
- userFragments: UserFragmentMap;
-}
-
/** Replaces populate decorator with fragment spreads + fragments. */
-export const addFragmentsToQuery = ({
- schema,
- query,
- typeFragments,
- userFragments,
-}: AddFragmentsToQuery) => {
+export const addFragmentsToQuery = (
+ schema: GraphQLSchema,
+ query: DocumentNode,
+ typeFragments: TypeFragmentMap,
+ userFragments: UserFragmentMap
+) => {
const typeInfo = new TypeInfo(schema);
- const requiredUserFragments = new Set();
- let additionalFragments: Record = {};
+
+ const requiredUserFragments: Record<
+ string,
+ FragmentDefinitionNode
+ > = Object.create(null);
+
+ const additionalFragments: Record<
+ string,
+ FragmentDefinitionNode
+ > = Object.create(null);
return visit(
query,
visitWithTypeInfo(typeInfo, {
Field: {
enter: node => {
- if (
- !node.directives ||
- !node.directives.find(d => d.name.value === 'populate')
- ) {
+ if (!node.directives) {
return;
}
const directives = node.directives.filter(
- d => d.name.value !== 'populate'
+ d => getName(d) !== 'populate'
);
+ if (directives.length === node.directives.length) {
+ return;
+ }
const types = getTypes(schema, typeInfo);
const newSelections = types.reduce((p, t) => {
@@ -232,44 +206,38 @@ export const addFragmentsToQuery = ({
return p;
}
- return [
- ...p,
- ...typeFragments[t.name].map(({ fragment }) => {
- // Add used fragment for insertion at Document node
- getUsedFragments(fragment).forEach(f =>
- requiredUserFragments.add(userFragments[f])
- );
-
- // Add fragment for insertion at Document node
- additionalFragments = {
- ...additionalFragments,
- [fragment.name.value]: fragment,
- };
-
- return {
- kind: 'FragmentSpread',
- name: {
- kind: 'Name',
- value: fragment.name.value,
- },
- } as const;
- }),
- ];
+ for (let i = 0, l = typeFrags.length; i < l; i++) {
+ const { fragment } = typeFrags[i];
+ const fragmentName = getName(fragment);
+ const usedFragments = getUsedFragments(fragment);
+
+ // Add used fragment for insertion at Document node
+ for (let j = 0, l = usedFragments.length; j < l; j++) {
+ const name = usedFragments[j];
+ requiredUserFragments[name] = userFragments[name];
+ }
+
+ // Add fragment for insertion at Document node
+ additionalFragments[fragmentName] = fragment;
+
+ p.push({
+ kind: Kind.FRAGMENT_SPREAD,
+ name: nameNode(fragmentName),
+ });
+ }
+
+ return p;
}, [] as FragmentSpreadNode[]);
- const existingSelections =
- (node.selectionSet && node.selectionSet.selections) || [];
+ const existingSelections = getSelectionSet(node);
const selections =
existingSelections.length + newSelections.length !== 0
? [...newSelections, ...existingSelections]
: [
{
- kind: 'Field',
- name: {
- kind: 'Name',
- value: '__typename',
- },
+ kind: Kind.FIELD,
+ name: nameNode('__typename'),
},
];
@@ -277,7 +245,7 @@ export const addFragmentsToQuery = ({
...node,
directives,
selectionSet: {
- kind: 'SelectionSet',
+ kind: Kind.SELECTION_SET,
selections,
},
};
@@ -285,30 +253,26 @@ export const addFragmentsToQuery = ({
},
Document: {
leave: node => {
- return {
- ...node,
- definitions: [
- ...node.definitions,
- ...Object.keys(additionalFragments).map(
- k => additionalFragments[k]
- ), // Object.values
- ...Array.from(requiredUserFragments),
- ],
- };
+ const definitions = [...node.definitions];
+ for (const key in additionalFragments)
+ definitions.push(additionalFragments[key]);
+ for (const key in requiredUserFragments)
+ definitions.push(requiredUserFragments[key]);
+ return { ...node, definitions };
},
},
})
);
};
+const nameNode = (value: string): NameNode => ({
+ kind: Kind.NAME,
+ value,
+});
+
/** Get all possible types for node with TypeInfo. */
const getTypes = (schema: GraphQLSchema, typeInfo: TypeInfo) => {
- const typeInfoType = typeInfo.getType();
- const type =
- isListType(typeInfoType) || isNonNullType(typeInfoType)
- ? typeInfoType.ofType
- : typeInfoType;
-
+ const type = unwrapType(typeInfo.getType());
if (!isCompositeType(type)) {
warn(
'Invalid type: The type ` + type + ` is used with @populate but does not exist.',
@@ -317,24 +281,17 @@ const getTypes = (schema: GraphQLSchema, typeInfo: TypeInfo) => {
return [];
}
- if (isInterfaceType(type) || isUnionType(type)) {
- return schema.getPossibleTypes(type);
- }
-
- return [type];
+ return isInterfaceType(type) || isUnionType(type)
+ ? schema.getPossibleTypes(type)
+ : [type];
};
/** Get name of non-abstract type for adding to 'typeFragments'. */
-const getTypeName = (t: TypeInfo) => {
- const typeInfoType = t.getType();
- const type =
- isListType(typeInfoType) || isNonNullType(typeInfoType)
- ? typeInfoType.ofType
- : typeInfoType;
-
+const getTypeName = (typeInfo: TypeInfo) => {
+ const type = unwrapType(typeInfo.getType());
invariant(
- !isAbstractType(type),
- 'Invalid TypeInfo state: Found an abstract type when none was expected.',
+ type && !isAbstractType(type),
+ 'Invalid TypeInfo state: Found no flat schema type when one was expected.',
18
);
@@ -343,11 +300,11 @@ const getTypeName = (t: TypeInfo) => {
/** Get fragment names referenced by node. */
const getUsedFragments = (node: ASTNode) => {
- let names: string[] = [];
+ const names: string[] = [];
visit(node, {
FragmentSpread: f => {
- names = [...names, f.name.value];
+ names.push(getName(f));
},
});
diff --git a/src/store.ts b/src/store.ts
index 6c3f401..73d82b8 100644
--- a/src/store.ts
+++ b/src/store.ts
@@ -17,7 +17,7 @@ import {
KeyingConfig,
} from './types';
-import * as KVMap from './map';
+import * as KVMap from './helpers/map';
import { joinKeys, keyOfField } from './helpers';
import { invariant, currentDebugStack } from './helpers/help';
import { read, readFragment } from './operations/query';
diff --git a/src/types.ts b/src/types.ts
index acccc06..92fd690 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,4 +1,10 @@
-import { DocumentNode, FragmentDefinitionNode, SelectionNode } from 'graphql';
+import {
+ DocumentNode,
+ FragmentDefinitionNode,
+ SelectionNode,
+ GraphQLOutputType,
+ GraphQLWrappingType,
+} from 'graphql';
// Helper types
export type NullArray = Array;
@@ -9,6 +15,7 @@ export interface Ref {
// GraphQL helper types
export type SelectionSet = ReadonlyArray;
+export type GraphQLFlatType = Exclude;
export interface Fragments {
[fragmentName: string]: void | FragmentDefinitionNode;
}