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

Generate Swift operation IDs and operation <--> operation ID mapping file #147

Merged
merged 19 commits into from
Jun 29, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be a separate option?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think having this as a separate option reads a lot better, especially in codeGeneration.js, which doesn't need to know or care about file paths.

};

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