Skip to content

Commit

Permalink
Merge pull request #94 from tlivings/master
Browse files Browse the repository at this point in the history
Introduce schema pruning and remove broken makeExecutableSchema override
  • Loading branch information
tlivings committed Mar 10, 2022
2 parents 0120f81 + 5949a1c commit 4354d71
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 114 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ federation (2 subschema services implemented via `GraphQLComponent` and a vanill
- `dataSources` - an array of data sources instances to make available on `context.dataSources` .
- `dataSourceOverrides` - overrides for data sources in the component tree.
- `federation` - make this component's schema an Apollo Federated schema (default: `false`).
- `makeExecutableSchema` - an optional custom implementation of the function which builds the executable schema from the provided type definitions, imports and resolvers. Defaults to the version provided by the [`graphql-tools`](https://www.graphql-tools.com/docs/generate-schema) package. Not used when `federation` is set to `true`.
- `pruneSchema` - (optional) prune the schema according to [pruneSchema in graphql-tools](https://www.graphql-tools.com/docs/api/modules/utils_src#pruneschema) (default: false)
- `pruneSchemaOptions` - (optional) schema options as per [PruneSchemaOptions in graphql-tools](https://www.graphql-tools.com/docs/api/interfaces/utils_src.PruneSchemaOptions)

- `static GraphQLComponent.delegateToComponent(component, options)` - a wrapper function that utilizes `graphql-tools` `delegateToSchema()` to delegate the calling resolver's selection set to a root type field (`Query`, `Mutuation`) of another `GraphQLComponent`'s schema
- `component` (instance of `GraphQLComponent`) - the component's whose schema will be the target of the delegated operation
Expand Down
215 changes: 123 additions & 92 deletions lib/__tests__.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const gql = require('graphql-tag');
const { SchemaDirectiveVisitor } = require('@graphql-tools/utils');
const graphql = require('graphql');
const GraphQLComponent = require('./index');
const sinon = require('sinon');

Test('GraphQLComponent instance API (getters/setters)', (t) => {

Expand Down Expand Up @@ -963,7 +962,6 @@ Test('components with federated schemas can be stitched locally by importing roo

const { schema } = emptyRoot;

console.log(graphql.printSchema(schema));
const _serviceType = schema.getType('_Service');
const _serviceTypeFields = _serviceType.getFields();
const _entityType = schema.getType('_Entity');
Expand Down Expand Up @@ -1151,112 +1149,145 @@ Test('federated schema can include custom directive', (t) => {
});
});

Test('custom makeExecutableSchema option', (t) => {

const baseComponentOptions = {
types: [`
type A {
value: String
Test('pruning schema', (t) => {

t.test('does not prune types used only at root', (t) => {
const component1 = new GraphQLComponent({
types: `
type Query {
property(id: ID!): Property
}
type Property {
id: ID!
geo: [String]
}
`,
resolvers: {
Query: {
property(_, { id }) {
return {
id,
geo: ['lat', 'long']
}
}
}
}
type Query {
a: A
}
`]
};

t.test('component schema uses custom makeExecutableSchema when not federated', async (st) => {
st.plan(2);
const customMakeExecutableSchema = sinon.spy();
const componentA = new GraphQLComponent({
...baseComponentOptions,
makeExecutableSchema: customMakeExecutableSchema,
federation: false
});

const component2 = new GraphQLComponent({
types: `
type Query {
reviews(propertyId: ID!): [Review]
}
type Review {
id: ID!
content: String
}
st.doesNotThrow(() => componentA.schema, 'builds schema correctly');
st.equal(customMakeExecutableSchema.calledOnce, true, 'calls custom makeExecutableSchema');
});

t.test('component schema uses custom makeExecutableSchema when federation is unspecified', async (st) => {
st.plan(2);
const customMakeExecutableSchema = sinon.spy();
const componentA = new GraphQLComponent({
...baseComponentOptions,
makeExecutableSchema: customMakeExecutableSchema
type Property {
reviews: [Review]
}
`,
resolvers: {
Query: {
reviews() {
return {
id: 'rev-id-1',
content: 'content'
}
}
},
}
});

st.doesNotThrow(() => componentA.schema, 'builds schema correctly');
st.equal(customMakeExecutableSchema.calledOnce, true, 'calls custom makeExecutableSchema');
});

t.test('component schema uses custom makeExecutableSchema when federation is specified but undefined', async (st) => {
st.plan(2);
const customMakeExecutableSchema = sinon.spy();
const componentA = new GraphQLComponent({
...baseComponentOptions,
makeExecutableSchema: customMakeExecutableSchema,
federation: undefined

const rootComponent = new GraphQLComponent({
imports: [component1, component2],
pruneSchema: true
});

st.doesNotThrow(() => componentA.schema, 'builds schema correctly');
st.equal(customMakeExecutableSchema.calledOnce, true, 'calls custom makeExecutableSchema');

const { schema } = rootComponent;

t.ok(schema.getType('Property'), 'Property type exists');
t.ok(schema.getType('Review'), 'Review type exists');

t.end();
});

t.test('component schema uses custom makeExecutableSchema when federation is specified but null', async (st) => {
st.plan(2);
const customMakeExecutableSchema = sinon.spy();
const componentA = new GraphQLComponent({
...baseComponentOptions,
makeExecutableSchema: customMakeExecutableSchema,
federation: null
});

st.doesNotThrow(() => componentA.schema, 'builds schema correctly');
st.equal(customMakeExecutableSchema.calledOnce, true, 'calls custom makeExecutableSchema');
});

t.test('component schema does not use custom makeExecutableSchema when specified but undefined', async (st) => {
st.plan(1);
const componentA = new GraphQLComponent({
...baseComponentOptions,
makeExecutableSchema: undefined
});
st.doesNotThrow(() => componentA.schema, 'builds schema correctly');
});
t.test('Unused types not removed if prune is false', (t) => {

const component = new GraphQLComponent({
types: `
type Query {
reviews(propertyId: ID!): [Review]
}
type Review {
id: ID!
content: String
}
t.test('component schema does not use custom makeExecutableSchema when specified but null', async (st) => {
st.plan(1);
const componentA = new GraphQLComponent({
...baseComponentOptions,
makeExecutableSchema: null
type Property {
reviews: [Review]
}
`,
resolvers: {
Query: {
reviews() {
return {
id: 'rev-id-1',
content: 'content'
}
}
},
},
pruneSchema: false
});
st.doesNotThrow(() => componentA.schema, 'builds schema correctly');

const { schema } = component;

t.ok(schema.getType('Property'), 'Property type exists');
t.ok(schema.getType('Review'), 'Review type exists');

t.end();
});

t.test('component schema does not use custom makeExecutableSchema when federation is true', async (st) => {
st.plan(2);
const customMakeExecutableSchema = sinon.spy();
const componentA = new GraphQLComponent({
types: [
`
t.test('prune removed unused types', (t) => {

const component = new GraphQLComponent({
types: `
type Query {
property(id: ID!): Property
reviews(propertyId: ID!): [Review]
}
type Property @key(fields: "id") {
type Review {
id: ID!
geo: [String]
content: String
}
extend type Extended @key(fields: "id") {
id: ID!
newProp: String
type Property {
reviews: [Review]
}
`
],
federation: true,
makeExecutableSchema: customMakeExecutableSchema
`,
resolvers: {
Query: {
reviews() {
return {
id: 'rev-id-1',
content: 'content'
}
}
},
},
pruneSchema: true
});
st.doesNotThrow(() => componentA.schema, 'builds schema correctly');
st.equal(customMakeExecutableSchema.calledOnce, false, 'does not call custom makeExecutableSchema');

const { schema } = component;

t.ok(!schema.getType('Property'), 'Property type was pruned');
t.ok(schema.getType('Review'), 'Review type exists');

t.end();
});

});
44 changes: 32 additions & 12 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
import { GraphQLSchema } from 'graphql';
import { IDelegateToSchemaOptions, IExecutableSchemaDefinition } from 'graphql-tools'
import { DocumentNode, GraphQLSchema, Source } from 'graphql';
import { DirectiveUseMap, IDelegateToSchemaOptions, IExecutableSchemaDefinition, IResolvers, IMocks, PruneSchemaOptions } from 'graphql-tools'

interface GraphQLComponentConfigObject {
interface IGraphQLComponentConfigObject {
component: GraphQLComponent;
excludes?: string[];
}

interface GraphQLComponentOptions {
types?: string | string[];
resolvers?: object;
mocks?: boolean | object;
directives?: any;
type ContextFunction = ((ctx: any) => any);

interface IContextMiddleware {
name: string
fn: ContextFunction
}

interface IContextConfig {
namespace: string
factory: ContextFunction
}

interface IContextWrapper extends ContextFunction {
use: (name: string|ContextFunction|null, fn?: ContextFunction|string) => void
}

interface IGraphQLComponentOptions {
types?: (string | Source | DocumentNode)[] | (string | Source | DocumentNode);
resolvers?: IResolvers<any, any>;
mocks?: boolean | MocksConfigFunction;
directives?: DirectiveUseMap;
federation?: boolean;
makeExecutableSchema?: <TContext = any>({
typeDefs,
Expand All @@ -22,14 +38,18 @@ interface GraphQLComponentOptions {
updateResolversInPlace,
schemaExtensions
}) => IExecutableSchemaDefinition<TContext>;
imports?: GraphQLComponent[] | GraphQLComponentConfigObject[];
context?: any;
imports?: GraphQLComponent[] | IGraphQLComponentConfigObject[];
context?: IContextConfig;
dataSources?: any[];
dataSourceOverrides?: any;
pruneSchema?: boolean;
pruneSchemaOptions?: PruneSchemaOptions
}

type MocksConfigFunction = (IMocks) => IMocks;

export default class GraphQLComponent {
constructor(options?: GraphQLComponentOptions);
constructor(options?: IGraphQLComponentOptions);
static delegateToComponent(component: GraphQLComponent, options: IDelegateToSchemaOptions): Promise<any>
readonly name: string;
readonly schema: GraphQLSchema;
Expand All @@ -39,7 +59,7 @@ export default class GraphQLComponent {
};
readonly types: string[];
readonly resolvers: object;
readonly imports: GraphQLComponentConfigObject[];
readonly imports: IGraphQLComponentConfigObject[];
readonly mocks: any;
readonly directives: any;
readonly dataSources: any[];
Expand Down
Loading

0 comments on commit 4354d71

Please sign in to comment.