Skip to content

Commit

Permalink
Allows to add schema definition missing in the original schema (graph…
Browse files Browse the repository at this point in the history
…ql#1441)

Split out from graphql#1438

Allows to inject directives and types into SDL-based schema:

```js
import { GrapQLDateTime } from 'somepackage';
let schema = new GraphQLSchema({
  types: [GrapQLDateTime],
});

const ast = parse(`
  type Query {
    timestamp: DateTime
  }
`);
schema = extendSchema(schema, ast);
```

```js
let schema = new GraphQLSchema({
  directives: [
    new GraphQLDirective({
      name: 'onSchema',
      locations: ['SCHEMA'],
    }),
  ],
});

const ast = parse(`
  schema @onSchema {
    query: Foo
  }
  type Foo {
    bar: String
  }
`);
schema = extendSchema(schema, ast);
```
  • Loading branch information
IvanGoncharov authored Aug 1, 2018
1 parent 732c90f commit a1ee52a
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 36 deletions.
61 changes: 39 additions & 22 deletions src/utilities/__tests__/extendSchema-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,26 @@ const SomeInputType = new GraphQLInputObjectType({
}),
});

const FooDirective = new GraphQLDirective({
name: 'foo',
args: {
input: { type: SomeInputType },
},
locations: [
DirectiveLocation.SCHEMA,
DirectiveLocation.SCALAR,
DirectiveLocation.OBJECT,
DirectiveLocation.FIELD_DEFINITION,
DirectiveLocation.ARGUMENT_DEFINITION,
DirectiveLocation.INTERFACE,
DirectiveLocation.UNION,
DirectiveLocation.ENUM,
DirectiveLocation.ENUM_VALUE,
DirectiveLocation.INPUT_OBJECT,
DirectiveLocation.INPUT_FIELD_DEFINITION,
],
});

const testSchema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
Expand All @@ -112,27 +132,7 @@ const testSchema = new GraphQLSchema({
}),
}),
types: [FooType, BarType],
directives: specifiedDirectives.concat([
new GraphQLDirective({
name: 'foo',
args: {
input: { type: SomeInputType },
},
locations: [
DirectiveLocation.SCHEMA,
DirectiveLocation.SCALAR,
DirectiveLocation.OBJECT,
DirectiveLocation.FIELD_DEFINITION,
DirectiveLocation.ARGUMENT_DEFINITION,
DirectiveLocation.INTERFACE,
DirectiveLocation.UNION,
DirectiveLocation.ENUM,
DirectiveLocation.ENUM_VALUE,
DirectiveLocation.INPUT_OBJECT,
DirectiveLocation.INPUT_FIELD_DEFINITION,
],
}),
]),
directives: specifiedDirectives.concat([FooDirective]),
});

function extendTestSchema(sdl, options) {
Expand Down Expand Up @@ -1271,7 +1271,7 @@ describe('extendSchema', () => {
expect(schema.getMutationType()).to.equal(null);
});

it('does not allow new schema within an extension', () => {
it('does not allow overriding schema within an extension', () => {
const sdl = `
schema {
mutation: Mutation
Expand All @@ -1286,6 +1286,23 @@ describe('extendSchema', () => {
);
});

it('adds schema definition missing in the original schema', () => {
let schema = new GraphQLSchema({
directives: [FooDirective],
types: [FooType],
});
expect(schema.getQueryType()).to.equal(undefined);

const ast = parse(`
schema @foo {
query: Foo
}
`);
schema = extendSchema(schema, ast);
const queryType = schema.getQueryType();
expect(queryType).to.have.property('name', 'Foo');
});

it('adds new root types via schema extension', () => {
const schema = extendTestSchema(`
extend schema {
Expand Down
47 changes: 33 additions & 14 deletions src/utilities/extendSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import type {
DocumentNode,
DirectiveDefinitionNode,
SchemaExtensionNode,
SchemaDefinitionNode,
} from '../language/ast';

type Options = {|
Expand Down Expand Up @@ -106,18 +107,28 @@ export function extendSchema(
// have the same name. For example, a type named "skip".
const directiveDefinitions: Array<DirectiveDefinitionNode> = [];

let schemaDef: ?SchemaDefinitionNode;
// Schema extensions are collected which may add additional operation types.
const schemaExtensions: Array<SchemaExtensionNode> = [];

for (let i = 0; i < documentAST.definitions.length; i++) {
const def = documentAST.definitions[i];
switch (def.kind) {
case Kind.SCHEMA_DEFINITION:
// Sanity check that a schema extension is not defining a new schema
throw new GraphQLError(
'Cannot define a new schema within a schema extension.',
[def],
);
// Sanity check that a schema extension is not overriding the schema
if (
schema.astNode ||
schema.getQueryType() ||
schema.getMutationType() ||
schema.getSubscriptionType()
) {
throw new GraphQLError(
'Cannot define a new schema within a schema extension.',
[def],
);
}
schemaDef = def;
break;
case Kind.SCHEMA_EXTENSION:
schemaExtensions.push(def);
break;
Expand Down Expand Up @@ -184,7 +195,8 @@ export function extendSchema(
Object.keys(typeExtensionsMap).length === 0 &&
Object.keys(typeDefinitionMap).length === 0 &&
directiveDefinitions.length === 0 &&
schemaExtensions.length === 0
schemaExtensions.length === 0 &&
!schemaDef
) {
return schema;
}
Expand Down Expand Up @@ -216,19 +228,28 @@ export function extendSchema(
subscription: extendMaybeNamedType(schema.getSubscriptionType()),
};

// Then, incorporate all schema extensions.
if (schemaDef) {
for (const { operation, type } of schemaDef.operationTypes) {
if (operationTypes[operation]) {
throw new Error(`Must provide only one ${operation} type in schema.`);
}
// Note: While this could make early assertions to get the correctly
// typed values, that would throw immediately while type system
// validation with validateSchema() will produce more actionable results.
operationTypes[operation] = (astBuilder.buildType(type): any);
}
}
// Then, incorporate schema definition and all schema extensions.
for (const schemaExtension of schemaExtensions) {
if (schemaExtension.operationTypes) {
for (const operationType of schemaExtension.operationTypes) {
const operation = operationType.operation;
for (const { operation, type } of schemaExtension.operationTypes) {
if (operationTypes[operation]) {
throw new Error(`Must provide only one ${operation} type in schema.`);
}
const typeRef = operationType.type;
// Note: While this could make early assertions to get the correctly
// typed values, that would throw immediately while type system
// validation with validateSchema() will produce more actionable results.
operationTypes[operation] = (astBuilder.buildType(typeRef): any);
operationTypes[operation] = (astBuilder.buildType(type): any);
}
}
}
Expand All @@ -254,9 +275,7 @@ export function extendSchema(

// Then produce and return a Schema with these types.
return new GraphQLSchema({
query: operationTypes.query,
mutation: operationTypes.mutation,
subscription: operationTypes.subscription,
...operationTypes,
types,
directives: getMergedDirectives(),
astNode: schema.astNode,
Expand Down

0 comments on commit a1ee52a

Please sign in to comment.