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

Commit f9dd920

Browse files
authored
Online schema support (#215)
* Support `.graphqlconfig` without `schemaPath` * Update tests * Update cache key logic - pass directly through to extendschema to avoid duplicate logic - for online schema use endpoint name rather than url (as this may not be unique)
1 parent 123825d commit f9dd920

File tree

6 files changed

+174
-46
lines changed

6 files changed

+174
-46
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"eslint-plugin-dependencies": "2.4.0",
5151
"eslint-plugin-flowtype": "2.40.1",
5252
"eslint-plugin-prefer-object-spread": "1.2.1",
53+
"fetch-mock": "^6.0.0",
5354
"flow-bin": "0.62.0",
5455
"graphql": "^0.12.3",
5556
"graphql-language-service-interface": "^1.0.0-0",

packages/interface/src/GraphQLLanguageService.js

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,6 @@ export class GraphQLLanguageService {
109109
];
110110
}
111111

112-
if (!schemaPath) {
113-
return [];
114-
}
115-
116112
// If there's a matching config, proceed to prepare to run validation
117113
let source = query;
118114
const fragmentDefinitions = await this._graphQLCache.getFragmentDefinitions(
@@ -140,11 +136,6 @@ export class GraphQLLanguageService {
140136
return [];
141137
}
142138

143-
const schema = await this._graphQLCache.getSchema(
144-
projectConfig.projectName,
145-
queryHasExtensions,
146-
);
147-
148139
// Check if there are custom validation rules to be used
149140
let customRules;
150141
const customRulesModulePath =
@@ -158,6 +149,14 @@ export class GraphQLLanguageService {
158149
/* eslint-enable no-implicit-coercion */
159150
}
160151

152+
const schema = await this._graphQLCache
153+
.getSchema(projectConfig.projectName, queryHasExtensions)
154+
.catch(() => null);
155+
156+
if (!schema) {
157+
return [];
158+
}
159+
161160
return validateQuery(validationAst, schema, customRules, isRelayCompatMode);
162161
}
163162

@@ -167,14 +166,12 @@ export class GraphQLLanguageService {
167166
filePath: Uri,
168167
): Promise<Array<CompletionItem>> {
169168
const projectConfig = this._graphQLConfig.getConfigForFile(filePath);
170-
if (projectConfig.schemaPath) {
171-
const schema = await this._graphQLCache.getSchema(
172-
projectConfig.projectName,
173-
);
169+
const schema = await this._graphQLCache
170+
.getSchema(projectConfig.projectName)
171+
.catch(() => null);
174172

175-
if (schema) {
176-
return getAutocompleteSuggestions(schema, query, position);
177-
}
173+
if (schema) {
174+
return getAutocompleteSuggestions(schema, query, position);
178175
}
179176
return [];
180177
}

packages/server/src/GraphQLCache.js

Lines changed: 86 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import {
4242
INPUT_OBJECT_TYPE_EXTENSION,
4343
DIRECTIVE_DEFINITION,
4444
} from 'graphql/language/kinds';
45-
import {getGraphQLConfig, GraphQLConfig} from 'graphql-config';
45+
import {getGraphQLConfig, GraphQLConfig, GraphQLEndpoint} from 'graphql-config';
4646
import {getQueryAndRange} from './MessageProcessor';
4747
import stringToHash from './stringToHash';
4848
import glob from 'glob';
@@ -370,8 +370,8 @@ export class GraphQLCache implements GraphQLCacheInterface {
370370

371371
_extendSchema(
372372
schema: GraphQLSchema,
373-
schemaPath: string,
374-
projectName: string,
373+
schemaPath: ?string,
374+
schemaCacheKey: ?string,
375375
): GraphQLSchema {
376376
const graphQLFileMap = this._graphQLFileListCache.get(this._configDir);
377377
const typeExtensions = [];
@@ -405,22 +405,25 @@ export class GraphQLCache implements GraphQLCacheInterface {
405405
});
406406
});
407407
});
408-
const sorted = typeExtensions.sort((a: any, b: any) => {
409-
const aName = a.definition ? a.definition.name.value : a.name.value;
410-
const bName = b.definition ? b.definition.name.value : b.name.value;
411-
return aName > bName ? 1 : -1;
412-
});
413408

