Skip to content

Commit

Permalink
Generate Swift operation IDs and operation <--> operation ID mapping …
Browse files Browse the repository at this point in the history
…file (apollographql#147)

* Initial progress on Swift operationId generation

* Add dependency on crypto library for SHA256

* In Swift, operationId is computed and output with every operation class

* Factor out Swift operationId into its own function

* Write to a JSON file the mapping between operations and their ids (Swift-only)

* Update operationIds map to be keyed by operation name (which are guaranteed by apollo-codegen to be unique) and providing values with both operation id and source

* OperationIds map is keyed by id, and operation name is part of the value

* Use === instead of ==

* Update Swift Jest snapshots with new newline

* Fix missing comma

* Add fragments to classDeclarationForOperation invocation in Swift tests

* Add unit tests exercising operation id generation in Swift

* Move generation of operation IDs and operation source + fragments to compilation step rather than code generation step.
Also, when generating operation source + fragments, handle possibility of nested fragment references.

* Remove obsolete test checking for generated mapping between operation ids and sources on context (it's no longer done that way)

* Improve readability of generateOperationIds option in compileFromSource function in Swif tests

* Add Swift code generation test that verifies that, when there are nested fragment refrences, the correct source + fragments is generated for the operation id mapping file

* Fix typescript error related to operation ids map

* PR feedback and tweaks:
- Improve CLI message
- Remove unnecessary parameter in a function call
- Rename Swift operationId to operationIdentifier

* Update test snapshot
  • Loading branch information
ecstasy2 authored and Bobo Diallo committed Jun 29, 2017
1 parent 78c388a commit ddc90e6
Show file tree
Hide file tree
Showing 7 changed files with 360 additions and 21 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"inflected": "^2.0.2",
"mkdirp": "^0.5.1",
"node-fetch": "^1.5.3",
"sjcl": "^1.0.6",
"source-map-support": "^0.4.15",
"yargs": "^8.0.1"
},
Expand Down
10 changes: 9 additions & 1 deletion src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ yargs
demand: false,
describe: "Name of the template literal tag used to identify template literals containing GraphQL queries in Javascript/Typescript code",
default: 'gql'
},
"operation-ids-path": {
demand: false,
describe: "Path to an operation id JSON map file. If specified, also stores the operation ids (hashes) as properties on operation types [currently Swift-only]",
default: null,
normalize: true
}
},
argv => {
Expand All @@ -155,7 +161,9 @@ yargs
passthroughCustomScalars: argv["passthrough-custom-scalars"] || argv["custom-scalars-prefix"] !== '',
customScalarsPrefix: argv["custom-scalars-prefix"] || '',
addTypename: argv["add-typename"],
namespace: argv.namespace
namespace: argv.namespace,
operationIdsPath: argv["operation-ids-path"],
generateOperationIds: !!argv["operation-ids-path"]
};

generate(inputPaths, argv.schema, argv.output, argv.target, argv.tagName, options);
Expand Down
34 changes: 34 additions & 0 deletions src/compilation.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ import {
indent
} from './utilities/printing';

import {
flatMap,
uniqBy
} from 'lodash';

import * as sjcl from 'sjcl';

// Parts of this code are adapted from graphql-js

export function compileToIR(schema, document, options = { mergeInFieldsFromFragmentSpreads: true }) {
Expand All @@ -65,6 +72,10 @@ export function compileToIR(schema, document, options = { mergeInFieldsFromFragm
fragments[fragment.name.value] = compiler.compileFragment(fragment)
});

Object.values(operations).forEach(operation => {
augmentCompiledOperationWithFragments(operation, fragments)
});

const typesUsed = compiler.typesUsed;

return { schema, operations, fragments, typesUsed };
Expand Down Expand Up @@ -417,6 +428,29 @@ export class Compiler {
}
}

function augmentCompiledOperationWithFragments(compiledOperation, compiledFragments) {
const operationAndFragments = operationAndRelatedFragments(compiledOperation, compiledFragments);
compiledOperation.sourceWithFragments = operationAndFragments.map(operationOrFragment => {
return operationOrFragment.source;
}).join('\n');
const idBits = sjcl.hash.sha256.hash(compiledOperation.sourceWithFragments);
compiledOperation.operationId = sjcl.codec.hex.fromBits(idBits);
}

function operationAndRelatedFragments(compiledOperationOrFragment, allCompiledFragments) {
let result = flatMap(compiledOperationOrFragment.fragmentsReferenced, (fragmentName) => {
return operationAndRelatedFragments(allCompiledFragments[fragmentName], allCompiledFragments);
});
result.unshift(compiledOperationOrFragment);
result = uniqBy(result, (compiledOperationOrFragment) => {
return compiledOperationOrFragment.fragmentName;
});
result = result.sort((a, b) => {
return a.fragmentName > b.fragmentName;
});
return result;
}

function argumentsFromAST(args) {
return args && args.map(arg => {
return { name: arg.name.value, value: valueFromValueNode(arg.value) };
Expand Down
22 changes: 21 additions & 1 deletion src/generate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as fs from 'fs'
import * as fs from 'fs';

import { ToolError, logError } from './errors'
import { loadSchema, loadAndMergeQueryDocuments } from './loading'
Expand Down Expand Up @@ -56,4 +56,24 @@ export default function generate(
} else {
console.log(output);
}

if (context.generateOperationIds) {
writeOperationIdsMap(context)
}
}

interface OperationIdsMap {
name: string,
source: string
}

function writeOperationIdsMap(context: any) {
let operationIdsMap: { [id: string]: OperationIdsMap } = {};
Object.values(context.operations).forEach(operation => {
operationIdsMap[operation.operationId] = {
name: operation.operationName,
source: operation.sourceWithFragments
};
});
fs.writeFileSync(context.operationIdsPath, JSON.stringify(operationIdsMap, null, 2));
}
14 changes: 14 additions & 0 deletions src/swift/codeGeneration.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ export function classDeclarationForOperation(
fragmentSpreads,
fragmentsReferenced,
source,
sourceWithFragments,
operationId
}
) {
let className;
Expand Down Expand Up @@ -122,7 +124,10 @@ export function classDeclarationForOperation(
});
}

