Skip to content

Commit 3f2c547

Browse files
authored
Initial support for client-side schemas (#480)
* Initial support for client-side schemas * Update unit tests for API with client schema parameter * Increase timeout for CLI tests * Enable query validation for queries with client-side data * Add unit test for nested local data * Update snapshots * Update test to include mixed client-server data * Support not having a server schema for local state only queries * Have client schema extend server schema * Fix unit tests with old API * Support loading client-side schema from GQL tag
1 parent c00df3d commit 3f2c547

17 files changed

Lines changed: 326 additions & 37 deletions

File tree

packages/apollo-cli/src/commands/codegen/__tests__/__snapshots__/generate.test.ts.snap

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,113 @@ export interface SimpleQuery {
213213
//=============================================================="
214214
`;
215215

216+
exports[`successful codegen infers TypeScript target and writes types for query with client-side data 1`] = `
217+
"
218+
219+
/* tslint:disable */
220+
// This file was automatically generated and should not be edited.
221+
222+
// ====================================================
223+
// GraphQL query operation: SimpleQuery
224+
// ====================================================
225+
226+
export interface SimpleQuery_complexLocalState {
227+
someData: string;
228+
}
229+
230+
export interface SimpleQuery_serverSideField {
231+
serverData: string;
232+
addedLocalData: string;
233+
}
234+
235+
export interface SimpleQuery {
236+
hello: string;
237+
localState: string;
238+
complexLocalState: SimpleQuery_complexLocalState;
239+
serverSideField: SimpleQuery_serverSideField;
240+
}
241+
242+
/* tslint:disable */
243+
// This file was automatically generated and should not be edited.
244+
245+
//==============================================================
246+
// START Enums and Input Objects
247+
//==============================================================
248+
249+
//==============================================================
250+
// END Enums and Input Objects
251+
//=============================================================="
252+
`;
253+
254+
exports[`successful codegen infers TypeScript target and writes types for query with client-side data with schema in a JS file 1`] = `
255+
"
256+
257+
/* tslint:disable */
258+
// This file was automatically generated and should not be edited.
259+
260+
// ====================================================
261+
// GraphQL query operation: SimpleQuery
262+
// ====================================================
263+
264+
export interface SimpleQuery_complexLocalState {
265+
someData: string;
266+
}
267+
268+
export interface SimpleQuery_serverSideField {
269+
serverData: string;
270+
addedLocalData: string;
271+
}
272+
273+
export interface SimpleQuery {
274+
hello: string;
275+
localState: string;
276+
complexLocalState: SimpleQuery_complexLocalState;
277+
serverSideField: SimpleQuery_serverSideField;
278+
}
279+
280+
/* tslint:disable */
281+
// This file was automatically generated and should not be edited.
282+
283+
//==============================================================
284+
// START Enums and Input Objects
285+
//==============================================================
286+
287+
//==============================================================
288+
// END Enums and Input Objects
289+
//=============================================================="
290+
`;
291+
292+
exports[`successful codegen infers TypeScript target and writes types for query with only client-side data 1`] = `
293+
"
294+
295+
/* tslint:disable */
296+
// This file was automatically generated and should not be edited.
297+
298+
// ====================================================
299+
// GraphQL query operation: SimpleQuery
300+
// ====================================================
301+
302+
export interface SimpleQuery_complexLocalState {
303+
someData: string;
304+
}
305+
306+
export interface SimpleQuery {
307+
localState: string;
308+
complexLocalState: SimpleQuery_complexLocalState;
309+
}
310+
311+
/* tslint:disable */
312+
// This file was automatically generated and should not be edited.
313+
314+
//==============================================================
315+
// START Enums and Input Objects
316+
//==============================================================
317+
318+
//==============================================================
319+
// END Enums and Input Objects
320+
//=============================================================="
321+
`;
322+
216323
exports[`successful codegen writes Flow types into a __generated__ directory next to sources when no output is set 1`] = `
217324
"
218325
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
query SimpleQuery {
2+
localState @client
3+
complexLocalState @client {
4+
someData
5+
}
6+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
type Query {
2+
localState: String!
3+
complexLocalState: LocalType!
4+
}
5+
6+
type LocalType {
7+
someData: String!
8+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
extend type Query {
2+
localState: String!
3+
complexLocalState: LocalType!
4+
}
5+
6+
type LocalType {
7+
someData: String!
8+
}
9+
10+
extend type ServerField {
11+
addedLocalData: String!
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
query SimpleQuery {
2+
hello
3+
localState @client
4+
complexLocalState @client {
5+
someData
6+
}
7+
8+
serverSideField {
9+
serverData
10+
addedLocalData @client
11+
}
12+
}

packages/apollo-cli/src/commands/codegen/__tests__/generate.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,39 @@ const otherQuery = fs.readFileSync(
3232
path.resolve(__dirname, "./fixtures/otherQuery.graphql")
3333
);
3434

35+
const clientSideSchema = fs.readFileSync(
36+
path.resolve(__dirname, "./fixtures/clientSideSchema.graphql")
37+
);
38+
39+
const clientSideSchemaTag = `
40+
gql\`
41+
extend type Query {
42+
localState: String!
43+
complexLocalState: LocalType!
44+
}
45+
46+
type LocalType {
47+
someData: String!
48+
}
49+
50+
extend type ServerField {
51+
addedLocalData: String!
52+
}
53+
\`
54+
`;
55+
56+
const clientSideSchemaQuery = fs.readFileSync(
57+
path.resolve(__dirname, "./fixtures/clientSideSchemaQuery.graphql")
58+
);
59+
60+
const clientSideOnlySchema = fs.readFileSync(
61+
path.resolve(__dirname, "./fixtures/clientSideOnlySchema.graphql")
62+
);
63+
64+
const clientSideOnlyQuery = fs.readFileSync(
65+
path.resolve(__dirname, "./fixtures/clientSideOnlyQuery.graphql")
66+
);
67+
3568
beforeEach(() => {
3669
vol.reset();
3770
vol.fromJSON({
@@ -104,6 +137,44 @@ describe("successful codegen", () => {
104137
expect(mockFS.readFileSync("API.ts").toString()).toMatchSnapshot();
105138
});
106139

140+
test
141+
.do(() => {
142+
vol.fromJSON({
143+
"schema.json": JSON.stringify(fullSchema.__schema),
144+
"clientSideSchema.graphql": clientSideSchema.toString(),
145+
"clientSideSchemaQuery.graphql": clientSideSchemaQuery.toString()
146+
});
147+
})
148+
.command(["codegen:generate", "--schema=schema.json", "--clientSchema=clientSideSchema.graphql", "--outputFlat", "API.ts"])
149+
.it("infers TypeScript target and writes types for query with client-side data", () => {
150+
expect(mockFS.readFileSync("API.ts").toString()).toMatchSnapshot();
151+
});
152+
153+
test
154+
.do(() => {
155+
vol.fromJSON({
156+
"schema.json": JSON.stringify(fullSchema.__schema),
157+
"clientSideSchemaTag.js": clientSideSchemaTag.toString(),
158+
"clientSideSchemaQuery.graphql": clientSideSchemaQuery.toString()
159+
});
160+
})
161+
.command(["codegen:generate", "--schema=schema.json", "--clientSchema=clientSideSchemaTag.js", "--outputFlat", "API.ts"])
162+
.it("infers TypeScript target and writes types for query with client-side data with schema in a JS file", () => {
163+
expect(mockFS.readFileSync("API.ts").toString()).toMatchSnapshot();
164+
});
165+
166+
test
167+
.do(() => {
168+
vol.fromJSON({
169+
"clientSideOnlySchema.graphql": clientSideOnlySchema.toString(),
170+
"clientSideOnlyQuery.graphql": clientSideOnlyQuery.toString()
171+
});
172+
})
173+
.command(["codegen:generate", "--clientSchema=clientSideOnlySchema.graphql", "--outputFlat", "API.ts"])
174+
.it("infers TypeScript target and writes types for query with only client-side data", () => {
175+
expect(mockFS.readFileSync("API.ts").toString()).toMatchSnapshot();
176+
});
177+
107178
test
108179
.do(() => {
109180
vol.fromJSON({

packages/apollo-cli/src/commands/codegen/generate.ts

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as path from "path";
55

66
import { TargetType, default as generate } from "../../generate";
77

8-
import { buildClientSchema } from "graphql";
8+
import { buildClientSchema, buildSchema, parse, visit, GraphQLCompositeType, extendSchema, buildASTSchema } from "graphql";
99

1010
import * as fg from "glob";
1111
import { fs, withGlobalFS } from "apollo-codegen-core/lib/localfs";
@@ -14,6 +14,8 @@ import { promisify } from "util";
1414
import { loadSchemaStep } from "../../load-schema";
1515

1616
import { engineFlags } from "../../engine-cli";
17+
import { fromFile } from '../../fetch-schema';
18+
import { loadQueryDocuments } from 'apollo-codegen-core/lib/loading';
1719

1820
export default class Generate extends Command {
1921
static description =
@@ -32,6 +34,9 @@ export default class Generate extends Command {
3234
schema: flags.string({
3335
description: "Path to your GraphQL schema introspection result"
3436
}),
37+
clientSchema: flags.string({
38+
description: "Path to your client-side GraphQL schema file for `apollo-link-state` (either .graphql or .json)"
39+
}),
3540

3641
...engineFlags,
3742

@@ -177,7 +182,7 @@ export default class Generate extends Command {
177182
);
178183
});
179184
task.title = `Scanning for GraphQL queries (${paths.length} found)`;
180-
ctx.queryPaths = paths;
185+
ctx.queryPaths = paths.filter(p => path.resolve(p) != (flags.clientSchema ? path.resolve(flags.clientSchema) : undefined));
181186
}
182187
},
183188
loadSchemaStep(
@@ -187,21 +192,62 @@ export default class Generate extends Command {
187192
flags.engine,
188193
"Loading GraphQL schema",
189194
async ctx => {
190-
const schemaFileContent = await promisify(fs.readFile)(
191-
path.resolve(flags.schema as string)
192-
);
193-
const schemaData = JSON.parse((schemaFileContent as any) as string);
194-
ctx.schema = schemaData.data
195-
? schemaData.data.__schema
196-
: schemaData.__schema
197-
? schemaData.__schema
198-
: schemaData;
195+
if (flags.schema) {
196+
const schemaFileContent = await promisify(fs.readFile)(
197+
path.resolve(flags.schema as string)
198+
);
199+
const schemaData = JSON.parse((schemaFileContent as any) as string);
200+
ctx.schema = schemaData.data
201+
? schemaData.data.__schema
202+
: schemaData.__schema
203+
? schemaData.__schema
204+
: schemaData;
205+
} else {
206+
this.log("Not loading because no path was provided (you should have a client-side schema)");
207+
}
199208
}
200209
),
201210
{
202211
title: "Parsing GraphQL schema",
203-
task: async ctx => {
204-
ctx.schema = buildClientSchema({ __schema: ctx.schema });
212+
task: async (ctx, task) => {
213+
if (ctx.schema) {
214+
ctx.schema = buildClientSchema({ __schema: ctx.schema });
215+
} else {
216+
task.skip("No server-side schema provided")
217+
}
218+
}
219+
},
220+
{
221+
title: "Loading client-side GraphQL schema",
222+
task: async (ctx, task) => {
223+
if (!flags.clientSchema) {
224+
task.skip("Path to client schema not provided")
225+
} else {
226+
const foundDocuments = loadQueryDocuments([path.resolve(flags.clientSchema)]);
227+
if (foundDocuments.length == 0) {
228+
this.error("Found no query documents, aborting");
229+
}
230+
231+
if (foundDocuments.length > 1) {
232+
this.warn("Found more than one query document, using the first one");
233+
}
234+
235+
const ast = foundDocuments[0];
236+
const clientNodes: {parentType: string}[] = [];
237+
visit(ast, {
238+
enter(node, key, parent, path, ancestors) {
239+
if (node.kind == "FieldDefinition") {
240+
(node as any).__client = true;
241+
}
242+
}
243+
});
244+
245+
if (ctx.schema) {
246+
ctx.schema = extendSchema(ctx.schema, ast);
247+
} else {
248+
ctx.schema = buildASTSchema(ast);
249+
}
250+
}
205251
}
206252
},
207253
{

packages/apollo-cli/src/commands/schema/__tests__/__snapshots__/download.test.ts.snap

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

packages/apollo-cli/src/commands/schema/__tests__/check.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ const engineSuccess = ({ schema, tag, results } = {}) => nock => {
8282
});
8383
};
8484

85-
jest.setTimeout(15000);
85+
jest.setTimeout(25000);
8686

8787
describe("successful checks", () => {
8888
test

packages/apollo-cli/src/commands/schema/__tests__/download.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ beforeEach(() => {
4141
});
4242
})
4343

44-
jest.setTimeout(15000);
44+
jest.setTimeout(25000);
4545

4646
describe("successful schema downloading", () => {
4747
test

0 commit comments

Comments
 (0)