Skip to content

Commit

Permalink
Implement proper validation of directives args.
Browse files Browse the repository at this point in the history
  • Loading branch information
IvanGoncharov committed May 30, 2020
1 parent cf57dcd commit fdf3066
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 62 deletions.
32 changes: 10 additions & 22 deletions src/editor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import 'graphiql/graphiql.css';

import * as classNames from 'classnames';
import GraphiQL from 'graphiql';
import { buildASTSchema, extendSchema, GraphQLSchema, parse } from 'graphql';
import { mergeWithFakeDefinitions } from '../fake_definition';
import { Source, GraphQLSchema } from 'graphql';
import { buildWithFakeDefinitions } from '../fake_definition';
import * as React from 'react';
import * as ReactDOM from 'react-dom';

Expand All @@ -25,22 +25,6 @@ type FakeEditorState = {
remoteSDL: string | null;
};

function parseSDL(sdl) {
return parse(sdl, {
allowLegacySDLEmptyFields: true,
allowLegacySDLImplementsInterfaces: true,
});
}

function buildSchema(sdl, extensionSDL?) {
const userSDL = mergeWithFakeDefinitions(parseSDL(sdl));
const schema = buildASTSchema(userSDL, { commentDescriptions: true });
if (extensionSDL) {
return extendSchema(schema, parseSDL(extensionSDL), { commentDescriptions: true });
}
return schema;
}

class FakeEditor extends React.Component<any, FakeEditorState> {
constructor(props) {
super(props);
Expand Down Expand Up @@ -103,11 +87,15 @@ class FakeEditor extends React.Component<any, FakeEditorState> {
});
}

