Skip to content

Commit

Permalink
fix(HoistField): allow hoisting to root
Browse files Browse the repository at this point in the history
requires passing additional parameters to each transformSchema call beyond the subschemaConfig: the transforms, and the transformedSchema.

Because transformSchema may need to create additional proxying resolvers, it needs all the parameters that would be passed to delegateToSchema.

It is NOT a catch-22 that transformSchema requires the transformed schema as a parameter, because it does NOT require an executable transformedSchema as a parameter, only the non-executable version for type comparisions performed ultimately by delegateToSchema within the proxying process.
  • Loading branch information
yaacovCR committed Oct 1, 2020
1 parent fda89db commit e3083d7
Show file tree
Hide file tree
Showing 35 changed files with 273 additions and 87 deletions.
2 changes: 1 addition & 1 deletion .changeset/weak-peaches-count.md
Expand Up @@ -11,7 +11,7 @@
Breaking Changes:
- Delegation result: ExternalObject concept formalized and matured, resulting in deprecation of slicedError, getErrors, getErrorsByPathSegment functions, see new getUnpathedErrors function for errors that cannot be merged into the ExternalObject. See as well new annotateExternalObject and mergeExternalObjects functions. Rename handleResult to resolveExternalValue.

- Transforms: transforms now take delegationContext and transformationContext arguments within their transformRequest/Result methods and take an optional subschemaConfig as the second argument for transformSchema. Transform types and applySchemaTransforms now within delegate package; applyRequest/ResultTransforms functions deprecated.
- Transforms: transforms now take delegationContext and transformationContext arguments within their transformRequest/Result methods. the transformSchema method may wish to create additional delegating resolvers and so it takes the subschemaConfig, transforms, and (non-executable) transformed schema as optional parameters. Transform types and applySchemaTransforms now within delegate package; applyRequest/ResultTransforms functions deprecated.

- Wrapping schema standardization: remove support for createMergedResolver and non-standard transforms where resolvers do not use defaultMergedResolver. This is still possible, but not supported out of the box due to conflicts with type merging, where resolvers expected to be identical across subschemas.

Expand Down
2 changes: 1 addition & 1 deletion packages/delegate/src/Subschema.ts
Expand Up @@ -89,7 +89,7 @@ export class Subschema<K = any, V = any, C = K> {

this.createProxyingResolver = config.createProxyingResolver;
this.transforms = config.transforms ?? [];
this.transformedSchema = applySchemaTransforms(this.schema, this.transforms, config);
this.transformedSchema = applySchemaTransforms(this.schema, config);

this.merge = config.merge;
}
Expand Down
21 changes: 17 additions & 4 deletions packages/delegate/src/applySchemaTransforms.ts
Expand Up @@ -6,12 +6,25 @@ import { SubschemaConfig, Transform } from './types';

export function applySchemaTransforms(
originalWrappingSchema: GraphQLSchema,
transforms: Array<Transform>,
subschema: SubschemaConfig
subschemaConfig?: SubschemaConfig,
transforms?: Array<Transform>,
transformedSchema?: GraphQLSchema
): GraphQLSchema {
return transforms.reduce(
let schemaTransforms: Array<Transform> = [];

if (subschemaConfig?.transforms != null) {
schemaTransforms = schemaTransforms.concat(subschemaConfig.transforms);
}

if (transforms) {
schemaTransforms = schemaTransforms.concat(transforms);
}

return schemaTransforms.reduce(
(schema: GraphQLSchema, transform: Transform) =>
transform.transformSchema != null ? transform.transformSchema(cloneSchema(schema), subschema) : schema,
transform.transformSchema != null
? transform.transformSchema(cloneSchema(schema), subschemaConfig, transforms, transformedSchema)
: schema,
originalWrappingSchema
);
}
4 changes: 3 additions & 1 deletion packages/delegate/src/types.ts
Expand Up @@ -23,7 +23,9 @@ import { OBJECT_SUBSCHEMA_SYMBOL, FIELD_SUBSCHEMA_MAP_SYMBOL, UNPATHED_ERRORS_SY

export type SchemaTransform = (
originalWrappingSchema: GraphQLSchema,
subschemaConfig?: SubschemaConfig
subschemaConfig?: SubschemaConfig,
transforms?: Array<Transform>,
transformedSchema?: GraphQLSchema
) => GraphQLSchema;
export type RequestTransform<T = Record<string, any>> = (
originalRequest: Request,
Expand Down
18 changes: 18 additions & 0 deletions packages/stitch/tests/alternateStitchSchemas.test.ts
Expand Up @@ -1176,6 +1176,7 @@ describe('schema transformation with extraction of nested fields', () => {
},
});
});

});

