Last active
August 2, 2019 22:19
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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(', ') }`; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @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) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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