operationIdentifier(generator, { operationName, sourceWithFragments, operationId });

if (fragmentsReferenced && fragmentsReferenced.length > 0) {
generator.printNewlineIfNeeded();
generator.printOnNewline('public static var requestString: String { return operationString');
fragmentsReferenced.forEach(fragment => {
generator.print(`.appending(${structNameForFragmentName(fragment)}.fragmentString)`)
Expand Down Expand Up @@ -376,6 +381,15 @@ export function structDeclarationForSelectionSet(
});
}

function operationIdentifier(generator, { operationName, sourceWithFragments, operationId }) {
if (!generator.context.generateOperationIds) {
return
}

generator.printNewlineIfNeeded();
generator.printOnNewline(`public static let operationIdentifier = "${operationId}"`);
}

function propertyDeclarationForField(generator, field) {
const { kind, propertyName, typeName, type, isConditional, description } = propertyFromField(generator.context, field);
const responseName = field.responseName;
Expand Down
117 changes: 117 additions & 0 deletions test/swift/__snapshots__/codeGeneration.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ exports[`Swift code generation #classDeclarationForOperation() should generate a
\\" }\\" +
\\" }\\" +
\\"}\\"
public static var requestString: String { return operationString.appending(HeroDetails.fragmentString) }
public init() {
Expand Down Expand Up @@ -223,6 +224,7 @@ exports[`Swift code generation #classDeclarationForOperation() should generate a
\\" ...DroidDetails\\" +
\\" }\\" +
\\"}\\"
public static var requestString: String { return operationString.appending(DroidDetails.fragmentString) }
public init() {
Expand Down Expand Up @@ -372,6 +374,7 @@ exports[`Swift code generation #classDeclarationForOperation() should generate a
\\" ...HeroDetails\\" +
\\" }\\" +
\\"}\\"
public static var requestString: String { return operationString.appending(HeroDetails.fragmentString) }
public init() {
Expand Down Expand Up @@ -540,6 +543,120 @@ exports[`Swift code generation #classDeclarationForOperation() should generate a
}"
`;
exports[`Swift code generation #classDeclarationForOperation() when generateOperationIds is specified should generate a class declaration with an operationId property 1`] = `
"public final class HeroQuery: GraphQLQuery {
public static let operationString =
\\"query Hero {\\" +
\\" hero {\\" +
\\" ...HeroDetails\\" +
\\" }\\" +
\\"}\\"
public static let operationIdentifier = \\"90d0d674eb6a7b33776f63200d6cec3d09f881247c360a2ac9a29037a02210c4\\"
public static var requestString: String { return operationString.appending(HeroDetails.fragmentString) }
public init() {
}
public struct Data: GraphQLSelectionSet {
public static let possibleTypes = [\\"Query\\"]
public static let selections: [Selection] = [
Field(\\"hero\\", type: .object(Data.Hero.self)),
]
public var snapshot: Snapshot
public init(snapshot: Snapshot) {
self.snapshot = snapshot
}
public init(hero: Hero? = nil) {
self.init(snapshot: [\\"__typename\\": \\"Query\\", \\"hero\\": hero])
}
public var hero: Hero? {
get {
return (snapshot[\\"hero\\"]! as! Snapshot?).flatMap { Hero(snapshot: $0) }
}
set {
snapshot.updateValue(newValue?.snapshot, forKey: \\"hero\\")
}
}
public struct Hero: GraphQLSelectionSet {
public static let possibleTypes = [\\"Human\\", \\"Droid\\"]
public static let selections: [Selection] = [
Field(\\"name\\", type: .nonNull(.scalar(String.self))),
]
public var snapshot: Snapshot
public init(snapshot: Snapshot) {
self.snapshot = snapshot
}
public static func makeHuman(name: String) -> Hero {
return Hero(snapshot: [\\"__typename\\": \\"Human\\", \\"name\\": name])
}
public static func makeDroid(name: String) -> Hero {
return Hero(snapshot: [\\"__typename\\": \\"Droid\\", \\"name\\": name])
}
/// The name of the character
public var name: String {
get {
return snapshot[\\"name\\"]! as! String
}
set {
snapshot.updateValue(newValue, forKey: \\"name\\")
}
}
public var fragments: Fragments {
get {
return Fragments(snapshot: snapshot)
}
set {
snapshot = newValue.snapshot
}
}
public struct Fragments {
public var snapshot: Snapshot
public var heroDetails: HeroDetails {
get {
return HeroDetails(snapshot: snapshot)
}
set {
snapshot = newValue.snapshot
}
}
}
}
}
}"
`;
exports[`Swift code generation #classDeclarationForOperation() when generateOperationIds is specified should generate appropriate operation id mapping source when there are nested fragment references 1`] = `
"query Hero {
hero {
...HeroDetails
}
}
fragment HeroDetails on Character {
...HeroName
appearsIn
}
fragment HeroName on Character {
name
}"
`;
exports[`Swift code generation #initializerDeclarationForProperties() should generate initializer for a property 1`] = `
"public init(episode: Episode) {
self.episode = episode
Expand Down

0 comments on commit ddc90e6

Please sign in to comment.