describe('HoistField transform', () => {
Expand Down Expand Up @@ -1270,6 +1271,23 @@ describe('HoistField transform', () => {

expect(result).toEqual(expectedResult);
});

test('should work to hoist fields to new root fields', async () => {
const wrappedSchema = wrapSchema({
schema,
transforms: [new HoistField('Query', ['query', 'inner', 'test'], 'hoisted'), new PruneSchema({})],
})

const result = await graphql(wrappedSchema, '{ hoisted }');

const expectedResult = {
data: {
hoisted: 'test',
},
};

expect(result).toEqual(expectedResult);
});
});

describe('schema transformation with wrapping of object fields', () => {
Expand Down
10 changes: 1 addition & 9 deletions packages/wrap/src/generateProxyingResolvers.ts
Expand Up @@ -20,27 +20,19 @@ export function generateProxyingResolvers(
transforms: Array<Transform>
): Record<string, Record<string, GraphQLFieldResolver<any, any>>> {
let targetSchema: GraphQLSchema;
let schemaTransforms: Array<Transform> = [];
let createProxyingResolver: CreateProxyingResolverFn;
let subschemaConfig: SubschemaConfig;

if (isSubschemaConfig(subschemaOrSubschemaConfig)) {
targetSchema = subschemaOrSubschemaConfig.schema;
subschemaConfig = subschemaOrSubschemaConfig;
createProxyingResolver = subschemaOrSubschemaConfig.createProxyingResolver ?? defaultCreateProxyingResolver;
if (subschemaOrSubschemaConfig.transforms != null) {
schemaTransforms = schemaTransforms.concat(subschemaOrSubschemaConfig.transforms);
}
} else {
targetSchema = subschemaOrSubschemaConfig;
createProxyingResolver = defaultCreateProxyingResolver;
}

if (transforms != null) {
schemaTransforms = schemaTransforms.concat(transforms);
}

const transformedSchema = applySchemaTransforms(targetSchema, schemaTransforms, subschemaConfig);
const transformedSchema = applySchemaTransforms(targetSchema, subschemaConfig, transforms);

const operationTypes: Record<Operation, GraphQLObjectType> = {
query: targetSchema.getQueryType(),
Expand Down
9 changes: 7 additions & 2 deletions packages/wrap/src/transforms/FilterInputObjectFields.ts
Expand Up @@ -20,8 +20,13 @@ export default class FilterInputObjectFields implements Transform {
);
}

public transformSchema(originalWrappingSchema: GraphQLSchema, subschemaConfig?: SubschemaConfig): GraphQLSchema {
return this.transformer.transformSchema(originalWrappingSchema, subschemaConfig);
public transformSchema(
originalWrappingSchema: GraphQLSchema,
subschemaConfig?: SubschemaConfig,
transforms?: Array<Transform>,
transformedSchema?: GraphQLSchema
): GraphQLSchema {
return this.transformer.transformSchema(originalWrappingSchema, subschemaConfig, transforms, transformedSchema);
}

public transformRequest(
Expand Down
9 changes: 7 additions & 2 deletions packages/wrap/src/transforms/FilterInterfaceFields.ts
Expand Up @@ -16,7 +16,12 @@ export default class FilterInterfaceFields implements Transform {
);
}

public transformSchema(originalWrappingSchema: GraphQLSchema, subschemaConfig?: SubschemaConfig): GraphQLSchema {
return this.transformer.transformSchema(originalWrappingSchema, subschemaConfig);
public transformSchema(
originalWrappingSchema: GraphQLSchema,
subschemaConfig?: SubschemaConfig,
transforms?: Array<Transform>,
transformedSchema?: GraphQLSchema
): GraphQLSchema {
return this.transformer.transformSchema(originalWrappingSchema, subschemaConfig, transforms, transformedSchema);
}
}
9 changes: 7 additions & 2 deletions packages/wrap/src/transforms/FilterObjectFieldDirectives.ts
Expand Up @@ -13,7 +13,12 @@ export default class FilterObjectFieldDirectives implements Transform {
this.filter = filter;
}

public transformSchema(originalWrappingSchema: GraphQLSchema, _subschemaConfig?: SubschemaConfig): GraphQLSchema {
public transformSchema(
originalWrappingSchema: GraphQLSchema,
subschemaConfig?: SubschemaConfig,
transforms?: Array<Transform>,
transformedSchema?: GraphQLSchema
): GraphQLSchema {
const transformer = new TransformObjectFields(
(_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig<any, any>) => {
const keepDirectives = fieldConfig.astNode.directives.filter(dir => {
Expand All @@ -35,6 +40,6 @@ export default class FilterObjectFieldDirectives implements Transform {
}
);

return transformer.transformSchema(originalWrappingSchema);
return transformer.transformSchema(originalWrappingSchema, subschemaConfig, transforms, transformedSchema);
}
}
9 changes: 7 additions & 2 deletions packages/wrap/src/transforms/FilterObjectFields.ts
Expand Up @@ -16,7 +16,12 @@ export default class FilterObjectFields implements Transform {
);
}

public transformSchema(originalWrappingSchema: GraphQLSchema, subschemaConfig?: SubschemaConfig): GraphQLSchema {
return this.transformer.transformSchema(originalWrappingSchema, subschemaConfig);
public transformSchema(
originalWrappingSchema: GraphQLSchema,
subschemaConfig?: SubschemaConfig,
transforms?: Array<Transform>,
transformedSchema?: GraphQLSchema
): GraphQLSchema {
return this.transformer.transformSchema(originalWrappingSchema, subschemaConfig, transforms, transformedSchema);
}
}
9 changes: 7 additions & 2 deletions packages/wrap/src/transforms/FilterRootFields.ts
Expand Up @@ -25,7 +25,12 @@ export default class FilterRootFields implements Transform {
);
}

public transformSchema(originalWrappingSchema: GraphQLSchema, subschemaConfig?: SubschemaConfig): GraphQLSchema {
return this.transformer.transformSchema(originalWrappingSchema, subschemaConfig);
public transformSchema(
originalWrappingSchema: GraphQLSchema,
subschemaConfig?: SubschemaConfig,
transforms?: Array<Transform>,
transformedSchema?: GraphQLSchema
): GraphQLSchema {
return this.transformer.transformSchema(originalWrappingSchema, subschemaConfig, transforms, transformedSchema);
}
}
11 changes: 8 additions & 3 deletions packages/wrap/src/transforms/FilterTypes.ts
Expand Up @@ -2,7 +2,7 @@ import { GraphQLSchema, GraphQLNamedType } from 'graphql';

import { mapSchema, MapperKind } from '@graphql-tools/utils';

import { Transform } from '@graphql-tools/delegate';
import { SubschemaConfig, Transform } from '@graphql-tools/delegate';

export default class FilterTypes implements Transform {
private readonly filter: (type: GraphQLNamedType) => boolean;
Expand All @@ -11,8 +11,13 @@ export default class FilterTypes implements Transform {
this.filter = filter;
}

public transformSchema(schema: GraphQLSchema): GraphQLSchema {
return mapSchema(schema, {
public transformSchema(
originalWrappingSchema: GraphQLSchema,
_subschemaConfig?: SubschemaConfig,
_transforms?: Array<Transform>,
_transformedSchema?: GraphQLSchema
): GraphQLSchema {
return mapSchema(originalWrappingSchema, {
[MapperKind.TYPE]: (type: GraphQLNamedType) => {
if (this.filter(type)) {
return undefined;
Expand Down
43 changes: 34 additions & 9 deletions packages/wrap/src/transforms/HoistField.ts
Expand Up @@ -6,11 +6,14 @@ import {
Kind,
GraphQLError,
GraphQLArgument,
GraphQLFieldConfig,
} from 'graphql';

import { appendObjectFields, removeObjectFields, Request, ExecutionResult, relocatedError } from '@graphql-tools/utils';

import { Transform, defaultMergedResolver, DelegationContext } from '@graphql-tools/delegate';
import { Transform, defaultMergedResolver, DelegationContext, SubschemaConfig } from '@graphql-tools/delegate';

import { defaultCreateProxyingResolver } from '../generateProxyingResolvers';

import MapFields from './MapFields';

Expand Down Expand Up @@ -61,7 +64,12 @@ export default class HoistField implements Transform {
this.argLevels = argLevels;
}

public transformSchema(schema: GraphQLSchema): GraphQLSchema {
public transformSchema(
originalWrappingSchema: GraphQLSchema,
subschemaConfig: SubschemaConfig,
transforms: Array<Transform>,
transformedSchema: GraphQLSchema
): GraphQLSchema {
const argsMap: Record<string, GraphQLArgument> = Object.create(null);
const innerType: GraphQLObjectType = this.pathToField.reduce((acc, pathSegment, index) => {
const field = acc.getFields()[pathSegment];
Expand All @@ -72,21 +80,38 @@ export default class HoistField implements Transform {
}
});
return getNullableType(field.type) as GraphQLObjectType;
}, schema.getType(this.typeName) as GraphQLObjectType);
}, originalWrappingSchema.getType(this.typeName) as GraphQLObjectType);

let [newSchema, targetFieldConfigMap] = removeObjectFields(
schema,
originalWrappingSchema,
innerType.name,
fieldName => fieldName === this.oldFieldName
);

const targetField = targetFieldConfigMap[this.oldFieldName];

const newTargetField = {
...targetField,
resolve: defaultMergedResolver,
};

const hoistingToRootField =
this.typeName === originalWrappingSchema.getQueryType()?.name || originalWrappingSchema.getMutationType()?.name;

let newTargetField: GraphQLFieldConfig<any, any>;
if (hoistingToRootField) {
const createProxyingResolver = subschemaConfig.createProxyingResolver ?? defaultCreateProxyingResolver;
newTargetField = {
...targetField,
resolve: createProxyingResolver({
schema: subschemaConfig,
transforms,
transformedSchema,
operation: this.typeName === originalWrappingSchema.getQueryType().name ? 'query' : 'mutation',
fieldName: this.newFieldName,
}),
};
} else {
newTargetField = {
...targetField,
resolve: defaultMergedResolver,
};
}
const level = this.pathToField.length;

Object.keys(targetField.args).forEach(argName => {
Expand Down
9 changes: 7 additions & 2 deletions packages/wrap/src/transforms/MapFields.ts
Expand Up @@ -54,8 +54,13 @@ export default class MapFields implements Transform {
);
}

public transformSchema(originalWrappingSchema: GraphQLSchema, subschemaConfig?: SubschemaConfig): GraphQLSchema {
return this.transformer.transformSchema(originalWrappingSchema, subschemaConfig);
public transformSchema(
originalWrappingSchema: GraphQLSchema,
subschemaConfig?: SubschemaConfig,
transforms?: Array<Transform>,
transformedSchema?: GraphQLSchema
): GraphQLSchema {
return this.transformer.transformSchema(originalWrappingSchema, subschemaConfig, transforms, transformedSchema);
}

public transformRequest(
Expand Down
7 changes: 6 additions & 1 deletion packages/wrap/src/transforms/MapLeafValues.ts
Expand Up @@ -44,7 +44,12 @@ export default class MapLeafValues implements Transform<MapLeafValuesTransformat
this.resultVisitorMap = Object.create(null);
}

public transformSchema(originalWrappingSchema: GraphQLSchema, _subschemaConfig: SubschemaConfig): GraphQLSchema {
public transformSchema(
originalWrappingSchema: GraphQLSchema,
_subschemaConfig: SubschemaConfig,
_transforms?: Array<Transform>,
_transformedSchema?: GraphQLSchema
): GraphQLSchema {
this.originalWrappingSchema = originalWrappingSchema;
const typeMap = originalWrappingSchema.getTypeMap();
Object.keys(typeMap).forEach(typeName => {
Expand Down
11 changes: 8 additions & 3 deletions packages/wrap/src/transforms/PruneSchema.ts
Expand Up @@ -2,7 +2,7 @@ import { GraphQLSchema } from 'graphql';

import { PruneSchemaOptions, pruneSchema } from '@graphql-tools/utils';

import { Transform } from '@graphql-tools/delegate';
import { SubschemaConfig, Transform } from '@graphql-tools/delegate';

export default class PruneTypes implements Transform {
private readonly options: PruneSchemaOptions;
Expand All @@ -11,7 +11,12 @@ export default class PruneTypes implements Transform {
this.options = options;
}

public transformSchema(schema: GraphQLSchema): GraphQLSchema {
return pruneSchema(schema, this.options);
public transformSchema(
originalWrappingSchema: GraphQLSchema,
_subschemaConfig?: SubschemaConfig,
_transforms?: Array<Transform>,
_transformedSchema?: GraphQLSchema
): GraphQLSchema {
return pruneSchema(originalWrappingSchema, this.options);
}
}
11 changes: 9 additions & 2 deletions packages/wrap/src/transforms/RemoveObjectFieldDeprecations.ts
Expand Up @@ -27,10 +27,17 @@ export default class RemoveObjectFieldDeprecations implements Transform {
);
}

public transformSchema(originalWrappingSchema: GraphQLSchema, subschemaConfig?: SubschemaConfig): GraphQLSchema {
public transformSchema(
originalWrappingSchema: GraphQLSchema,
subschemaConfig?: SubschemaConfig,
transforms?: Array<Transform>,
transformedSchema?: GraphQLSchema
): GraphQLSchema {
return this.removeDeprecations.transformSchema(
this.removeDirectives.transformSchema(originalWrappingSchema, subschemaConfig),
subschemaConfig
subschemaConfig,
transforms,
transformedSchema
);
}
}
9 changes: 7 additions & 2 deletions packages/wrap/src/transforms/RemoveObjectFieldDirectives.ts
Expand Up @@ -15,7 +15,12 @@ export default class RemoveObjectFieldDirectives implements Transform {
});
}

public transformSchema(originalWrappingSchema: GraphQLSchema, subschemaConfig?: SubschemaConfig): GraphQLSchema {
return this.transformer.transformSchema(originalWrappingSchema, subschemaConfig);
public transformSchema(
originalWrappingSchema: GraphQLSchema,
subschemaConfig?: SubschemaConfig,
transforms?: Array<Transform>,
transformedSchema?: GraphQLSchema
): GraphQLSchema {
return this.transformer.transformSchema(originalWrappingSchema, subschemaConfig, transforms, transformedSchema);
}
}

0 comments on commit e3083d7

Please sign in to comment.