Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use improved mapSchema to make schema package functions not modify existing schema #1456

Merged
merged 8 commits into from
May 11, 2020
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
5 changes: 2 additions & 3 deletions packages/delegate/src/transforms/FilterToSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@ import {
getNamedType,
isObjectType,
isInterfaceType,
GraphQLNamedType,
} from 'graphql';

import { Transform, Request, implementsAbstractType } from '@graphql-tools/utils';
import { Transform, Request, implementsAbstractType, TypeMap } from '@graphql-tools/utils';

export default class FilterToSchema implements Transform {
private readonly targetSchema: GraphQLSchema;
Expand Down Expand Up @@ -61,7 +60,7 @@ function filterToSchema(
return Boolean(targetSchema.getType(typeName));
});

const validFragmentsWithType: Record<string, GraphQLNamedType> = validFragments.reduce(
const validFragmentsWithType: TypeMap = validFragments.reduce(
(prev, fragment) => ({
...prev,
[fragment.name.value]: targetSchema.getType(fragment.typeCondition.name.value),
Expand Down
6 changes: 3 additions & 3 deletions packages/merge/src/merge-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,11 @@ function makeSchema(
}: { resolvers: IResolvers; typeDefs: string | DocumentNode; extensions: SchemaExtensions },
config: MergeSchemasConfig
) {
const schema = typeof typeDefs === 'string' ? buildSchema(typeDefs, config) : buildASTSchema(typeDefs, config);
let schema = typeof typeDefs === 'string' ? buildSchema(typeDefs, config) : buildASTSchema(typeDefs, config);

// add resolvers
if (resolvers) {
addResolversToSchema({
schema = addResolversToSchema({
schema,
resolvers,
resolverValidationOptions: {
Expand All @@ -90,7 +90,7 @@ function makeSchema(

// use logger
if (config.logger) {
addErrorLoggingToSchema(schema, config.logger);
schema = addErrorLoggingToSchema(schema, config.logger);
}

// use schema directives
Expand Down
150 changes: 77 additions & 73 deletions packages/mock/src/mocking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,25 @@ import {
GraphQLObjectType,
GraphQLList,
GraphQLType,
GraphQLField,
GraphQLResolveInfo,
getNullableType,
getNamedType,
GraphQLNamedType,
GraphQLFieldResolver,
GraphQLNullableType,
isSchema,
isObjectType,
isUnionType,
isInterfaceType,
isListType,
isEnumType,
isAbstractType,
GraphQLInterfaceType,
GraphQLUnionType,
GraphQLTypeResolver,
} from 'graphql';

import { buildSchemaFromTypeDefinitions } from '@graphql-tools/schema';
import { IMocks, IMockServer, IMockFn, IMockOptions, IMockTypeFn } from './types';
import { forEachField, ITypeDefinitions } from '@graphql-tools/utils';
import { ITypeDefinitions, mapSchema, MapperKind } from '@graphql-tools/utils';

/**
* This function wraps addMocksToSchema for more convenience
Expand All @@ -40,7 +40,7 @@ export function mockServer(
mySchema = schema;
}

addMocksToSchema({ schema: mySchema, mocks, preserveResolvers });
mySchema = addMocksToSchema({ schema: mySchema, mocks, preserveResolvers });

return { query: (query, vars) => graphql(mySchema, query, {}, {}, vars) };
}
Expand All @@ -64,7 +64,7 @@ defaultMockMap.set('ID', () => uuidv4());
// TODO allow providing a seed such that lengths of list could be deterministic
// this could be done by using casual to get a random list length if the casual
// object is global.
export function addMocksToSchema({ schema, mocks = {}, preserveResolvers = false }: IMockOptions): void {
export function addMocksToSchema({ schema, mocks = {}, preserveResolvers = false }: IMockOptions): GraphQLSchema {
if (!schema) {
throw new Error('Must provide schema to mock');
}
Expand Down Expand Up @@ -181,43 +181,74 @@ export function addMocksToSchema({ schema, mocks = {}, preserveResolvers = false
};
};

forEachField(schema, (field: GraphQLField<any, any>, typeName: string, fieldName: string) => {
assignResolveType(field.type, preserveResolvers);
let mockResolver: GraphQLFieldResolver<any, any> = mockType(field.type, typeName, fieldName);

// we have to handle the root mutation and root query types differently,
// because no resolver is called at the root.
const queryType = schema.getQueryType();
const isOnQueryType = queryType != null && queryType.name === typeName;

const mutationType = schema.getMutationType();
const isOnMutationType = mutationType != null && mutationType.name === typeName;

if (isOnQueryType || isOnMutationType) {
if (mockFunctionMap.has(typeName)) {
const rootMock = mockFunctionMap.get(typeName);
// XXX: BUG in here, need to provide proper signature for rootMock.
if (typeof rootMock(undefined, {}, {}, {} as any)[fieldName] === 'function') {
mockResolver = (root: any, args: Record<string, any>, context: any, info: GraphQLResolveInfo) => {
const updatedRoot = root ?? {}; // TODO: should we clone instead?
updatedRoot[fieldName] = rootMock(root, args, context, info)[fieldName];
// XXX this is a bit of a hack to still use mockType, which
// lets you mock lists etc. as well
// otherwise we could just set field.resolve to rootMock()[fieldName]
// it's like pretending there was a resolver that ran before
// the root resolver.
return mockType(field.type, typeName, fieldName)(updatedRoot, args, context, info);
};
return mapSchema(schema, {
[MapperKind.ABSTRACT_TYPE]: type => {
const oldResolveType = type.resolveType;
if (preserveResolvers && oldResolveType != null && oldResolveType.length) {
return;
}

// the default `resolveType` always returns null. We add a fallback
// resolution that works with how unions and interface are mocked
const resolveType: GraphQLTypeResolver<any, any> = (data, _context, info: GraphQLResolveInfo) =>
info.schema.getType(data.__typename) as GraphQLObjectType;

if (isInterfaceType(type)) {
return new GraphQLInterfaceType({
...type.toConfig(),
resolveType,
});
} else {
return new GraphQLUnionType({
...type.toConfig(),
resolveType,
});
}
},
[MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName) => {
const fieldType = fieldConfig.type;
const fieldResolver = fieldConfig.resolve;
const newFieldConfig = {
...fieldConfig,
};

let mockResolver: GraphQLFieldResolver<any, any> = mockType(fieldType, typeName, fieldName);

// we have to handle the root mutation and root query types differently,
// because no resolver is called at the root.
const queryType = schema.getQueryType();
const isOnQueryType = queryType != null && queryType.name === typeName;

const mutationType = schema.getMutationType();
const isOnMutationType = mutationType != null && mutationType.name === typeName;

if (isOnQueryType || isOnMutationType) {
if (mockFunctionMap.has(typeName)) {
const rootMock = mockFunctionMap.get(typeName);
// XXX: BUG in here, need to provide proper signature for rootMock.
if (typeof rootMock(undefined, {}, {}, {} as any)[fieldName] === 'function') {
mockResolver = (root: any, args: Record<string, any>, context: any, info: GraphQLResolveInfo) => {
const updatedRoot = root ?? {}; // TODO: should we clone instead?
updatedRoot[fieldName] = rootMock(root, args, context, info)[fieldName];
// XXX this is a bit of a hack to still use mockType, which
// lets you mock lists etc. as well
// otherwise we could just set field.resolve to rootMock()[fieldName]
// it's like pretending there was a resolver that ran before
// the root resolver.
return mockType(fieldConfig.type, typeName, fieldName)(updatedRoot, args, context, info);
};
}
}
}
}
if (!preserveResolvers || !field.resolve) {
field.resolve = mockResolver;
} else {
const oldResolver = field.resolve;
field.resolve = (rootObject: any, args: Record<string, any>, context: any, info: GraphQLResolveInfo) =>
Promise.all([mockResolver(rootObject, args, context, info), oldResolver(rootObject, args, context, info)]).then(
values => {
if (!preserveResolvers || !fieldResolver) {
newFieldConfig.resolve = mockResolver;
} else {
const oldResolver = fieldResolver;
newFieldConfig.resolve = (rootObject: any, args: Record<string, any>, context: any, info: GraphQLResolveInfo) =>
Promise.all([
mockResolver(rootObject, args, context, info),
oldResolver(rootObject, args, context, info),
]).then(values => {
const [mockedValue, resolvedValue] = values;

// In case we couldn't mock
Expand All @@ -240,9 +271,11 @@ export function addMocksToSchema({ schema, mocks = {}, preserveResolvers = false
return copyOwnProps(emptyObject, resolvedValue, mockedValue);
}
return undefined !== resolvedValue ? resolvedValue : mockedValue;
}
);
}
});
}

return newFieldConfig;
},
});
}

Expand Down Expand Up @@ -294,29 +327,6 @@ function mergeMocks(genericMockFunction: () => any, customMock: any): any {
return customMock;
}

function getResolveType(namedFieldType: GraphQLNamedType) {
if (isAbstractType(namedFieldType)) {
return namedFieldType.resolveType;
}
}

function assignResolveType(type: GraphQLType, preserveResolvers: boolean) {
const fieldType = getNullableType(type) as GraphQLNullableType;
const namedFieldType = getNamedType(fieldType);

const oldResolveType = getResolveType(namedFieldType);
if (preserveResolvers && oldResolveType != null && oldResolveType.length) {
return;
}

if (isInterfaceType(namedFieldType) || isUnionType(namedFieldType)) {
// the default `resolveType` always returns null. We add a fallback
// resolution that works with how unions and interface are mocked
namedFieldType.resolveType = (data: any, _context: any, info: GraphQLResolveInfo) =>
info.schema.getType(data.__typename) as GraphQLObjectType;
}
}

export function isMockList(obj: any): obj is MockList {
if (typeof obj?.len === 'number' || (Array.isArray(obj?.len) && typeof obj?.len[0] === 'number')) {
if (typeof obj.wrappedFunction === 'undefined' || typeof obj.wrappedFunction === 'function') {
Expand Down Expand Up @@ -377,9 +387,3 @@ export class MockList {
return Math.floor(Math.random() * (high - low + 1) + low);
}
}

// retain addMockFunctionsToSchema for backwards compatibility

export function addMockFunctionsToSchema({ schema, mocks = {}, preserveResolvers = false }: IMockOptions): void {
addMocksToSchema({ schema, mocks, preserveResolvers });
}
Loading