Skip to content

Commit dd662a5

Browse files
authored
Merge pull request #1158 from apollographql/abernix/op-reg-backport
Update operation normalization to deterministically sort fragments.
2 parents 1938030 + ea54115 commit dd662a5

12 files changed

Lines changed: 3280 additions & 672 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## Upcoming
44

5+
## `apollo`
6+
7+
- `apollo`
8+
- Update operation normalization technique to deterministically order fragments within operations. This update affects those users of the [operation registry](https://www.apollographql.com/docs/platform/operation-registry.html) feature of the Apollo Platform. Anyone using the operation registry should re-register their operations with this new version of the `apollo` CLI via the `apollo client:push` command. Once all client operations are re-registered, the `apollo-server-plugin-operation-manifest` plugin within Apollo Server (which reads the manifest published with `apollo client:push`) should be updated to `0.1.0-alpha.1`. [#1158](https://github.com/apollographql/apollo-tooling/pull/1158)
9+
510
## `apollo-language-server`
611

712
- apollo-language-server

package-lock.json

Lines changed: 100 additions & 534 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/apollo-language-server/src/engine/operations/registerOperations.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ export const REGISTER_OPERATIONS = gql`
55
$id: ID!
66
$clientIdentity: RegisteredClientIdentityInput!
77
$operations: [RegisteredOperationInput!]!
8+
$manifestVersion: Int!
89
) {
910
service(id: $id) {
1011
registerOperations(
1112
clientIdentity: $clientIdentity
1213
operations: $operations
14+
manifestVersion: $manifestVersion
1315
)
1416
}
1517
}

packages/apollo-language-server/src/graphqlTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export interface RegisterOperationsVariables {
114114
id: string;
115115
clientIdentity: RegisteredClientIdentityInput;
116116
operations: RegisteredOperationInput[];
117+
manifestVersion: number;
117118
}
118119

119120
/* tslint:disable */

packages/apollo/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@
4545
"apollo-codegen-scala": "file:../apollo-codegen-scala",
4646
"apollo-codegen-swift": "file:../apollo-codegen-swift",
4747
"apollo-codegen-typescript": "file:../apollo-codegen-typescript",
48-
"apollo-engine-reporting": "0.2.2",
4948
"apollo-env": "file:../apollo-env",
49+
"apollo-graphql": "file:../apollo-graphql",
5050
"apollo-language-server": "file:../apollo-language-server",
5151
"chalk": "2.4.2",
5252
"cli-ux": "4.9.3",

packages/apollo/src/commands/client/extract.ts

Lines changed: 29 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,10 @@
1-
import { createHash } from "crypto";
21
import { writeFileSync } from "fs";
3-
import {
4-
printWithReducedWhitespace,
5-
sortAST,
6-
defaultSignature as engineDefaultSignature
7-
} from "apollo-engine-reporting";
8-
import { DocumentNode } from "graphql";
9-
102
import { ClientCommand } from "../../Command";
11-
import { hideCertainLiterals } from "./push";
12-
13-
// XXX this is duplicated code
14-
const manifestOperationHash = (str: string): string =>
15-
createHash("sha256")
16-
.update(str)
17-
.digest("hex");
18-
19-
const engineSignature = (_TODO_operationAST: DocumentNode): string => {
20-
// TODO. We don't currently have access to the operation name since it's
21-
// currently omitted by the `apollo-codegen-core` package logic.
22-
return engineDefaultSignature(_TODO_operationAST, "TODO");
23-
};
3+
import {
4+
getOperationManifestFromProject,
5+
ManifestEntry
6+
} from "../../utils/getOperationManifestFromProject";
7+
import { ClientIdentity } from "apollo-language-server";
248

259
export default class ClientExtract extends ClientCommand {
2610
static description = "Extract queries from a client";
@@ -38,52 +22,31 @@ export default class ClientExtract extends ClientCommand {
3822
];
3923

4024
async run() {
41-
const { clientIdentity, operations, filename }: any = await this.runTasks(
42-
({ flags, project, config, args }) => [
43-
{
44-
title: "Extracting operations from project",
45-
task: async ctx => {
46-
const operations = Object.values(
47-
this.project.mergedOperationsAndFragmentsForService
48-
).map(operationAST => {
49-
// While this could include dropping unused definitions, they are
50-
// kept because the registered operations should mirror those in the
51-
// client bundle minus any PII which lives within string literals.
52-
const printed = printWithReducedWhitespace(
53-
sortAST(hideCertainLiterals(operationAST))
54-
);
55-
56-
return {
57-
signature: manifestOperationHash(printed),
58-
document: printed,
59-
metadata: {
60-
engineSignature: engineSignature(operationAST)
61-
}
62-
};
63-
});
64-
65-
ctx.operations = operations;
66-
ctx.clientIdentity = config.client;
67-
}
68-
},
69-
{
70-
title: "Outputing extracted queries",
71-
task: (ctx, task) => {
72-
const filename = args.output;
73-
task.title = "Outputing extracted queries to " + filename;
74-
ctx.filename = filename;
75-
writeFileSync(
76-
filename,
77-
JSON.stringify(
78-
{ version: 1, operations: ctx.operations },
79-
null,
80-
2
81-
)
82-
);
83-
}
25+
const { clientIdentity, operations, filename } = await this.runTasks<{
26+
clientIdentity: ClientIdentity;
27+
operations: ManifestEntry[];
28+
filename: string;
29+
}>(({ flags, project, config, args }) => [
30+
{
31+
title: "Extracting operations from project",
32+
task: async ctx => {
33+
ctx.operations = getOperationManifestFromProject(this.project);
34+
ctx.clientIdentity = config.client;
8435
}
85-
]
86-
);
36+
},
37+
{
38+
title: "Outputing extracted queries",
39+
task: (ctx, task) => {
40+
const filename = args.output;
41+
task.title = "Outputing extracted queries to " + filename;
42+
ctx.filename = filename;
43+
writeFileSync(
44+
filename,
45+
JSON.stringify({ version: 2, operations: ctx.operations }, null, 2)
46+
);
47+
}
48+
}
49+
]);
8750

8851
this.log(
8952
`Successfully wrote ${operations.length} operations from the ${

packages/apollo/src/commands/client/push.ts

Lines changed: 16 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,9 @@
1-
import { createHash } from "crypto";
2-
import {
3-
printWithReducedWhitespace,
4-
sortAST,
5-
defaultSignature as engineDefaultSignature
6-
} from "apollo-engine-reporting";
7-
8-
import {
9-
visit,
10-
DocumentNode,
11-
IntValueNode,
12-
FloatValueNode,
13-
StringValueNode
14-
} from "graphql";
15-
161
import { ClientCommand } from "../../Command";
17-
18-
const manifestOperationHash = (str: string): string =>
19-
createHash("sha256")
20-
.update(str)
21-
.digest("hex");
22-
23-
const engineSignature = (_TODO_operationAST: DocumentNode): string => {
24-
// TODO. We don't currently have access to the operation name since it's
25-
// currently omitted by the `apollo-codegen-core` package logic.
26-
return engineDefaultSignature(_TODO_operationAST, "TODO");
27-
};
28-
29-
// In the same spirit as the similarly named `hideLiterals` function from the
30-
// `apollo-engine-reporting/src/signature.ts` module, we'll do an AST visit
31-
// to redact literals. Developers are strongly encouraged to use the
32-
// `variables` aspect of the which would avoid these being explicitly
33-
// present in the operation manifest at all. The primary area of concern here
34-
// is to avoid sending in-lined literals which might contain sensitive
35-
// information (e.g. API keys, etc.).
36-
export function hideCertainLiterals(ast: DocumentNode): DocumentNode {
37-
return visit(ast, {
38-
IntValue(node: IntValueNode): IntValueNode {
39-
return { ...node, value: "0" };
40-
},
41-
FloatValue(node: FloatValueNode): FloatValueNode {
42-
return { ...node, value: "0" };
43-
},
44-
StringValue(node: StringValueNode): StringValueNode {
45-
return { ...node, value: "", block: false };
46-
}
47-
});
48-
}
2+
import {
3+
getOperationManifestFromProject,
4+
ManifestEntry
5+
} from "../../utils/getOperationManifestFromProject";
6+
import { ClientIdentity } from "apollo-language-server";
497

508
export default class ServicePush extends ClientCommand {
519
static description = "Push a service to Engine";
@@ -54,35 +12,21 @@ export default class ServicePush extends ClientCommand {
5412
};
5513

5614
async run() {
57-
const {
58-
clientIdentity,
59-
operations,
60-
serviceName
61-
}: any = await this.runTasks(({ flags, project, config }) => [
15+
const { clientIdentity, operations, serviceName } = await this.runTasks<{
16+
clientIdentity: ClientIdentity;
17+
operations: ManifestEntry[];
18+
serviceName: string;
19+
}>(({ flags, project, config }) => [
6220
{
6321
title: "Pushing client information to Engine",
6422
task: async ctx => {
6523
if (!config.name) {
6624
throw new Error("No service found to link to Engine");
6725
}
68-
const operations = Object.values(
69-
this.project.mergedOperationsAndFragmentsForService
70-
).map(operationAST => {
71-
// While this could include dropping unused definitions, they are
72-
// kept because the registered operations should mirror those in the
73-
// client bundle minus any PII which lives within string literals.
74-
const printed = printWithReducedWhitespace(
75-
sortAST(hideCertainLiterals(operationAST))
76-
);
7726

78-
return {
79-
signature: manifestOperationHash(printed),
80-
document: printed,
81-
metadata: {
82-
engineSignature: engineSignature(operationAST)
83-
}
84-
};
85-
});
27+
const operationManifest = getOperationManifestFromProject(
28+
this.project
29+
);
8630

8731
const { name, referenceID, version } = config.client!;
8832
if (!name) {
@@ -96,13 +40,14 @@ export default class ServicePush extends ClientCommand {
9640
version
9741
},
9842
id: config.name,
99-
operations
43+
operations: operationManifest,
44+
manifestVersion: 2
10045
};
10146

10247
await project.engine.registerOperations(variables);
10348

10449
// store data for logging
105-
ctx.operations = operations;
50+
ctx.operations = operationManifest;
10651
ctx.serviceName = variables.id;
10752
ctx.clientIdentity = variables.clientIdentity;
10853
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`getOperationManifestFromProject builds an operation manifest 1`] = `
4+
Array [
5+
Object {
6+
"document": "query SchemaTagsAndFieldStats($id:ID!){service(id:$id){__typename schemaTags{__typename tag}stats(from:\\"\\",to:\\"\\"){__typename fieldStats{__typename groupBy{__typename field}metrics{__typename fieldHistogram{__typename durationMs(percentile:0)}}}}}}",
7+
"metadata": Object {
8+
"engineSignature": "",
9+
},
10+
"signature": "135e8314de0c2f23f4a2be87b20e59f30ecf2cbcdfad6676a46aad21d29cdb5b",
11+
},
12+
Object {
13+
"document": "mutation CheckSchema($frontend:String,$gitContext:GitContextInput,$historicParameters:HistoricQueryParameters,$id:ID!,$schema:IntrospectionSchemaInput!,$tag:String){service(id:$id){__typename checkSchema(baseSchemaTag:$tag,frontend:$frontend,gitContext:$gitContext,historicParameters:$historicParameters,proposedSchema:$schema){__typename diffToPrevious{__typename changes{__typename code description type}type validationConfig{__typename from queryCountThreshold queryCountThresholdPercentage to}}targetUrl}}}",
14+
"metadata": Object {
15+
"engineSignature": "",
16+
},
17+
"signature": "3fd18a0f0369d801024bb42268bc4d4d1f6edd84a412923c4678c026bc1bdc62",
18+
},
19+
Object {
20+
"document": "mutation RegisterOperations($clientIdentity:RegisteredClientIdentityInput!,$id:ID!,$operations:[RegisteredOperationInput!]!){service(id:$id){__typename registerOperations(clientIdentity:$clientIdentity,operations:$operations)}}",
21+
"metadata": Object {
22+
"engineSignature": "",
23+
},
24+
"signature": "1428ab44b0f2f4ea25bc28c08b5a7235cb96a2d535d1a15e0edd5f0695a4903f",
25+
},
26+
Object {
27+
"document": "mutation UploadSchema($gitContext:GitContextInput,$id:ID!,$schema:IntrospectionSchemaInput!,$tag:String!){service(id:$id){__typename uploadSchema(gitContext:$gitContext,schema:$schema,tag:$tag){__typename code message success tag{__typename schema{__typename hash}tag}}}}",
28+
"metadata": Object {
29+
"engineSignature": "",
30+
},
31+
"signature": "a2497bb8c32f97870dfc58823b1377f6339c3e5ba53010b2a6b1e5d8fa8b8634",
32+
},
33+
Object {
34+
"document": "mutation ValidateOperations($gitContext:GitContextInput,$id:ID!,$operations:[OperationDocumentInput!]!,$tag:String){service(id:$id){__typename validateOperations(gitContext:$gitContext,operations:$operations,tag:$tag){__typename validationResults{__typename code description operation{__typename name}type}}}}",
35+
"metadata": Object {
36+
"engineSignature": "",
37+
},
38+
"signature": "600e261efe1f25ee30c53edd0b2b83c7eb6691ef5ae1c8f460ace2d303ad053d",
39+
},
40+
Object {
41+
"document": "fragment IntrospectionFullType on IntrospectionType{__typename description enumValues(includeDeprecated:true){__typename depreactionReason description isDeprecated name}fields{__typename args{__typename...IntrospectionInputValue}deprecationReason description isDeprecated name type{__typename...IntrospectionTypeRef}}inputFields{__typename...IntrospectionInputValue}interfaces{__typename...IntrospectionTypeRef}kind name possibleTypes{__typename...IntrospectionTypeRef}}fragment IntrospectionInputValue on IntrospectionInputValue{__typename defaultValue description name type{__typename...IntrospectionTypeRef}}fragment IntrospectionTypeRef on IntrospectionType{__typename kind name ofType{__typename kind name ofType{__typename kind name ofType{__typename kind name ofType{__typename kind name ofType{__typename kind name ofType{__typename kind name ofType{__typename kind name}}}}}}}}query GetSchemaByTag($tag:String!){service:me{__typename...on Service{schema(tag:$tag){__typename hash __schema:introspection{__typename directives{__typename args{__typename...IntrospectionInputValue}description locations name}mutationType{__typename name}queryType{__typename name}subscriptionType{__typename name}types{__typename...IntrospectionFullType}}}}}}",
42+
"metadata": Object {
43+
"engineSignature": "",
44+
},
45+
"signature": "f7f71dac7423a856fcc1b05a944e92247d0bba4beafd508410890242d4cf6d5f",
46+
},
47+
]
48+
`;

0 commit comments

Comments
 (0)