Skip to content
This repository was archived by the owner on Sep 2, 2020. It is now read-only.

Online schema support #215

Merged
merged 6 commits into from
Apr 6, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"eslint-plugin-dependencies": "2.4.0",
"eslint-plugin-flowtype": "2.40.1",
"eslint-plugin-prefer-object-spread": "1.2.1",
"fetch-mock": "^6.0.0",
"flow-bin": "0.62.0",
"graphql": "^0.12.3",
"graphql-language-service-interface": "^1.0.0-0",
Expand Down
29 changes: 13 additions & 16 deletions packages/interface/src/GraphQLLanguageService.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,6 @@ export class GraphQLLanguageService {
];
}

if (!schemaPath) {
return [];
}

// If there's a matching config, proceed to prepare to run validation
let source = query;
const fragmentDefinitions = await this._graphQLCache.getFragmentDefinitions(
Expand Down Expand Up @@ -138,11 +134,6 @@ export class GraphQLLanguageService {
return [];
}

const schema = await this._graphQLCache.getSchema(
projectConfig.projectName,
queryHasExtensions,
);

// Check if there are custom validation rules to be used
let customRules;
const customRulesModulePath =
Expand All @@ -156,6 +147,14 @@ export class GraphQLLanguageService {
/* eslint-enable no-implicit-coercion */
}

const schema = await this._graphQLCache
.getSchema(projectConfig.projectName, queryHasExtensions)
.catch(() => null);

if (!schema) {
return [];
}

return validateQuery(validationAst, schema, customRules, isRelayCompatMode);
}

Expand All @@ -165,14 +164,12 @@ export class GraphQLLanguageService {
filePath: Uri,
): Promise<Array<CompletionItem>> {
const projectConfig = this._graphQLConfig.getConfigForFile(filePath);
if (projectConfig.schemaPath) {
const schema = await this._graphQLCache.getSchema(
projectConfig.projectName,
);
const schema = await this._graphQLCache
.getSchema(projectConfig.projectName)
.catch(() => null);

if (schema) {
return getAutocompleteSuggestions(schema, query, position);
}
if (schema) {
return getAutocompleteSuggestions(schema, query, position);
}
return [];
}
Expand Down
113 changes: 86 additions & 27 deletions packages/server/src/GraphQLCache.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import {
INPUT_OBJECT_TYPE_EXTENSION,
DIRECTIVE_DEFINITION,
} from 'graphql/language/kinds';
import {getGraphQLConfig, GraphQLConfig} from 'graphql-config';
import {getGraphQLConfig, GraphQLConfig, GraphQLEndpoint} from 'graphql-config';
import {getQueryAndRange} from './MessageProcessor';
import stringToHash from './stringToHash';
import glob from 'glob';
Expand Down Expand Up @@ -370,8 +370,8 @@ export class GraphQLCache implements GraphQLCacheInterface {

_extendSchema(
schema: GraphQLSchema,
schemaPath: string,
projectName: string,
schemaPath: ?string,
schemaCacheKey: ?string,
): GraphQLSchema {
const graphQLFileMap = this._graphQLFileListCache.get(this._configDir);
const typeExtensions = [];
Expand Down Expand Up @@ -405,22 +405,25 @@ export class GraphQLCache implements GraphQLCacheInterface {
});
});
});
const sorted = typeExtensions.sort((a: any, b: any) => {
const aName = a.definition ? a.definition.name.value : a.name.value;
const bName = b.definition ? b.definition.name.value : b.name.value;
return aName > bName ? 1 : -1;
});

