Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update operation normalization to deterministically sort fragments. #1158

Merged
merged 2 commits into from
Apr 2, 2019
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Upcoming

## `apollo`

- `apollo`
- 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)

## `apollo-language-server`

- apollo-language-server
Expand Down
634 changes: 100 additions & 534 deletions package-lock.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ export const REGISTER_OPERATIONS = gql`
$id: ID!
$clientIdentity: RegisteredClientIdentityInput!
$operations: [RegisteredOperationInput!]!
$manifestVersion: Int!
) {
service(id: $id) {
registerOperations(
clientIdentity: $clientIdentity
operations: $operations
manifestVersion: $manifestVersion
)
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/apollo-language-server/src/graphqlTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export interface RegisterOperationsVariables {
id: string;
clientIdentity: RegisteredClientIdentityInput;
operations: RegisteredOperationInput[];
manifestVersion: number;
}

/* tslint:disable */
Expand Down
2 changes: 1 addition & 1 deletion packages/apollo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@
"apollo-codegen-scala": "file:../apollo-codegen-scala",
"apollo-codegen-swift": "file:../apollo-codegen-swift",
"apollo-codegen-typescript": "file:../apollo-codegen-typescript",
"apollo-engine-reporting": "0.2.2",
"apollo-env": "file:../apollo-env",
"apollo-graphql": "file:../apollo-graphql",
"apollo-language-server": "file:../apollo-language-server",
"chalk": "2.4.2",
"cli-ux": "4.9.3",
Expand Down
95 changes: 29 additions & 66 deletions packages/apollo/src/commands/client/extract.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,10 @@
import { createHash } from "crypto";
import { writeFileSync } from "fs";
import {
printWithReducedWhitespace,
sortAST,
defaultSignature as engineDefaultSignature
} from "apollo-engine-reporting";
import { DocumentNode } from "graphql";

import { ClientCommand } from "../../Command";
import { hideCertainLiterals } from "./push";

// XXX this is duplicated code
const manifestOperationHash = (str: string): string =>
createHash("sha256")
.update(str)
.digest("hex");

const engineSignature = (_TODO_operationAST: DocumentNode): string => {
// TODO. We don't currently have access to the operation name since it's
// currently omitted by the `apollo-codegen-core` package logic.
return engineDefaultSignature(_TODO_operationAST, "TODO");
};
import {
getOperationManifestFromProject,
ManifestEntry
} from "../../utils/getOperationManifestFromProject";
import { ClientIdentity } from "apollo-language-server";

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

async run() {
const { clientIdentity, operations, filename }: any = await this.runTasks(
({ flags, project, config, args }) => [
{
title: "Extracting operations from project",
task: async ctx => {
const operations = Object.values(
this.project.mergedOperationsAndFragmentsForService
).map(operationAST => {
// While this could include dropping unused definitions, they are
// kept because the registered operations should mirror those in the
// client bundle minus any PII which lives within string literals.
const printed = printWithReducedWhitespace(
sortAST(hideCertainLiterals(operationAST))
);

return {
signature: manifestOperationHash(printed),
document: printed,
metadata: {
engineSignature: engineSignature(operationAST)
}
};
});

ctx.operations = operations;
ctx.clientIdentity = config.client;
}
},
{
title: "Outputing extracted queries",
task: (ctx, task) => {
const filename = args.output;
task.title = "Outputing extracted queries to " + filename;
ctx.filename = filename;
writeFileSync(
filename,
JSON.stringify(
{ version: 1, operations: ctx.operations },
null,
2
)
);
}
const { clientIdentity, operations, filename } = await this.runTasks<{
clientIdentity: ClientIdentity;
operations: ManifestEntry[];
filename: string;
}>(({ flags, project, config, args }) => [
{
title: "Extracting operations from project",
task: async ctx => {
ctx.operations = getOperationManifestFromProject(this.project);
ctx.clientIdentity = config.client;
}
]
);
},
{
title: "Outputing extracted queries",
task: (ctx, task) => {
const filename = args.output;
task.title = "Outputing extracted queries to " + filename;
ctx.filename = filename;
writeFileSync(
filename,
JSON.stringify({ version: 2, operations: ctx.operations }, null, 2)
);
}
}
]);

this.log(
`Successfully wrote ${operations.length} operations from the ${
Expand Down
87 changes: 16 additions & 71 deletions packages/apollo/src/commands/client/push.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,9 @@
import { createHash } from "crypto";
import {
printWithReducedWhitespace,
sortAST,
defaultSignature as engineDefaultSignature
} from "apollo-engine-reporting";

import {
visit,
DocumentNode,
IntValueNode,
FloatValueNode,
StringValueNode
} from "graphql";

import { ClientCommand } from "../../Command";

const manifestOperationHash = (str: string): string =>
createHash("sha256")
.update(str)
.digest("hex");

const engineSignature = (_TODO_operationAST: DocumentNode): string => {
// TODO. We don't currently have access to the operation name since it's
// currently omitted by the `apollo-codegen-core` package logic.
return engineDefaultSignature(_TODO_operationAST, "TODO");
};

// In the same spirit as the similarly named `hideLiterals` function from the
// `apollo-engine-reporting/src/signature.ts` module, we'll do an AST visit
// to redact literals. Developers are strongly encouraged to use the
// `variables` aspect of the which would avoid these being explicitly
// present in the operation manifest at all. The primary area of concern here
// is to avoid sending in-lined literals which might contain sensitive
// information (e.g. API keys, etc.).
export function hideCertainLiterals(ast: DocumentNode): DocumentNode {
return visit(ast, {
IntValue(node: IntValueNode): IntValueNode {
return { ...node, value: "0" };
},
FloatValue(node: FloatValueNode): FloatValueNode {
return { ...node, value: "0" };
},
StringValue(node: StringValueNode): StringValueNode {
return { ...node, value: "", block: false };
}
});
}
import {
getOperationManifestFromProject,
ManifestEntry
} from "../../utils/getOperationManifestFromProject";
import { ClientIdentity } from "apollo-language-server";

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

async run() {
const {
clientIdentity,
operations,
serviceName
}: any = await this.runTasks(({ flags, project, config }) => [
const { clientIdentity, operations, serviceName } = await this.runTasks<{
clientIdentity: ClientIdentity;
operations: ManifestEntry[];
serviceName: string;
}>(({ flags, project, config }) => [
{
title: "Pushing client information to Engine",
task: async ctx => {
if (!config.name) {
throw new Error("No service found to link to Engine");
}
const operations = Object.values(
this.project.mergedOperationsAndFragmentsForService
).map(operationAST => {
// While this could include dropping unused definitions, they are
// kept because the registered operations should mirror those in the
// client bundle minus any PII which lives within string literals.
const printed = printWithReducedWhitespace(
sortAST(hideCertainLiterals(operationAST))
);

return {
signature: manifestOperationHash(printed),
document: printed,
metadata: {
engineSignature: engineSignature(operationAST)
}
};
});
const operationManifest = getOperationManifestFromProject(
this.project
);

const { name, referenceID, version } = config.client!;
if (!name) {
Expand All @@ -96,13 +40,14 @@ export default class ServicePush extends ClientCommand {
version
},
id: config.name,
operations
operations: operationManifest,
manifestVersion: 2
};

await project.engine.registerOperations(variables);

// store data for logging
ctx.operations = operations;
ctx.operations = operationManifest;
ctx.serviceName = variables.id;
ctx.clientIdentity = variables.clientIdentity;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`getOperationManifestFromProject builds an operation manifest 1`] = `
Array [
Object {
"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)}}}}}}",
"metadata": Object {
"engineSignature": "",
},
"signature": "135e8314de0c2f23f4a2be87b20e59f30ecf2cbcdfad6676a46aad21d29cdb5b",
},
Object {
"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}}}",
"metadata": Object {
"engineSignature": "",
},
"signature": "3fd18a0f0369d801024bb42268bc4d4d1f6edd84a412923c4678c026bc1bdc62",
},
Object {
"document": "mutation RegisterOperations($clientIdentity:RegisteredClientIdentityInput!,$id:ID!,$operations:[RegisteredOperationInput!]!){service(id:$id){__typename registerOperations(clientIdentity:$clientIdentity,operations:$operations)}}",
"metadata": Object {
"engineSignature": "",
},
"signature": "1428ab44b0f2f4ea25bc28c08b5a7235cb96a2d535d1a15e0edd5f0695a4903f",
},
Object {
"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}}}}",
"metadata": Object {
"engineSignature": "",
},
"signature": "a2497bb8c32f97870dfc58823b1377f6339c3e5ba53010b2a6b1e5d8fa8b8634",
},
Object {
"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}}}}",
"metadata": Object {
"engineSignature": "",
},
"signature": "600e261efe1f25ee30c53edd0b2b83c7eb6691ef5ae1c8f460ace2d303ad053d",
},
Object {
"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}}}}}}",
"metadata": Object {
"engineSignature": "",
},
"signature": "f7f71dac7423a856fcc1b05a944e92247d0bba4beafd508410890242d4cf6d5f",
},
]
`;
Loading