Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.

(graphcache) - Replace buildClientSchema with lean implementation (#1189)

* Add reimplementation of buildClientSchema

* Replace GraphQLSchema with local SchemaIntrospector

* Replace root type getters with properties

* Remove unnecessary cruft on SchemaIntrospector

* Fix linting errors

* Remove specific Scalar construction

* Add changeset

* Refactor schema predicates to be name-based

* Rename buildClientSchema.ts to schema.ts

* Update changeset

Changed files
+212 -113
.changeset
exchanges
+8
.changeset/nervous-beds-battle.md
···
+
---
+
'@urql/exchange-graphcache': patch
+
---
+
+
Replace `graphql/utilities/buildClientSchema.mjs` with a custom-tailored, lighter implementation
+
built into `@urql/exchange-graphcache`. This will appear to increase its size by about `0.2kB gzip`
+
but will actually save around `8.5kB gzip` to `9.4kB gzip` in any production bundle by using less of
+
`graphql`'s code.
+1
exchanges/graphcache/src/ast/index.ts
···
export * from './variables';
export * from './traversal';
+
export * from './schema';
export * from './schemaPredicates';
export * from './node';
+107
exchanges/graphcache/src/ast/schema.ts
···
+
import {
+
IntrospectionQuery,
+
IntrospectionInputValue,
+
IntrospectionTypeRef,
+
IntrospectionType,
+
} from 'graphql';
+
+
export interface SchemaField {
+
name: string;
+
type: IntrospectionTypeRef;
+
args: Record<string, IntrospectionInputValue>;
+
}
+
+
export interface SchemaObject {
+
name: string;
+
kind: 'INTERFACE' | 'OBJECT';
+
interfaces: Record<string, unknown>;
+
fields: Record<string, SchemaField>;
+
}
+
+
export interface SchemaUnion {
+
name: string;
+
kind: 'UNION';
+
types: Record<string, unknown>;
+
}
+
+
export interface SchemaIntrospector {
+
query: string | null;
+
mutation: string | null;
+
subscription: string | null;
+
types: Record<string, SchemaObject | SchemaUnion>;
+
isSubType(abstract: string, possible: string): boolean;
+
}
+
+
export const buildClientSchema = ({
+
__schema,
+
}: IntrospectionQuery): SchemaIntrospector => {
+
const typemap: Record<string, SchemaObject | SchemaUnion> = {};
+
+
const buildNameMap = <T extends { name: string }>(
+
arr: ReadonlyArray<T>
+
): { [name: string]: T } => {
+
const map: Record<string, T> = {};
+
for (let i = 0; i < arr.length; i++) map[arr[i].name] = arr[i];
+
return map;
+
};
+
+
const buildType = (
+
type: IntrospectionType
+
): SchemaObject | SchemaUnion | void => {
+
switch (type.kind) {
+
case 'OBJECT':
+
case 'INTERFACE':
+
return {
+
name: type.name,
+
kind: type.kind as 'OBJECT' | 'INTERFACE',
+
interfaces: buildNameMap(type.interfaces || []),
+
fields: buildNameMap(
+
type.fields.map(field => ({
+
name: field.name,
+
type: field.type,
+
args: buildNameMap(field.args),
+
}))
+
),
+
} as SchemaObject;
+
case 'UNION':
+
return {
+
name: type.name,
+
kind: type.kind as 'UNION',
+
types: buildNameMap(type.possibleTypes || []),
+
} as SchemaUnion;
+
}
+
};
+
+
for (let i = 0; i < __schema.types.length; i++) {
+
const type = __schema.types[i];
+
if (type && type.name) {
+
const out = buildType(type);
+
if (out) typemap[type.name] = out;
+
}
+
}
+
+
return {
+
query: __schema.queryType ? __schema.queryType.name : null,
+
mutation: __schema.mutationType ? __schema.mutationType.name : null,
+
subscription: __schema.subscriptionType
+
? __schema.subscriptionType.name
+
: null,
+
types: typemap,
+
isSubType(abstract: string, possible: string) {
+
const abstractType = typemap[abstract];
+
const possibleType = typemap[possible];
+
if (!abstractType || !possibleType) {
+
return false;
+
} else if (abstractType.kind === 'UNION') {
+
return !!abstractType.types[possible];
+
} else if (
+
abstractType.kind !== 'OBJECT' &&
+
possibleType.kind === 'OBJECT'
+
) {
+
return !!possibleType.interfaces[abstract];
+
} else {
+
return abstract === possible;
+
}
+
},
+
};
+
};
+2 -1
exchanges/graphcache/src/ast/schemaPredicates.test.ts
···
-
import { Kind, InlineFragmentNode, buildClientSchema } from 'graphql';
+
import { Kind, InlineFragmentNode } from 'graphql';
import { mocked } from 'ts-jest/utils';
+
import { buildClientSchema } from './schema';
import * as SchemaPredicates from './schemaPredicates';
describe('SchemaPredicates', () => {
+76 -92
exchanges/graphcache/src/ast/schemaPredicates.ts
···
-
import {
-
isNullableType,
-
isListType,
-
isNonNullType,
-
InlineFragmentNode,
-
FragmentDefinitionNode,
-
GraphQLSchema,
-
GraphQLAbstractType,
-
GraphQLObjectType,
-
GraphQLInterfaceType,
-
GraphQLUnionType,
-
} from 'graphql';
+
import { InlineFragmentNode, FragmentDefinitionNode } from 'graphql';
import { warn, invariant } from '../helpers/help';
import { getTypeCondition } from './node';
+
import { SchemaIntrospector, SchemaObject } from './schema';
+
import {
KeyingConfig,
UpdateResolver,
···
const BUILTIN_FIELD_RE = /^__/;
export const isFieldNullable = (
-
schema: GraphQLSchema,
+
schema: SchemaIntrospector,
typename: string,
fieldName: string
): boolean => {
if (BUILTIN_FIELD_RE.test(fieldName)) return true;
const field = getField(schema, typename, fieldName);
-
return !!field && isNullableType(field.type);
+
return !!field && field.type.kind !== 'NON_NULL';
};
export const isListNullable = (
-
schema: GraphQLSchema,
+
schema: SchemaIntrospector,
typename: string,
fieldName: string
): boolean => {
const field = getField(schema, typename, fieldName);
if (!field) return false;
-
const ofType = isNonNullType(field.type) ? field.type.ofType : field.type;
-
return isListType(ofType) && isNullableType(ofType.ofType);
+
const ofType =
+
field.type.kind === 'NON_NULL' ? field.type.ofType : field.type;
+
return ofType.kind === 'LIST' && ofType.ofType.kind !== 'NON_NULL';
};
export const isFieldAvailableOnType = (
-
schema: GraphQLSchema,
+
schema: SchemaIntrospector,
typename: string,
fieldName: string
): boolean => {
···
};
export const isInterfaceOfType = (
-
schema: GraphQLSchema,
+
schema: SchemaIntrospector,
node: InlineFragmentNode | FragmentDefinitionNode,
typename: string | void
): boolean => {
if (!typename) return false;
const typeCondition = getTypeCondition(node);
if (!typeCondition || typename === typeCondition) return true;
-
-
const abstractType = schema.getType(typeCondition);
-
const objectType = schema.getType(typename);
-
-
if (abstractType instanceof GraphQLObjectType) {
-
return abstractType === objectType;
-
}
-
-
expectAbstractType(abstractType, typeCondition);
-
expectObjectType(objectType, typename);
-
return schema.isPossibleType(abstractType, objectType);
+
if (
+
schema.types[typeCondition] &&
+
schema.types[typeCondition].kind === 'OBJECT'
+
)
+
return typeCondition === typename;
+
expectAbstractType(schema, typeCondition!);
+
expectObjectType(schema, typename!);
+
return schema.isSubType(typeCondition, typename);
};
const getField = (
-
schema: GraphQLSchema,
+
schema: SchemaIntrospector,
typename: string,
fieldName: string
) => {
-
const object = schema.getType(typename);
-
expectObjectType(object, typename);
-
-
const field = object.getFields()[fieldName];
+
expectObjectType(schema, typename);
+
const object = schema.types[typename] as SchemaObject;
+
const field = object.fields[fieldName];
if (!field) {
warn(
'Invalid field: The field `' +
···
return field;
};
-
function expectObjectType(
-
x: any,
-
typename: string
-
): asserts x is GraphQLObjectType {
+
function expectObjectType(schema: SchemaIntrospector, typename: string) {
invariant(
-
x instanceof GraphQLObjectType,
+
schema.types[typename] && schema.types[typename].kind === 'OBJECT',
'Invalid Object type: The type `' +
typename +
'` is not an object in the defined schema, ' +
···
);
}
-
function expectAbstractType(
-
x: any,
-
typename: string
-
): asserts x is GraphQLAbstractType {
+
function expectAbstractType(schema: SchemaIntrospector, typename: string) {
invariant(
-
x instanceof GraphQLInterfaceType || x instanceof GraphQLUnionType,
+
schema.types[typename] &&
+
(schema.types[typename].kind === 'INTERFACE' ||
+
schema.types[typename].kind === 'UNION'),
'Invalid Abstract type: The type `' +
typename +
'` is not an Interface or Union type in the defined schema, ' +
···
}
export function expectValidKeyingConfig(
-
schema: GraphQLSchema,
+
schema: SchemaIntrospector,
keys: KeyingConfig
): void {
if (process.env.NODE_ENV !== 'production') {
-
const types = schema.getTypeMap();
for (const key in keys) {
-
if (!types[key]) {
+
if (!schema.types[key]) {
warn(
'Invalid Object type: The type `' +
key +
···
}
export function expectValidUpdatesConfig(
-
schema: GraphQLSchema,
+
schema: SchemaIntrospector,
updates: Record<string, Record<string, UpdateResolver>>
): void {
if (process.env.NODE_ENV === 'production') {
return;
}
-
const mutation = schema.getMutationType();
-
const subscription = schema.getSubscriptionType();
-
const mutationFields = mutation ? mutation.getFields() : {};
-
const subscriptionFields = subscription ? subscription.getFields() : {};
-
const givenMutations = (mutation && updates[mutation.name]) || {};
-
const givenSubscription = (subscription && updates[subscription.name]) || {};
-
-
for (const fieldName in givenMutations) {
-
if (mutationFields[fieldName] === undefined) {
-
warn(
-
'Invalid mutation field: `' +
-
fieldName +
-
'` is not in the defined schema, but the `updates.Mutation` option is referencing it.',
-
21
-
);
+
if (schema.mutation) {
+
const mutationFields = (schema.types[schema.mutation] as SchemaObject)
+
.fields;
+
const givenMutations = updates[schema.mutation] || {};
+
for (const fieldName in givenMutations) {
+
if (mutationFields[fieldName] === undefined) {
+
warn(
+
'Invalid mutation field: `' +
+
fieldName +
+
'` is not in the defined schema, but the `updates.Mutation` option is referencing it.',
+
21
+
);
+
}
}
}
-
for (const fieldName in givenSubscription) {
-
if (subscriptionFields[fieldName] === undefined) {
-
warn(
-
'Invalid subscription field: `' +
-
fieldName +
-
'` is not in the defined schema, but the `updates.Subscription` option is referencing it.',
-
22
-
);
+
if (schema.subscription) {
+
const subscriptionFields = (schema.types[
+
schema.subscription
+
] as SchemaObject).fields;
+
const givenSubscription = updates[schema.subscription] || {};
+
for (const fieldName in givenSubscription) {
+
if (subscriptionFields[fieldName] === undefined) {
+
warn(
+
'Invalid subscription field: `' +
+
fieldName +
+
'` is not in the defined schema, but the `updates.Subscription` option is referencing it.',
+
22
+
);
+
}
}
}
}
···
}
export function expectValidResolversConfig(
-
schema: GraphQLSchema,
+
schema: SchemaIntrospector,
resolvers: ResolverConfig
): void {
if (process.env.NODE_ENV === 'production') {
return;
}
-
const validTypes = schema.getTypeMap();
for (const key in resolvers) {
if (key === 'Query') {
-
const queryType = schema.getQueryType();
-
if (queryType) {
-
const validQueries = queryType.getFields();
+
if (schema.query) {
+
const validQueries = (schema.types[schema.query] as SchemaObject)
+
.fields;
for (const resolverQuery in resolvers.Query) {
if (!validQueries[resolverQuery]) {
warnAboutResolver('Query.' + resolverQuery);
···
warnAboutResolver('Query');
}
} else {
-
if (!validTypes[key]) {
+
if (!schema.types[key]) {
warnAboutResolver(key);
} else {
-
const validTypeProperties = (schema.getType(
-
key
-
) as GraphQLObjectType).getFields();
+
const validTypeProperties = (schema.types[key] as SchemaObject).fields;
for (const resolverProperty in resolvers[key]) {
if (!validTypeProperties[resolverProperty]) {
warnAboutResolver(key + '.' + resolverProperty);
···
}
export function expectValidOptimisticMutationsConfig(
-
schema: GraphQLSchema,
+
schema: SchemaIntrospector,
optimisticMutations: OptimisticMutationConfig
): void {
if (process.env.NODE_ENV === 'production') {
return;
}
-
const validMutations = schema.getMutationType()
-
? (schema.getMutationType() as GraphQLObjectType).getFields()
-
: {};
-
-
for (const mutation in optimisticMutations) {
-
if (!validMutations[mutation]) {
-
warn(
-
`Invalid optimistic mutation field: \`${mutation}\` is not a mutation field in the defined schema, but the \`optimistic\` option is referencing it.`,
-
24
-
);
+
if (schema.mutation) {
+
const validMutations = (schema.types[schema.mutation] as SchemaObject)
+
.fields;
+
for (const mutation in optimisticMutations) {
+
if (!validMutations[mutation]) {
+
warn(
+
`Invalid optimistic mutation field: \`${mutation}\` is not a mutation field in the defined schema, but the \`optimistic\` option is referencing it.`,
+
24
+
);
+
}
}
}
}
+18 -20
exchanges/graphcache/src/store/store.ts
···
-
import {
-
buildClientSchema,
-
DocumentNode,
-
IntrospectionQuery,
-
GraphQLSchema,
-
} from 'graphql';
+
import { DocumentNode, IntrospectionQuery } from 'graphql';
import { TypedDocumentNode, formatDocument, createRequest } from '@urql/core';
···
import { invalidateEntity } from '../operations/invalidate';
import { keyOfField } from './keys';
import * as InMemoryData from './data';
-
import * as SchemaPredicates from '../ast/schemaPredicates';
+
+
import {
+
SchemaIntrospector,
+
buildClientSchema,
+
expectValidKeyingConfig,
+
expectValidUpdatesConfig,
+
expectValidResolversConfig,
+
expectValidOptimisticMutationsConfig,
+
} from '../ast';
type RootField = 'query' | 'mutation' | 'subscription';
···
updates: Record<string, Record<string, UpdateResolver>>;
optimisticMutations: OptimisticMutationConfig;
keys: KeyingConfig;
-
schema?: GraphQLSchema;
+
schema?: SchemaIntrospector;
rootFields: { query: string; mutation: string; subscription: string };
rootNames: { [name: string]: RootField };
···
let subscriptionName = 'Subscription';
if (opts.schema) {
const schema = (this.schema = buildClientSchema(opts.schema));
-
const queryType = schema.getQueryType();
-
const mutationType = schema.getMutationType();
-
const subscriptionType = schema.getSubscriptionType();
-
queryName = queryType ? queryType.name : queryName;
-
mutationName = mutationType ? mutationType.name : mutationName;
-
subscriptionName = subscriptionType
-
? subscriptionType.name
-
: subscriptionName;
+
queryName = schema.query || queryName;
+
mutationName = schema.mutation || mutationName;
+
subscriptionName = schema.subscription || subscriptionName;
}
this.updates = {
···
this.data = InMemoryData.make(queryName);
if (this.schema && process.env.NODE_ENV !== 'production') {
-
SchemaPredicates.expectValidKeyingConfig(this.schema, this.keys);
-
SchemaPredicates.expectValidUpdatesConfig(this.schema, this.updates);
-
SchemaPredicates.expectValidResolversConfig(this.schema, this.resolvers);
-
SchemaPredicates.expectValidOptimisticMutationsConfig(
+
expectValidKeyingConfig(this.schema, this.keys);
+
expectValidUpdatesConfig(this.schema, this.updates);
+
expectValidResolversConfig(this.schema, this.resolvers);
+
expectValidOptimisticMutationsConfig(
this.schema,
this.optimisticMutations
);