const hash = stringToHash(JSON.stringify(sorted));
const typeExtCacheKey = `${schemaPath}:${projectName}`;
if (
this._typeExtensionMap.has(typeExtCacheKey) &&
this._typeExtensionMap.get(typeExtCacheKey) === hash
) {
return schema;
if (schemaCacheKey) {
const sorted = typeExtensions.sort((a: any, b: any) => {
const aName = a.definition ? a.definition.name.value : a.name.value;
const bName = b.definition ? b.definition.name.value : b.name.value;
return aName > bName ? 1 : -1;
});
const hash = stringToHash(JSON.stringify(sorted));

if (
this._typeExtensionMap.has(schemaCacheKey) &&
this._typeExtensionMap.get(schemaCacheKey) === hash
) {
return schema;
}

this._typeExtensionMap.set(schemaCacheKey, hash);
}

this._typeExtensionMap.set(typeExtCacheKey, hash);
return extendSchema(schema, {
kind: DOCUMENT,
definitions: typeExtensions,
Expand All @@ -433,23 +436,52 @@ export class GraphQLCache implements GraphQLCacheInterface {
): Promise<?GraphQLSchema> => {
const projectConfig = this._graphQLConfig.getProjectConfig(appName);

if (!projectConfig || !projectConfig.schemaPath) {
if (!projectConfig) {
return null;
}

const projectName = appName || 'undefinedName';

const schemaPath = projectConfig.schemaPath;
const schemaCacheKey = `${schemaPath}:${projectName}`;
const endpointInfo = this._getDefaultEndpoint(projectConfig);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

const {endpointName, endpoint} = this._getDefaultEndpoint(projectConfig);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, it looks like _getDefaultEndpoint can return something that is falsy so destructuring here could possibly throw an error which is why I think it's handled below.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup


let schemaCacheKey = null;
let schema = null;

if (this._schemaMap.has(schemaCacheKey)) {
const schema = this._schemaMap.get(schemaCacheKey);
return schema && queryHasExtensions
? this._extendSchema(schema, schemaPath, projectName)
: schema;
if (endpointInfo) {
const {endpoint, endpointName} = endpointInfo;

schemaCacheKey = `${endpointName}:${projectName}`;

// Maybe use cache
if (this._schemaMap.has(schemaCacheKey)) {
schema = this._schemaMap.get(schemaCacheKey);
return schema && queryHasExtensions
? this._extendSchema(schema, schemaPath, schemaCacheKey)
: schema;
}

// Read schema from network
try {
schema = await endpoint.resolveSchema();
} catch (failure) {
// Never mind
}
}

let schema = projectConfig.getSchema();
if (!schema && schemaPath) {
schemaCacheKey = `${schemaPath}:${projectName}`;

// Maybe use cache
if (this._schemaMap.has(schemaCacheKey)) {
schema = this._schemaMap.get(schemaCacheKey);
return schema && queryHasExtensions
? this._extendSchema(schema, schemaPath, schemaCacheKey)
: schema;
}

// Read from disk
schema = projectConfig.getSchema();
}

const customDirectives = projectConfig.extensions.customDirectives;
if (customDirectives && schema) {
Expand All @@ -462,13 +494,40 @@ export class GraphQLCache implements GraphQLCacheInterface {
}

if (this._graphQLFileListCache.has(this._configDir)) {
schema = this._extendSchema(schema, schemaPath, projectName);
schema = this._extendSchema(schema, schemaPath, schemaCacheKey);
}

this._schemaMap.set(schemaCacheKey, schema);
if (schemaCacheKey) {
this._schemaMap.set(schemaCacheKey, schema);
}
return schema;
};

_getDefaultEndpoint(
projectConfig: GraphQLProjectConfig,
): ?{endpointName: string, endpoint: GraphQLEndpoint} {
// Jumping through hoops to get the default endpoint by name (needed for cache key)
const endpointsExtension = projectConfig.endpointsExtension;
if (!endpointsExtension) {
return null;
}

const defaultRawEndpoint = endpointsExtension.getRawEndpoint();
const rawEndpointsMap = endpointsExtension.getRawEndpointsMap();
const endpointName = Object.keys(rawEndpointsMap).find(
name => rawEndpointsMap[name] === defaultRawEndpoint,
);

if (!endpointName) {
return null;
}

return {
endpointName,
endpoint: endpointsExtension.getEndpoint(endpointName),
};
}

/**
* Given a list of GraphQL file metadata, read all files collected from watchman
* and create fragmentDefinitions and GraphQL files cache.
Expand Down
19 changes: 19 additions & 0 deletions packages/server/src/__tests__/.graphqlconfig
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@
"testWithSchema": {
"schemaPath": "__schema__/StarWarsSchema.graphql"
},
"testWithEndpoint": {
"extensions": {
"endpoints": {
"prod": {
"url": "https://example.com/graphql"
}
}
}
},
"testWithEndpointAndSchema": {
"schemaPath": "__schema__/StarWarsSchema.graphql",
"extensions": {
"endpoints": {
"prod": {
"url": "https://example.com/graphql"
}
}
}
},
"testWithoutSchema": {
},
"testWithCustomDirectives": {
Expand Down
43 changes: 40 additions & 3 deletions packages/server/src/__tests__/GraphQLCache-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {expect} from 'chai';
import {GraphQLSchema} from 'graphql/type';
import {parse} from 'graphql/language';
import {getGraphQLConfig} from 'graphql-config';
import {beforeEach, describe, it} from 'mocha';
import {beforeEach, afterEach, describe, it} from 'mocha';
import fetchMock from 'fetch-mock';

import {GraphQLCache} from '../GraphQLCache';
import {getQueryAndRange} from '../MessageProcessor';
Expand All @@ -25,20 +26,56 @@ function wihtoutASTNode(definition: object) {

describe('GraphQLCache', () => {
let cache;
let graphQLRC;

beforeEach(async () => {
const configDir = __dirname;
const graphQLRC = getGraphQLConfig(configDir);
graphQLRC = getGraphQLConfig(configDir);
cache = new GraphQLCache(configDir, graphQLRC);
});

afterEach(() => {
fetchMock.restore();
});

describe('getSchema', () => {
it('generates the schema correctly for the test app config', async () => {
const schema = await cache.getSchema('testWithSchema');
expect(schema instanceof GraphQLSchema).to.equal(true);
});

it('does not generate a schema without a schema path', async () => {
it('generates the schema correctly from endpoint', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good stuff here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:)

const introspectionResult = await graphQLRC
.getProjectConfig('testWithSchema')
.resolveIntrospection();

fetchMock.mock({
matcher: '*',
response: {
headers: {
'Content-Type': 'application/json',
},
body: introspectionResult,
},
});

const schema = await cache.getSchema('testWithEndpoint');
expect(fetchMock.called('*')).to.equal(true);
expect(schema instanceof GraphQLSchema).to.equal(true);
});

it('falls through to schema on disk if endpoint fails', async () => {
fetchMock.mock({
matcher: '*',
response: 500,
});

const schema = await cache.getSchema('testWithEndpointAndSchema');
expect(fetchMock.called('*')).to.equal(true);
expect(schema instanceof GraphQLSchema).to.equal(true);
});

it('does not generate a schema without a schema path or endpoint', async () => {
const schema = await cache.getSchema('testWithoutSchema');
expect(schema instanceof GraphQLSchema).to.equal(false);
});
Expand Down
25 changes: 25 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1413,6 +1413,13 @@ fb-watchman@^2.0.0:
dependencies:
bser "^2.0.0"

fetch-mock@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/fetch-mock/-/fetch-mock-6.0.0.tgz#4edb5acefa8ea90d7eb4213130ab73137fac9df1"
dependencies:
glob-to-regexp "^0.3.0"
path-to-regexp "^2.1.0"

figures@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
Expand Down Expand Up @@ -1635,6 +1642,10 @@ glob-parent@^3.1.0:
is-glob "^3.1.0"
path-dirname "^1.0.0"

glob-to-regexp@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"

[email protected], glob@^7.0.3, glob@^7.0.5, glob@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
Expand Down Expand Up @@ -1703,6 +1714,16 @@ graphql-import@^0.1.7:
graphql "^0.12.3"
lodash "^4.17.4"

graphql-language-service-parser@^0.1.0-0:
version "0.1.14"
resolved "https://registry.yarnpkg.com/graphql-language-service-parser/-/graphql-language-service-parser-0.1.14.tgz#dd25abda5dcff4f2268c9a19e026004271491661"
dependencies:
graphql-language-service-types "^0.1.14"

graphql-language-service-types@^0.1.0-0, graphql-language-service-types@^0.1.14:
version "0.1.14"
resolved "https://registry.yarnpkg.com/graphql-language-service-types/-/graphql-language-service-types-0.1.14.tgz#e6112785fc23ea8222f59a7f00e61b359f263c88"

graphql-request@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-1.4.0.tgz#f5b067c83070296d93fb45760e83dfad0d9f537a"
Expand Down Expand Up @@ -2600,6 +2621,10 @@ path-parse@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"

path-to-regexp@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.1.0.tgz#7e30f9f5b134bd6a28ffc2e3ef1e47075ac5259b"

path-type@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
Expand Down