Skip to content

Instantly share code, notes, and snippets.

@dfkaye
Last active August 2, 2019 22:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dfkaye/ad5e52bf2aa0179148741b7d29a204ec to your computer and use it in GitHub Desktop.
Save dfkaye/ad5e52bf2aa0179148741b7d29a204ec to your computer and use it in GitHub Desktop.
graphQL query builder helper and test (not entirely done yet) plus graphRequest example
/* graphQL query builder */
/**
* @function buildGraphQuery takes a map of field values, and other items for a
* graphQL query string, and returns an object containing each part, plus the
* `toString()` method that produces the graph query.
*
* The returned object can be tested on each of its parts that the `toString()`
* method uses to construct the final graphQL query string.
*
* Example input: {
* fields: { name: 'hello', address: { street: 'first st', city: 'happy dog' } },
* searchType: 'CYCLE_NOC',
* queryFields: { name, address: [ 'street', 'city', 'countryCode', 'postalCode' ]}
* }
*
* Output from the toString() method (linebreaks inserted here for
* readability):
*
* "{
* \"query\": {
* yapSearch (term: \"{
* 'searchTerms' :[
* {key: 'doc.name', value: 'hello'},
* {key: 'doc.address', street: 'first st', city: 'happy dog' }],
* 'searchType': 'CYCLE_NOC'}
* \") {
* name, address { street, city, countryCode, postalCode }
* }
* }
* }";
*
* @param {object} fields - map of graph search fields
* @param {string} searchType - db search
* @param {object} queryFields - map of graph result fields
* @returns {object}
*/
export function buildGraphQuery({ fields = {}, searchType = '', queryFields = {} }) {
return {
operationType: 'query',
operationName: 'yapSearch',
variable: 'term',
searchTerms: buildSearchTerms(fields),
searchType: `'searchType': '${ searchType }'`,
queryFields: `{ ${ buildQueryFields(queryFields) } }`, // `{ ${ collectionName } { ${ queryFields.join(' ') } } }`,
toString: function() {
const { operationType, operationName, variable, searchTerms, searchType, queryFields } = this;
return `{ "${ operationType }": "{ ${ operationName } (${ variable }: \\"{ 'searchTerms': ${ JSON.stringify(searchTerms).replace(/"/g, "'") }, ${ searchType } }\\") ${ queryFields } }" }`
}
};
}
/**
* @function buildSearchTerms takes a map of field values and converts them to a
* graphQL array of key-value pairs. If values are objects, each key is added to
* the query object, including queryType and queryClause which are required.
*
* Not exported.
*
* @param {object} fields
* @returns {Array}
*/
function buildSearchTerms(fields = {}) {
const searchTerms = [];
Object.keys(fields).forEach(name => {
const key = name === 'id' ? '_id' : `doc.${ name }`;
const item = fields[name];
const term = { key };
// Skip item if it is null, undefined, NaN, or empty or whitespace
// eslint-disable-next-line no-self-compare
if (/null|undefined/.test(item) || item !== item || !String(item).trim()) {
return;
}
if (item && /object/.test(typeof item)) {
// Object should contain expected graphql type and clause.
if (!(item.queryType && item.queryClause)) {
console.group('build query search terms;');
console.error(`Missing queryType or queryClause in "${ name }"`);
console.dir(item);
console.groupEnd();
return;
}
// Add each entry to the searchTerm
Object.keys(item).forEach(k => {
term[k] = item[k];
});
} else {
term.value = item;
}
searchTerms.push(term);
});
return searchTerms;
}
/**
* @function buildQueryFields processes a map of graphQL fields by collection
* name, returns partial graphQL query string by name and fields.
*
* If a field contains an object as one of its values, the function processes
* that value recursively.
*
* Not exported.
*
* Examples:
* { 'value': 'a'} produces `value { a }`
* { 'array': ['a', 'b'] } produces `multiple { a b }`
* { 'nested': ['a', { 'leaf': ['x', 'y'] }] } produces `nested { a leaf { x y } }`
*
* @param {@object} queryFields
* @returns {string}
*/
function buildQueryFields(queryFields = {}) {
const collections = Object.keys(queryFields).map(collectionName => {
const queryMap = queryFields[collectionName].map(entry => {
return typeof entry === 'object'
? buildQueryFields(entry)
: entry;
});
return `${ collectionName } { ${ queryMap.join(', ') } }`
});
return `${ collections.join(', ') }`;
}
/**
* @function graphRequest accepts a token and JSON graph query string, and
* fetches from graphQL endpoint, returning a Promisified result.
*
* @param {object} apiData `access_token` and `api_server` fields.
* @param {JSONString} jsonGraphQuery
*
* @returns {Promise}
*/
export async function graphRequest(apiData, jsonGraphQuery) {
// TODO: possibly move the pathName setup to Liferay?
const access_url = `${apiData.api_server}/yap-search/graphql/payments`;
return await fetch(access_url, {
method: "POST",
headers: {
"Authorization": `Bearer ${apiData.access_token}`,
"Content-type": "application/json"
},
body: jsonGraphQuery
})
.then(res => res.json())
.catch(res => res)
}
/* graphQL query builder */
describe('buildGraphQuery(values)', () => {
describe('searchTerms', () => {
// Scenario tests only the searchTerms clause.
it('converts key "id" to "_id"', () => {
const fields = {
id: 'test',
};
var { searchTerms } = buildGraphQuery({ fields });
var term = searchTerms[0];
expect(term.key).toBe('_id');
expect(term.value).toBe('test');
});
it('converts key "something" to "doc.something"', () => {
const fields = {
something: "else"
};
var { searchTerms } = buildGraphQuery({ fields });
var term = searchTerms[0];
expect(term.key).toBe('doc.something');
expect(term.value).toBe('else');
});
it('converts "nested.item:value" to "doc.nested", "item:value", and does not create value entry', () => {
/*
Example:
{'key':'doc.openedDate','fromValue':1561759680132,'toValue':1561759762609,'queryType':'date_range','queryClause':'must'}
*/
const fields = {
'nested': {
'from': "start",
'to': "end",
'queryType': 'date_range',
'queryClause': 'must'
}
};
var { searchTerms } = buildGraphQuery({ fields });
var term = searchTerms[0];
expect(term.value).toBe(undefined);
expect(term.key).toBe('doc.nested');
expect(term.from).toBe('start');
expect(term.to).toBe('end');
expect(term.queryType).toBe('date_range');
expect(term.queryClause).toBe('must');
});
it('skips item if queryType and queryClause are missing from nested input', () => {
const fields = {
'nested': {
'start': "day",
'end': "night"
}
};
var { searchTerms } = buildGraphQuery({ fields });
var term = searchTerms[0];
expect(term).toBe(undefined);
});
it('creates multiple entries for multiple keys for multiple value types', () => {
const fields = {
first: "one",
second: "two",
third: {
string: 'three',
number: 3,
boolean: false,
queryType: 'any',
queryClause: 'should'
}
};
var { searchTerms } = buildGraphQuery({ fields });
var first = searchTerms[0];
expect(first.key).toBe('doc.first');
expect(first.value).toBe('one');
var second = searchTerms[1];
expect(second.key).toBe('doc.second');
expect(second.value).toBe('two');
var third = searchTerms[2];
expect(third.key).toBe('doc.third');
expect(third.string).toBe('three');
expect(third.number).toBe(3);
expect(third.boolean).toBe(false);
expect(third.queryType).toBe('any');
expect(third.queryClause).toBe('should');
});
});
describe('queryFields', () => {
// Scenario tests only the queryFields clause.
it('walks collections to produce the queryFields clause', () => {
const fields = {};
const searchType = '';
const queryFields = {
'flat': ['a', 'b', 'c']
};
var test = buildGraphQuery({ fields, searchType, queryFields });
expect(test.queryFields).toBe("{ flat { a, b, c } }");
});
it('walks nested params in the queryFields clause', () => {
const fields = {};
const searchType = 'CYCLE_NOC';
const queryFields = {
'flat': ['a', 'b', 'c'],
'nested': ['a', 'b', { leaf: ['x', 'y', 'z'] }]
};
var test = buildGraphQuery({ fields, searchType, queryFields });
expect(test.queryFields).toBe("{ flat { a, b, c }, nested { a, b, leaf { x, y, z } } }");
});
});
describe('searchType param', () => {
// Scenario tests only the searchType clause.
it('results in the searchType clause', () => {
const fields = {};
const searchType = 'CYCLE_NOC';
const queryFields = {};
var test = buildGraphQuery({ fields, searchType, queryFields });
expect(test.searchType).toBe("'searchType': 'CYCLE_NOC'");
});
});
describe('toString', () => {
// Scenario tests the toString() operation for the full query output.
it('builds the complete graphQL query', () => {
const fields = {
'nested': {
'start': "day",
'end': "night",
'queryType': 'date_range',
'queryClause': 'should'
}
};
const searchType = 'CYCLE_NOC';
const queryFields = {
'nested': ['a', 'b', { leaf: ['x', 'y', 'z'] }]
};
var test = buildGraphQuery({ fields, searchType, queryFields });
var query = test.toString();
var expected = `{ "query": "{ yapSearch (term: \\"{ 'searchTerms': [{'key':'doc.nested','start':'day','end':'night','queryType':'date_range','queryClause':'should'}], 'searchType': 'CYCLE_NOC' }\\") { nested { a, b, leaf { x, y, z } } } }" }`;
// conversion by overridden toString() method.
expect(query).toBe(expected);
// conversion by type constructor.
expect(String(test)).toBe(expected);
// conversion by operator coercion.
expect('' + test).toBe(expected);
});
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment