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

Use graphql-js builtin function separateOperations #179

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
162 changes: 27 additions & 135 deletions loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
const os = require('os');
const gql = require('./src');

// Takes `source` (the source GraphQL query string)
// and `doc` (the parsed GraphQL document) and tacks on
// the imported definitions.
// Takes `source` (the source GraphQL query string) and `doc` (the parsed GraphQL document)
// and tacks on the imported definitions.
function expandImports(source, doc) {
const lines = source.split(/\r\n|\r|\n/);
let outputCode = `
Expand All @@ -14,21 +13,17 @@ function expandImports(source, doc) {
return defs.filter(
function(def) {
if (def.kind !== 'FragmentDefinition') return true;
var name = def.name.value
if (names[name]) {
return false;
} else {
names[name] = true;
return true;
}
var name = def.name.value;
// Filter out if seen; otherwise mark as seen and include.
return names[name] ? false : names[name] = true;
}
)
);
}
`;

lines.some((line) => {
if (line[0] === '#' && line.slice(1).split(' ')[0] === 'import') {
const importFile = line.slice(1).split(' ')[1];
if (line.substr(0, 7) === '#import') {
const importFile = line.split(' ')[1];
const parseDocument = `require(${importFile})`;
const appendDef = `doc.definitions = doc.definitions.concat(unique(${parseDocument}.definitions));`;
outputCode += appendDef + os.EOL;
Expand All @@ -47,138 +42,35 @@ module.exports = function(source) {
doc.loc.source = ${JSON.stringify(doc.loc.source)};
`;

let outputCode = "";
let outputCode = `
module.exports = doc;
`;

// Allow multiple query/mutation definitions in a file. This parses out dependencies
// at compile time, and then uses those at load time to create minimal query documents
// We cannot do the latter at compile time due to how the #import code works.
let operationCount = doc.definitions.reduce(function(accum, op) {
if (op.kind === "OperationDefinition") {
return accum + 1;
}
const countReducer = (accum, op) => op.kind === "OperationDefinition" ? accum + 1 : accum;
let operationCount = doc.definitions.reduce(countReducer, 0);

return accum;
}, 0);

if (operationCount < 1) {
outputCode += `
module.exports = doc;
`
} else {
if (operationCount > 1) {
outputCode += `
// Collect any fragment/type references from a node, adding them to the refs Set
function collectFragmentReferences(node, refs) {
if (node.kind === "FragmentSpread") {
refs.add(node.name.value);
} else if (node.kind === "VariableDefinition") {
var type = node.type;
if (type.kind === "NamedType") {
refs.add(type.name.value);
}
}

if (node.selectionSet) {
node.selectionSet.selections.forEach(function(selection) {
collectFragmentReferences(selection, refs);
});
}

if (node.variableDefinitions) {
node.variableDefinitions.forEach(function(def) {
collectFragmentReferences(def, refs);
});
}

if (node.definitions) {
node.definitions.forEach(function(def) {
collectFragmentReferences(def, refs);
});
}
}

var definitionRefs = {};
(function extractReferences() {
doc.definitions.forEach(function(def) {
if (def.name) {
var refs = new Set();
collectFragmentReferences(def, refs);
definitionRefs[def.name.value] = refs;
}
});
})();

function findOperation(doc, name) {
for (var i = 0; i < doc.definitions.length; i++) {
var element = doc.definitions[i];
if (element.name && element.name.value == name) {
return element;
}
}
}

function oneQuery(doc, operationName) {
// Copy the DocumentNode, but clear out the definitions
var newDoc = {
kind: doc.kind,
definitions: [findOperation(doc, operationName)]
};
if (doc.hasOwnProperty("loc")) {
newDoc.loc = doc.loc;
}

// Now, for the operation we're running, find any fragments referenced by
// it or the fragments it references
var opRefs = definitionRefs[operationName] || new Set();
var allRefs = new Set();
var newRefs = new Set(opRefs);
while (newRefs.size > 0) {
var prevRefs = newRefs;
newRefs = new Set();

prevRefs.forEach(function(refName) {
if (!allRefs.has(refName)) {
allRefs.add(refName);
var childRefs = definitionRefs[refName] || new Set();
childRefs.forEach(function(childRef) {
newRefs.add(childRef);
});
}
});
}

allRefs.forEach(function(refName) {
var op = findOperation(doc, refName);
if (op) {
newDoc.definitions.push(op);
}
});

return newDoc;
}

module.exports = doc;
`

for (const op of doc.definitions) {
if (op.kind === "OperationDefinition") {
if (!op.name) {
if (operationCount > 1) {
throw "Query/mutation names are required for a document with multiple definitions";
} else {
continue;
}
}
var separateOperations = require('graphql/utilities/separateOperations').separateOperations;
`;
}

const opName = op.name.value;
outputCode += `
module.exports["${opName}"] = oneQuery(doc, "${opName}");
`
for (const op of doc.definitions) {
const opName = op.name && op.name.value;
if (op.kind === "OperationDefinition") {
if (operationCount > 1) {
const errMsg = "Query/mutation names are required for a document with multiple definitions";
if (!opName) throw errMsg;
outputCode += 'module.exports = separateOperations(doc);';
} else if (opName) {
outputCode += `module.exports["${opName}"] = doc;`;
}
}
}

const importOutputCode = expandImports(source, doc);
const allCode = headerCode + os.EOL + importOutputCode + os.EOL + outputCode + os.EOL;

return allCode;
return headerCode + os.EOL + importOutputCode + os.EOL + outputCode + os.EOL;
};
52 changes: 32 additions & 20 deletions test/graphql.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ const gqlDefault = require('../src').default;
const loader = require('../loader');
const assert = require('chai').assert;

const oldRequire = require;

[gqlRequire, gqlDefault].forEach((gql, i) => {
describe(`gql ${i}`, () => {
it('parses queries', () => {
Expand Down Expand Up @@ -92,7 +94,7 @@ const assert = require('chai').assert;

gql.disableExperimentalFragmentVariables()
});

// see https://github.com/apollographql/graphql-tag/issues/168
it('does not nest queries needlessly in named exports', () => {
const jsSource = loader.call({ cacheable() {} }, `
Expand Down Expand Up @@ -120,6 +122,7 @@ const assert = require('chai').assert;
...F2
}
`);

const module = { exports: undefined };
eval(jsSource);

Expand All @@ -131,17 +134,17 @@ const assert = require('chai').assert;
const Q3 = module.exports.Q3.definitions;

assert.equal(Q1.length, 2);
assert.equal(Q1[0].name.value, 'Q1');
assert.equal(Q1[1].name.value, 'F1');
assert.equal(Q1[0].name.value, 'F1');
assert.equal(Q1[1].name.value, 'Q1');

assert.equal(Q2.length, 2);
assert.equal(Q2[0].name.value, 'Q2');
assert.equal(Q2[1].name.value, 'F2');
assert.equal(Q2[0].name.value, 'F2');
assert.equal(Q2[1].name.value, 'Q2');

assert.equal(Q3.length, 3);
assert.equal(Q3[0].name.value, 'Q3');
assert.equal(Q3[1].name.value, 'F1');
assert.equal(Q3[2].name.value, 'F2');
assert.equal(Q3[0].name.value, 'F1');
assert.equal(Q3[1].name.value, 'F2');
assert.equal(Q3[2].name.value, 'Q3');

});

Expand Down Expand Up @@ -176,10 +179,10 @@ const assert = require('chai').assert;
const Q2 = module.exports.Q2.definitions;

assert.equal(Q1.length, 4);
assert.equal(Q1[0].name.value, 'Q1');
assert.equal(Q1[1].name.value, 'F33');
assert.equal(Q1[2].name.value, 'F22');
assert.equal(Q1[3].name.value, 'F11');
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jnwng This pattern is brittle (hence me needing to change the tests). The order shouldn't matter IMO. I stuck with the same pattern for now, but if you'd like I can add a commit to change it. I'd map the names to an array and then do an assert.include for each value.

assert.equal(Q1[0].name.value, 'F11');
assert.equal(Q1[1].name.value, 'F22');
assert.equal(Q1[2].name.value, 'F33');
assert.equal(Q1[3].name.value, 'Q1');

assert.equal(Q2.length, 1);
});
Expand All @@ -192,9 +195,8 @@ const assert = require('chai').assert;
}
}`;
const jsSource = loader.call({ cacheable() {} }, query);
const oldRequire = require;
const module = { exports: undefined };
const require = (path) => {
let require = (path) => {
assert.equal(path, './fragment_definition.graphql');
return gql`
fragment authorDetails on Author {
Expand All @@ -203,6 +205,7 @@ const assert = require('chai').assert;
}`;
};
eval(jsSource);
require = oldRequire;
assert.equal(module.exports.kind, 'Document');
const definitions = module.exports.definitions;
assert.equal(definitions.length, 2);
Expand All @@ -225,17 +228,26 @@ const assert = require('chai').assert;
}
`;
const jsSource = loader.call({ cacheable() {} }, query);
const oldRequire = require;
const module = { exports: undefined };
const require = (path) => {
assert.equal(path, './fragment_definition.graphql');
return gql`

const paths = []
let require = (path) => {
paths.push(path);
if (path === './fragment_definition.graphql') {
return gql`
fragment F222 on F {
f1
f2
}`;
} else {
return oldRequire(path);
}
};

eval(jsSource);
require = oldRequire;

assert.include(paths, './fragment_definition.graphql');

assert.exists(module.exports.Q1);
assert.exists(module.exports.Q2);
Expand All @@ -244,8 +256,8 @@ const assert = require('chai').assert;
const Q2 = module.exports.Q2.definitions;

assert.equal(Q1.length, 3);
assert.equal(Q1[0].name.value, 'Q1');
assert.equal(Q1[1].name.value, 'F111');
assert.equal(Q1[0].name.value, 'F111');
assert.equal(Q1[1].name.value, 'Q1');
assert.equal(Q1[2].name.value, 'F222');

assert.equal(Q2.length, 1);
Expand Down