buildSchema(userSDL) {
buildSchema(userSDL, options?) {
if (this.state.remoteSDL) {
return buildSchema(this.state.remoteSDL, userSDL);
return buildWithFakeDefinitions(
new Source(this.state.remoteSDL),
new Source(userSDL),
options,
);
} else {
return buildSchema(userSDL);
return buildWithFakeDefinitions(new Source(userSDL), options);
}
}

Expand Down Expand Up @@ -170,7 +158,7 @@ class FakeEditor extends React.Component<any, FakeEditorState> {
if (this.state.error) this.updateSDL(val);
let dirtySchema = null as GraphQLSchema | null;
try {
dirtySchema = this.buildSchema(val);
dirtySchema = this.buildSchema(val, { skipValidation: true });
} catch (_) {}

this.setState((prevState) => ({
Expand Down
119 changes: 114 additions & 5 deletions src/fake_definition.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
import { Kind, parse, concatAST, DocumentNode } from 'graphql';
import {
Kind,
Source,
DocumentNode,
GraphQLError,
GraphQLSchema,
parse,
validate,
extendSchema,
buildASTSchema,
validateSchema,
isObjectType,
isInterfaceType,
ValuesOfCorrectTypeRule,
} from 'graphql';
import { validateSDL } from 'graphql/validation/validate';

const fakeDefinitionAST = parse(/* GraphQL */ `
enum fake__Locale {
Expand Down Expand Up @@ -217,9 +232,18 @@ const fakeDefinitionsSet = new Set(
fakeDefinitionAST.definitions.map(defToName),
);

export function mergeWithFakeDefinitions(
schemaAST: DocumentNode,
): DocumentNode {
const schemaWithOnlyFakedDefinitions = buildASTSchema(fakeDefinitionAST);
// FIXME: mark it as valid to be able to run `validate`
schemaWithOnlyFakedDefinitions['__validationErrors'] = []

export function buildWithFakeDefinitions(
schemaSDL: Source,
extensionSDL?: Source,
options?: { skipValidation: boolean },
): GraphQLSchema {
const skipValidation = options?.skipValidation ?? false;
const schemaAST = parseSDL(schemaSDL);

// Remove Faker's own definitions that were added to have valid SDL for other
// tools, see: https://github.com/APIs-guru/graphql-faker/issues/75
const filteredAST = {
Expand All @@ -230,5 +254,90 @@ export function mergeWithFakeDefinitions(
}),
};

return concatAST([filteredAST, fakeDefinitionAST]);
let schema = extendSchemaWithAST(schemaWithOnlyFakedDefinitions, filteredAST);

const config = schema.toConfig();
schema = new GraphQLSchema({
...config,
...(config.astNode ? {} : getDefaultRootTypes(schema)),
});

if (extensionSDL != null) {
schema = extendSchemaWithAST(schema, parseSDL(extensionSDL));

// FIXME: put in field extensions
for (const type of Object.values(schema.getTypeMap())) {
if (isObjectType(type) || isInterfaceType(type)) {
for (const field of Object.values(type.getFields())) {
const node = field.astNode;
if (node && node.loc && node.loc.source === extensionSDL) {
(field as any).isExtensionField = true;
}
}
}
}
}

if (!skipValidation) {
const errors = validateSchema(schema);
if (errors.length !== 0) {
throw new ValidationErrors(errors);
}
}

return schema;

function extendSchemaWithAST(
schema: GraphQLSchema,
extensionAST: DocumentNode,
): GraphQLSchema {
if (!skipValidation) {
const errors = [
...validateSDL(extensionAST, schema),
...validate(schemaWithOnlyFakedDefinitions, extensionAST, [ValuesOfCorrectTypeRule]),
];
if (errors.length !== 0) {
throw new ValidationErrors(errors);
}
}

return extendSchema(schema, extensionAST, {
assumeValid: true,
commentDescriptions: true,
});
}
}

// FIXME: move to 'graphql-js'
export class ValidationErrors extends Error {
subErrors: GraphQLError[];

constructor(errors) {
const message = errors.map((error) => error.message).join('\n\n');
super(message);

this.subErrors = errors;
this.name = this.constructor.name;

if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, this.constructor);
} else {
this.stack = (new Error(message)).stack;
}
}
}

function getDefaultRootTypes(schema) {
return {
query: schema.getType('Query'),
mutation: schema.getType('Mutation'),
subscription: schema.getType('Subscription'),
};
}

function parseSDL(sdl: Source) {
return parse(sdl, {
allowLegacySDLEmptyFields: true,
allowLegacySDLImplementsInterfaces: true,
});
}
67 changes: 32 additions & 35 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,19 @@
#!/usr/bin/env node

import {
Source,
GraphQLSchema,
parse,
printSchema,
buildASTSchema,
extendSchema,
isObjectType,
isInterfaceType,
} from 'graphql';

import * as fs from 'fs';
import * as path from 'path';

import * as express from 'express';
import * as graphqlHTTP from 'express-graphql';
import * as chalk from 'chalk';
import * as open from 'open';
import * as cors from 'cors';
import * as bodyParser from 'body-parser';
import { Source, printSchema } from 'graphql';

import { parseCLI } from './cli';
import { getProxyExecuteFn } from './proxy';
import { mergeWithFakeDefinitions } from './fake_definition';
import { ValidationErrors, buildWithFakeDefinitions } from './fake_definition';
import { existsSync, readSDL, getRemoteSchema } from './utils';
import { fakeTypeResolver, fakeFieldResolver } from './fake_schema';

Expand Down Expand Up @@ -100,14 +91,24 @@ function runServer(
};
const app = express();

let schema;
try {
schema = remoteSDL
? buildWithFakeDefinitions(remoteSDL, userSDL)
: buildWithFakeDefinitions(userSDL);
} catch (error) {
if (error instanceof ValidationErrors) {
prettyPrintValidationErrors(error);
process.exit(1);
}
}

app.options('/graphql', cors(corsOptions));
app.use(
'/graphql',
cors(corsOptions),
graphqlHTTP(() => ({
schema: remoteSDL
? buildSchema(remoteSDL, userSDL)
: buildSchema(userSDL),
schema,
typeResolver: fakeTypeResolver,
fieldResolver: fakeFieldResolver,
customExecuteFn,
Expand All @@ -128,6 +129,9 @@ function runServer(
const fileName = userSDL.name;
fs.writeFileSync(fileName, req.body);
userSDL = new Source(req.body, fileName);
schema = remoteSDL
? buildWithFakeDefinitions(remoteSDL, userSDL)
: buildWithFakeDefinitions(userSDL);

const date = new Date().toLocaleString();
log(
Expand Down Expand Up @@ -167,25 +171,18 @@ function runServer(
}
}

function buildSchema(schemaSDL: Source, extendSDL?: Source): GraphQLSchema {
let schemaAST = parse(schemaSDL);
let schema = buildASTSchema(mergeWithFakeDefinitions(schemaAST));

if (extendSDL) {
schema = extendSchema(schema, parse(extendSDL));

// FIXME: put in field extensions
for (const type of Object.values(schema.getTypeMap())) {
if (isObjectType(type) || isInterfaceType(type)) {
for (const field of Object.values(type.getFields())) {
const node = field.astNode;
if (node && node.loc && node.loc.source === extendSDL) {
(field as any).isExtensionField = true;
}
}
}
}
}
function prettyPrintValidationErrors(validationErrors: ValidationErrors) {
const { subErrors } = validationErrors;
log(
chalk.red(
subErrors.length > 1
? `\nYour schema constains ${subErrors.length} validation errors: \n`
: `\nYour schema constains a validation error: \n`,
),
);

return schema;
for (const error of subErrors) {
let [message, ...otherLines] = error.toString().split('\n');
log([chalk.yellow(message), ...otherLines].join('\n') + '\n\n');
}
}

0 comments on commit fdf3066

Please sign in to comment.