Skip to content

Commit

Permalink
Improve Stitched Schema generation (#4566)
Browse files Browse the repository at this point in the history
* Avoid transformedSchema

* Cleanup

* More

* Go

* Cleanup

* Remove 'computedFields'

* Less diff

* More

* Memoize applySchemaTransforms
  • Loading branch information
ardatan committed Aug 9, 2022
1 parent 047087a commit d8dc67a
Show file tree
Hide file tree
Showing 48 changed files with 290 additions and 427 deletions.
46 changes: 46 additions & 0 deletions .changeset/tiny-zoos-dance.md
@@ -0,0 +1,46 @@
---
'@graphql-tools/delegate': major
'@graphql-tools/wrap': major
---

Breaking changes;

**Schema generation optimization by removing `transfomedSchema` parameter**

Previously we were applying the transforms multiple times. We needed to introduced some breaking changes to improve the initial wrapped/stitched schema generation performance;

- `Transform.transformSchema` no longer accepts `transformedSchema` which can easily be created with `applySchemaTransforms(schema, subschemaConfig)` instead.
- Proxying resolver factory function that is passed as `createProxyingResolver` to `SubschemaConfig` no longer takes `transformedSchema` which can easily be created with `applySchemaTransforms(schema, subschemaConfig)` instead.

**`stitchSchemas` doesn't take nested arrays of subschemas**

`stitchSchemas` no longer accepts an array of arrays of subschema configuration objects. Instead, it accepts an array of subschema configuration objects or schema objects directly.

**`stitchSchemas` no longer prunes the schema with `pruningOptions`**

You can use `pruneSchema` from `@graphql-tools/utils` to prune the schema instead.

**`stitchSchemas` no longer respect "@computed" directive if stitchingDirectivesTransformer isn't applied**

Also `@graphql-tools/stitch` no longer exports `computedDirectiveTransformer` and `defaultSubschemaConfigTransforms`.
Instead, use `@graphql-tools/stitching-directives` package for `@computed` directive.
[Learn more about setting it up](https://www.graphql-tools.com/docs/schema-stitching/stitch-directives-sdl#directives-glossary)

**`computedFields` has been removed from the merged type configuration**

`MergeTypeConfig.computedFields` setting has been removed in favor of new computed field configuration written as:

```js
merge: {
MyType: {
fields: {
myComputedField: {
selectionSet: '{ weight }',
computed: true,
}
}
}
}
```

A field-level `selectionSet` specifies field dependencies while the `computed` setting structures the field in a way that assures it is always selected with this data provided. The `selectionSet` is intentionally generic to support possible future uses. This new pattern organizes all field-level configuration (including `canonical`) into a single structure.
14 changes: 6 additions & 8 deletions packages/delegate/src/applySchemaTransforms.ts
@@ -1,11 +1,12 @@
import { memoize2 } from '@graphql-tools/utils';
import { GraphQLSchema } from 'graphql';

import { SubschemaConfig } from './types.js';

export function applySchemaTransforms(
// TODO: Instead of memoization, we can make sure that this isn't called multiple times
export const applySchemaTransforms = memoize2(function applySchemaTransforms(
originalWrappingSchema: GraphQLSchema,
subschemaConfig: SubschemaConfig<any, any, any, any>,
transformedSchema?: GraphQLSchema
subschemaConfig: SubschemaConfig<any, any, any, any>
): GraphQLSchema {
const schemaTransforms = subschemaConfig.transforms;

Expand All @@ -14,10 +15,7 @@ export function applySchemaTransforms(
}

return schemaTransforms.reduce(
(schema: GraphQLSchema, transform) =>
transform.transformSchema != null
? transform.transformSchema(schema, subschemaConfig, transformedSchema)
: schema,
(schema: GraphQLSchema, transform) => transform.transformSchema?.(schema, subschemaConfig) || schema,
originalWrappingSchema
);
}
});
5 changes: 4 additions & 1 deletion packages/delegate/src/delegateToSchema.ts
Expand Up @@ -38,6 +38,7 @@ import { isSubschemaConfig } from './subschemaConfig.js';
import { Subschema } from './Subschema.js';
import { createRequest, getDelegatingOperation } from './createRequest.js';
import { Transformer } from './Transformer.js';
import { applySchemaTransforms } from './applySchemaTransforms.js';

export function delegateToSchema<
TContext extends Record<string, any> = Record<string, any>,
Expand Down Expand Up @@ -160,7 +161,9 @@ function getDelegationContext<TContext extends Record<string, any>>({
: transforms,
transformedSchema:
transformedSchema ??
(subschemaOrSubschemaConfig instanceof Subschema ? subschemaOrSubschemaConfig.transformedSchema : targetSchema),
(subschemaOrSubschemaConfig instanceof Subschema
? subschemaOrSubschemaConfig.transformedSchema
: applySchemaTransforms(targetSchema, subschemaOrSubschemaConfig)),
skipTypeMerging,
};
}
Expand Down
5 changes: 1 addition & 4 deletions packages/delegate/src/types.ts
Expand Up @@ -22,8 +22,7 @@ import { OBJECT_SUBSCHEMA_SYMBOL, FIELD_SUBSCHEMA_MAP_SYMBOL, UNPATHED_ERRORS_SY

export type SchemaTransform<TContext = Record<any, string>> = (
originalWrappingSchema: GraphQLSchema,
subschemaConfig: SubschemaConfig<any, any, any, TContext>,
transformedSchema?: GraphQLSchema
subschemaConfig: SubschemaConfig<any, any, any, TContext>
) => GraphQLSchema;
export type RequestTransform<T = Record<string, any>, TContext = Record<any, string>> = (
originalRequest: ExecutionRequest,
Expand Down Expand Up @@ -123,7 +122,6 @@ export interface MergedTypeInfo<TContext = Record<string, any>> {

export interface ICreateProxyingResolverOptions<TContext = Record<string, any>> {
subschemaConfig: SubschemaConfig<any, any, any, TContext>;
transformedSchema?: GraphQLSchema;
operation?: OperationTypeNode;
fieldName?: string;
}
Expand Down Expand Up @@ -152,7 +150,6 @@ export interface MergedTypeConfig<K = any, V = any, TContext = Record<string, an
extends MergedTypeEntryPoint<K, V, TContext> {
entryPoints?: Array<MergedTypeEntryPoint>;
fields?: Record<string, MergedFieldConfig>;
computedFields?: Record<string, { selectionSet?: string }>;
canonical?: boolean;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/delegate/tests/batchExecution.test.ts
Expand Up @@ -162,7 +162,7 @@ describe('batch execution', () => {
};

const outerSchemaWithSubschemasAsArray = stitchSchemas({
subschemas: [innerSubschemaConfigA, innerSubschemaConfigB],
subschemas: [...innerSubschemaConfigA, innerSubschemaConfigB],
});

const resultWhenAsArray = await graphql({ schema: outerSchemaWithSubschemasAsArray, source: query });
Expand Down
72 changes: 19 additions & 53 deletions packages/stitch/src/stitchSchemas.ts
@@ -1,13 +1,6 @@
import {
DocumentNode,
GraphQLObjectType,
GraphQLSchema,
GraphQLDirective,
specifiedDirectives,
extendSchema,
} from 'graphql';
import { GraphQLObjectType, GraphQLSchema, GraphQLDirective, specifiedDirectives, extendSchema } from 'graphql';

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

import { addResolversToSchema, assertResolversPresent, extendResolversFromInterfaces } from '@graphql-tools/schema';

Expand All @@ -18,7 +11,6 @@ import { IStitchSchemasOptions, SubschemaConfigTransform } from './types.js';
import { buildTypeCandidates, buildTypes } from './typeCandidates.js';
import { createStitchingInfo, completeStitchingInfo, addStitchingInfo } from './stitchingInfo.js';
import {
defaultSubschemaConfigTransforms,
isolateComputedFieldsTransformer,
splitMergedTypeEntryPointsTransformer,
} from './subschemaConfigTransforms/index.js';
Expand All @@ -27,24 +19,19 @@ import { applyExtensions, mergeExtensions, mergeResolvers } from '@graphql-tools
export function stitchSchemas<TContext extends Record<string, any> = Record<string, any>>({
subschemas = [],
types = [],
typeDefs,
typeDefs = [],
onTypeConflict,
mergeDirectives,
mergeTypes = true,
typeMergingOptions,
subschemaConfigTransforms = defaultSubschemaConfigTransforms,
subschemaConfigTransforms = [],
resolvers = {},
inheritResolversFromInterfaces = false,
resolverValidationOptions = {},
parseOptions = {},
pruningOptions,
updateResolversInPlace,
updateResolversInPlace = true,
schemaExtensions,
}: IStitchSchemasOptions<TContext>): GraphQLSchema {
if (typeof resolverValidationOptions !== 'object') {
throw new Error('Expected `resolverValidationOptions` to be an object');
}

const transformedSubschemas: Array<Subschema<any, any, any, TContext>> = [];
const subschemaMap: Map<
GraphQLSchema | SubschemaConfig<any, any, any, TContext>,
Expand All @@ -55,45 +42,29 @@ export function stitchSchemas<TContext extends Record<string, any> = Record<stri
GraphQLSchema | SubschemaConfig<any, any, any, TContext>
> = new Map();

for (const subschemaOrSubschemaArray of subschemas) {
if (Array.isArray(subschemaOrSubschemaArray)) {
for (const s of subschemaOrSubschemaArray) {
for (const transformedSubschemaConfig of applySubschemaConfigTransforms(
subschemaConfigTransforms,
s,
subschemaMap,
originalSubschemaMap
)) {
transformedSubschemas.push(transformedSubschemaConfig);
}
}
} else {
for (const transformedSubschemaConfig of applySubschemaConfigTransforms(
subschemaConfigTransforms,
subschemaOrSubschemaArray,
subschemaMap,
originalSubschemaMap
)) {
transformedSubschemas.push(transformedSubschemaConfig);
}
for (const subschema of subschemas) {
for (const transformedSubschemaConfig of applySubschemaConfigTransforms(
subschemaConfigTransforms,
subschema,
subschemaMap,
originalSubschemaMap
)) {
transformedSubschemas.push(transformedSubschemaConfig);
}
}

const extensions: Array<DocumentNode> = [];

const directiveMap: Record<string, GraphQLDirective> = Object.create(null);
for (const directive of specifiedDirectives) {
directiveMap[directive.name] = directive;
}
const schemaDefs = Object.create(null);

const [typeCandidates, rootTypeNameMap] = buildTypeCandidates({
const [typeCandidates, rootTypeNameMap, extensions] = buildTypeCandidates({
subschemas: transformedSubschemas,
originalSubschemaMap,
types,
typeDefs: typeDefs || [],
typeDefs,
parseOptions,
extensions,
directiveMap,
schemaDefs,
mergeDirectives,
Expand Down Expand Up @@ -146,18 +117,13 @@ export function stitchSchemas<TContext extends Record<string, any> = Record<stri
updateResolversInPlace,
});

if (
Object.keys(resolverValidationOptions).length > 0 &&
Object.values(resolverValidationOptions).some(o => o !== 'ignore')
) {
const resolverValidationOptionsEntries = Object.entries(resolverValidationOptions);

if (resolverValidationOptionsEntries.length > 0 && resolverValidationOptionsEntries.some(([, o]) => o !== 'ignore')) {
assertResolversPresent(schema, resolverValidationOptions);
}

schema = addStitchingInfo(schema, stitchingInfo);

if (pruningOptions) {
schema = pruneSchema(schema, pruningOptions);
}
addStitchingInfo(schema, stitchingInfo);

if (schemaExtensions) {
if (Array.isArray(schemaExtensions)) {
Expand Down
13 changes: 5 additions & 8 deletions packages/stitch/src/stitchingInfo.ts
Expand Up @@ -336,14 +336,11 @@ function updateArrayMap<T>(
export function addStitchingInfo<TContext = Record<string, any>>(
stitchedSchema: GraphQLSchema,
stitchingInfo: StitchingInfo<TContext>
): GraphQLSchema {
return new GraphQLSchema({
...stitchedSchema.toConfig(),
extensions: {
...stitchedSchema.extensions,
stitchingInfo,
},
});
) {
stitchedSchema.extensions = {
...stitchedSchema.extensions,
stitchingInfo,
};
}

export function selectionSetContainsTopLevelField(selectionSet: SelectionSetNode, fieldName: string) {
Expand Down

This file was deleted.

8 changes: 0 additions & 8 deletions packages/stitch/src/subschemaConfigTransforms/index.ts
@@ -1,10 +1,2 @@
import { SubschemaConfigTransform } from '../types.js';
import { computedDirectiveTransformer } from './computedDirectiveTransformer.js';

export { computedDirectiveTransformer } from './computedDirectiveTransformer.js';
export { isolateComputedFieldsTransformer } from './isolateComputedFieldsTransformer.js';
export { splitMergedTypeEntryPointsTransformer } from './splitMergedTypeEntryPointsTransformer.js';

export const defaultSubschemaConfigTransforms: Array<SubschemaConfigTransform<any>> = [
computedDirectiveTransformer('computed'),
];
Expand Up @@ -19,23 +19,6 @@ export function isolateComputedFieldsTransformer(subschemaConfig: SubschemaConfi

baseSchemaTypes[typeName] = mergedTypeConfig;

if (mergedTypeConfig.computedFields) {
const mergeConfigFields = mergedTypeConfig.fields ?? Object.create(null);
for (const fieldName in mergedTypeConfig.computedFields) {
const mergedFieldConfig = mergedTypeConfig.computedFields[fieldName];
console.warn(
`The "computedFields" setting is deprecated. Update your @graphql-tools/stitching-directives package, and/or update static merged type config to "${typeName}.fields.${fieldName} = { selectionSet: '${mergedFieldConfig.selectionSet}', computed: true }"`
);
mergeConfigFields[fieldName] = {
...(mergeConfigFields[fieldName] ?? {}),
...mergedFieldConfig,
computed: true,
};
}
delete mergedTypeConfig.computedFields;
mergedTypeConfig.fields = mergeConfigFields;
}

if (mergedTypeConfig.fields) {
const baseFields: Record<string, MergedFieldConfig> = Object.create(null);
const isolatedFields: Record<string, MergedFieldConfig> = Object.create(null);
Expand Down
7 changes: 3 additions & 4 deletions packages/stitch/src/typeCandidates.ts
Expand Up @@ -40,7 +40,6 @@ export function buildTypeCandidates<TContext extends Record<string, any> = Recor
types,
typeDefs,
parseOptions,
extensions,
directiveMap,
schemaDefs,
mergeDirectives,
Expand All @@ -53,14 +52,14 @@ export function buildTypeCandidates<TContext extends Record<string, any> = Recor
types: Array<GraphQLNamedType>;
typeDefs: TypeSource;
parseOptions: GraphQLParseOptions;
extensions: Array<DocumentNode>;
directiveMap: Record<string, GraphQLDirective>;
schemaDefs: {
schemaDef: SchemaDefinitionNode;
schemaExtensions: Array<SchemaExtensionNode>;
};
mergeDirectives?: boolean | undefined;
}): [Record<string, Array<MergeTypeCandidate<TContext>>>, Record<OperationTypeNode, string>] {
}): [Record<string, Array<MergeTypeCandidate<TContext>>>, Record<OperationTypeNode, string>, DocumentNode[]] {
const extensions: Array<DocumentNode> = [];
const typeCandidates: Record<string, Array<MergeTypeCandidate<TContext>>> = Object.create(null);

let schemaDef: SchemaDefinitionNode | undefined;
Expand Down Expand Up @@ -148,7 +147,7 @@ export function buildTypeCandidates<TContext extends Record<string, any> = Recor
addTypeCandidate(typeCandidates, type.name, { type });
}

return [typeCandidates, rootTypeNameMap];
return [typeCandidates, rootTypeNameMap, extensions];
}

function getRootTypeNameMap({
Expand Down

0 comments on commit d8dc67a

Please sign in to comment.