414-
const hash = stringToHash(JSON.stringify(sorted));
415-
const typeExtCacheKey = `${schemaPath}:${projectName}`;
416-
if (
417-
this._typeExtensionMap.has(typeExtCacheKey) &&
418-
this._typeExtensionMap.get(typeExtCacheKey) === hash
419-
) {
420-
return schema;
409+
if (schemaCacheKey) {
410+
const sorted = typeExtensions.sort((a: any, b: any) => {
411+
const aName = a.definition ? a.definition.name.value : a.name.value;
412+
const bName = b.definition ? b.definition.name.value : b.name.value;
413+
return aName > bName ? 1 : -1;
414+
});
415+
const hash = stringToHash(JSON.stringify(sorted));
416+
417+
if (
418+
this._typeExtensionMap.has(schemaCacheKey) &&
419+
this._typeExtensionMap.get(schemaCacheKey) === hash
420+
) {
421+
return schema;
422+
}
423+
424+
this._typeExtensionMap.set(schemaCacheKey, hash);
421425
}
422426

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

436-
if (!projectConfig || !projectConfig.schemaPath) {
439+
if (!projectConfig) {
437440
return null;
438441
}
439442

440443
const projectName = appName || 'undefinedName';
441-
442444
const schemaPath = projectConfig.schemaPath;
443-
const schemaCacheKey = `${schemaPath}:${projectName}`;
445+
const endpointInfo = this._getDefaultEndpoint(projectConfig);
446+
447+
let schemaCacheKey = null;
448+
let schema = null;
444449

445-
if (this._schemaMap.has(schemaCacheKey)) {
446-
const schema = this._schemaMap.get(schemaCacheKey);
447-
return schema && queryHasExtensions
448-
? this._extendSchema(schema, schemaPath, projectName)
449-
: schema;
450+
if (endpointInfo) {
451+
const {endpoint, endpointName} = endpointInfo;
452+
453+
schemaCacheKey = `${endpointName}:${projectName}`;
454+
455+
// Maybe use cache
456+
if (this._schemaMap.has(schemaCacheKey)) {
457+
schema = this._schemaMap.get(schemaCacheKey);
458+
return schema && queryHasExtensions
459+
? this._extendSchema(schema, schemaPath, schemaCacheKey)
460+
: schema;
461+
}
462+
463+
// Read schema from network
464+
try {
465+
schema = await endpoint.resolveSchema();
466+
} catch (failure) {
467+
// Never mind
468+
}
450469
}
451470

452-
let schema = projectConfig.getSchema();
471+
if (!schema && schemaPath) {
472+
schemaCacheKey = `${schemaPath}:${projectName}`;
473+
474+
// Maybe use cache
475+
if (this._schemaMap.has(schemaCacheKey)) {
476+
schema = this._schemaMap.get(schemaCacheKey);
477+
return schema && queryHasExtensions
478+
? this._extendSchema(schema, schemaPath, schemaCacheKey)
479+
: schema;
480+
}
481+
482+
// Read from disk
483+
schema = projectConfig.getSchema();
484+
}
453485

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

464496
if (this._graphQLFileListCache.has(this._configDir)) {
465-
schema = this._extendSchema(schema, schemaPath, projectName);
497+
schema = this._extendSchema(schema, schemaPath, schemaCacheKey);
466498
}
467499

468-
this._schemaMap.set(schemaCacheKey, schema);
500+
if (schemaCacheKey) {
501+
this._schemaMap.set(schemaCacheKey, schema);
502+
}
469503
return schema;
470504
};
471505

506+
_getDefaultEndpoint(
507+
projectConfig: GraphQLProjectConfig,
508+
): ?{endpointName: string, endpoint: GraphQLEndpoint} {
509+
// Jumping through hoops to get the default endpoint by name (needed for cache key)
510+
const endpointsExtension = projectConfig.endpointsExtension;
511+
if (!endpointsExtension) {
512+
return null;
513+
}
514+
515+
const defaultRawEndpoint = endpointsExtension.getRawEndpoint();
516+
const rawEndpointsMap = endpointsExtension.getRawEndpointsMap();
517+
const endpointName = Object.keys(rawEndpointsMap).find(
518+
name => rawEndpointsMap[name] === defaultRawEndpoint,
519+
);
520+
521+
if (!endpointName) {
522+
return null;
523+
}
524+
525+
return {
526+
endpointName,
527+
endpoint: endpointsExtension.getEndpoint(endpointName),
528+
};
529+
}
530+
472531
/**
473532
* Given a list of GraphQL file metadata, read all files collected from watchman
474533
* and create fragmentDefinitions and GraphQL files cache.

packages/server/src/__tests__/.graphqlconfig

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,25 @@
55
"testWithSchema": {
66
"schemaPath": "__schema__/StarWarsSchema.graphql"
77
},
8+
"testWithEndpoint": {
9+
"extensions": {
10+
"endpoints": {
11+
"prod": {
12+
"url": "https://example.com/graphql"
13+
}
14+
}
15+
}
16+
},
17+
"testWithEndpointAndSchema": {
18+
"schemaPath": "__schema__/StarWarsSchema.graphql",
19+
"extensions": {
20+
"endpoints": {
21+
"prod": {
22+
"url": "https://example.com/graphql"
23+
}
24+
}
25+
}
26+
},
827
"testWithoutSchema": {
928
},
1029
"testWithCustomDirectives": {

packages/server/src/__tests__/GraphQLCache-test.js

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import {expect} from 'chai';
1212
import {GraphQLSchema} from 'graphql/type';
1313
import {parse} from 'graphql/language';
1414
import {getGraphQLConfig} from 'graphql-config';
15-
import {beforeEach, describe, it} from 'mocha';
15+
import {beforeEach, afterEach, describe, it} from 'mocha';
16+
import fetchMock from 'fetch-mock';
1617

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

2627
describe('GraphQLCache', () => {
2728
let cache;
29+
let graphQLRC;
2830

2931
beforeEach(async () => {
3032
const configDir = __dirname;
31-
const graphQLRC = getGraphQLConfig(configDir);
33+
graphQLRC = getGraphQLConfig(configDir);
3234
cache = new GraphQLCache(configDir, graphQLRC);
3335
});
3436

37+
afterEach(() => {
38+
fetchMock.restore();
39+
});
40+
3541
describe('getSchema', () => {
3642
it('generates the schema correctly for the test app config', async () => {
3743
const schema = await cache.getSchema('testWithSchema');
3844
expect(schema instanceof GraphQLSchema).to.equal(true);
3945
});
4046

41-
it('does not generate a schema without a schema path', async () => {
47+
it('generates the schema correctly from endpoint', async () => {
48+
const introspectionResult = await graphQLRC
49+
.getProjectConfig('testWithSchema')
50+
.resolveIntrospection();
51+
52+
fetchMock.mock({
53+
matcher: '*',
54+
response: {
55+
headers: {
56+
'Content-Type': 'application/json',
57+
},
58+
body: introspectionResult,
59+
},
60+
});
61+
62+
const schema = await cache.getSchema('testWithEndpoint');
63+
expect(fetchMock.called('*')).to.equal(true);
64+
expect(schema instanceof GraphQLSchema).to.equal(true);
65+
});
66+
67+
it('falls through to schema on disk if endpoint fails', async () => {
68+
fetchMock.mock({
69+
matcher: '*',
70+
response: 500,
71+
});
72+
73+
const schema = await cache.getSchema('testWithEndpointAndSchema');
74+
expect(fetchMock.called('*')).to.equal(true);
75+
expect(schema instanceof GraphQLSchema).to.equal(true);
76+
});
77+
78+
it('does not generate a schema without a schema path or endpoint', async () => {
4279
const schema = await cache.getSchema('testWithoutSchema');
4380
expect(schema instanceof GraphQLSchema).to.equal(false);
4481
});

yarn.lock

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,6 +1413,13 @@ fb-watchman@^2.0.0:
14131413
dependencies:
14141414
bser "^2.0.0"
14151415

1416+
fetch-mock@^6.0.0:
1417+
version "6.0.0"
1418+
resolved "https://registry.yarnpkg.com/fetch-mock/-/fetch-mock-6.0.0.tgz#4edb5acefa8ea90d7eb4213130ab73137fac9df1"
1419+
dependencies:
1420+
glob-to-regexp "^0.3.0"
1421+
path-to-regexp "^2.1.0"
1422+
14161423
figures@^2.0.0:
14171424
version "2.0.0"
14181425
resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
@@ -1635,6 +1642,10 @@ glob-parent@^3.1.0:
16351642
is-glob "^3.1.0"
16361643
path-dirname "^1.0.0"
16371644

1645+
glob-to-regexp@^0.3.0:
1646+
version "0.3.0"
1647+
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
1648+
16381649
[email protected], glob@^7.0.3, glob@^7.0.5, glob@^7.1.2:
16391650
version "7.1.2"
16401651
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
@@ -2610,6 +2621,10 @@ path-parse@^1.0.5:
26102621
version "1.0.5"
26112622
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
26122623

2624+
path-to-regexp@^2.1.0:
2625+
version "2.1.0"
2626+
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.1.0.tgz#7e30f9f5b134bd6a28ffc2e3ef1e47075ac5259b"
2627+
26132628
path-type@^1.0.0:
26142629
version "1.1.0"
26152630
resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"

0 commit comments

Comments
 (0)