diff --git a/source/index.js b/source/index.js index a5c17de..3682004 100644 --- a/source/index.js +++ b/source/index.js @@ -1,8 +1,9 @@ const buildTypeGraph = require('./build-type-graph'); const parseInput = require('./parse-input'); +const parseQueryTree = require('./parse-query-tree'); //the main function to be returned (sineQL()) -const sineQL = (schema, handler, options = {}) => { +const sineQL = (schema, { queryHandlers }, options = {}) => { let typeGraph; try { @@ -18,21 +19,27 @@ const sineQL = (schema, handler, options = {}) => { try { //parse the query const tokens = parseInput(reqBody, true, options); - let pos = 0; + const queryTree = parseQueryTree(tokens, typeGraph, options); - switch(tokens[pos]) { - //check for keywords + switch(tokens[0]) { + //check for leading keywords case 'create': case 'update': case 'delete': - return [501, 'Keyword not implemented: ' + tokens[pos]]; + return [501, 'Keyword not implemented: ' + tokens[0]]; //TODO: implement these keywords break; //no leading keyword - regular query - default: - //TODO: implement queries - return [501, 'Queries not implemented']; + default: { + const result = await queryHandlers[queryTree.typeName](queryTree, typeGraph); + + if (options.debug) { + console.log('Query tree results:\n', result); + } + + return [200, result]; + } } } catch(e) { diff --git a/source/parse-query-tree.js b/source/parse-query-tree.js new file mode 100644 index 0000000..312f067 --- /dev/null +++ b/source/parse-query-tree.js @@ -0,0 +1,98 @@ +//build the tokens into a single object of types representing the initial query +const parseQueryTree = (tokens, typeGraph, options) => { + let current = 1; //primed + + //TODO: check for top-level keywords + + //get a token that matches a type + if (!typeGraph[tokens[current - 1]]) { + throw `Expected a type in the type graph (found ${tokens[current - 1]})`; + } + + //read the block of lines + const [block, pos] = readBlock(tokens, current, tokens[current - 1], typeGraph, options); + + //insert the typename into the top-level block + block['typeName'] = tokens[current - 1]; + + //finally + return block; +}; + +const readBlock = (tokens, current, superType, typeGraph, options) => { + //scan the '{' + if (tokens[current++] != '{') { + throw `Expected '{'`; + } + + //result + const result = {}; + + //scan each "line" in this block + while(tokens[current++]) { + //check for end of block + if (tokens[current - 1] == '}') { + break; + } + + //check for block-level keywords + let modifier = null; + if (['match'].includes(tokens[current - 1])) { + modifier = tokens[current - 1]; + current++; + } + + //read field name + const fieldName = tokens[current - 1]; + + if (options.debug) { + console.log(`Trying to process field ${fieldName}`); + } + + //if the field is non-scalar, read the sub-block + if (!typeGraph[typeGraph[superType][fieldName].typeName].scalar) { + //recurse + const [block, pos] = readBlock(tokens, current, typeGraph[superType][fieldName].typeName, typeGraph, options); + + //insert the typename into the block + block['typeName'] = typeGraph[superType][fieldName].typeName; + + //insert into result + result[fieldName] = block; + + //insert the block-level modifier signal + if (modifier) { + result[fieldName][modifier] = true; + } + + current = pos; //pos points past the end of the block + + if (options.debug) { + console.log(`${fieldName}: ${JSON.stringify(result[fieldName])}`); + } + + continue; + } + + //scalar + else { + //save the typeGraph type into result + result[fieldName] = JSON.parse(JSON.stringify( typeGraph[ typeGraph[superType][fieldName].typeName ] )); + + //insert the block-level modifier signal + if (modifier) { + result[fieldName][modifier] = tokens[current++]; + } + + if (options.debug) { + console.log(`${fieldName}: `, result[fieldName]); + } + + continue; + } + } + + return [result, current]; +}; + +module.exports = parseQueryTree; \ No newline at end of file diff --git a/test/index.js b/test/index.js index d169910..c175f60 100644 --- a/test/index.js +++ b/test/index.js @@ -1,13 +1,20 @@ //mock tools +const Op = { + eq: 'eq' +}; + const books = { findAll: async args => { let arr = [ - { title: 'The Wind in the Willows', published: '1908-06-15' } + { id: 1, title: 'The Wind in the Willows', published: '1908-06-15' }, + { id: 2, title: 'The Fart in the Fronds', published: '1908-06-15' } ]; const { attributes, where } = args; - arr = arr.filter(el => !where || el.title == where.title || el.published == where.published); //TODO: fix this + //TODO: make this more generic + console.log('books attributes:', attributes); + console.log('books where', where); return arr; } @@ -15,13 +22,16 @@ const books = { const authors = { findAll: async args => { - const arr = [ - { name: 'Kenneth Grahame', bookIds: [1] }, + let arr = [ + { name: 'Kenneth Grahame', books: [1] }, + { name: 'Frank', books: [1, 2] }, + { name: 'Betty', books: [2] } ]; const { attributes, where } = args; - arr = arr.filter(el => !where || el.title == where.title || el.published == where.published); //TODO: fix this + console.log('authors attributes:', attributes); + console.log('authors where', where); return arr; } @@ -51,59 +61,71 @@ const authors = { //depth-first search seems to be the best option //Each query shouldn't know if it's a sub-query -const handler = { +const queryHandlers = { //complex compound Author: async (query, graph) => { + //DEBUGGING + // console.log('Author():', query); + //get the fields alone const { typeName, ...fields } = query; //get the names of matched fields - const matchedNames = Object.keys(fields.filter(field => field.match)); + const matchedNames = Object.keys(fields).filter(field => fields[field].match); //short-circuit if querying everything const where = {}; if (matchedNames.length > 0) { //build the "where" object matchedNames.forEach(mn => { - where[mn] = { - [Op.eq]: query[mn].match + if (query[mn].match !== true) { + where[mn] = { [Op.eq]: query[mn].match }; } }); } //these are field names - const scalars = Object.keys(fields).filter(field => graph[field.typeName].scalar); - const nonScalars = Object.keys(fields).filter(field => !graph[field.typeName].scalar); + const scalars = Object.keys(fields).filter(field => graph[fields[field].typeName].scalar); + const nonScalars = Object.keys(fields).filter(field => !graph[fields[field].typeName].scalar); - const results = await authors.findAll({ + const authorResults = await authors.findAll({ attributes: scalars, //fields to find (keys) where: where }); //sequelize ORM model - nonScalars.forEach(nonScalar => { + const promiseArray = nonScalars.map(async nonScalar => { //delegate to a deeper part of the tree - results[nonScalar] = handler[fields[nonScalar].typeName](fields[nonScalar], graph); + const nonScalarArray = await queryHandlers[fields[nonScalar].typeName](fields[nonScalar], graph); + + //for each author, update this non-scalar field with the non-scalar's recursed value + authorResults.forEach(author => { + author[nonScalar] = nonScalarArray.filter(ns => author[nonScalar].includes(ns.id)); + }); }); + await Promise.all(promiseArray); + //finally, return the results - return results; + return authorResults; }, //simple compound Book: async (query, graph) => { + // console.log('Book():', query); + //get the fields alone const { typeName, ...fields } = query; //get the names of matched fields - const matchedNames = Object.keys(fields.filter(field => field.match)); + const matchedNames = Object.keys(fields).filter(field => fields[field].match); //short-circuit if querying everything const where = {}; if (matchedNames.length > 0) { //build the "where" object matchedNames.forEach(mn => { - where[mn] = { - [Op.eq]: query[mn].match + if (query[mn].match !== true) { + where[mn] = { [Op.eq]: query[mn].match }; } }); } @@ -135,4 +157,10 @@ type Author { const sineQL = require('../source/index.js'); //run the function in debug mode (builds type graph) -const sine = sineQL(schema, handler, { debug: true }); +const sine = sineQL(schema, { queryHandlers }, { debug: true }); + +(async () => { + const [code, result] = await sine('Author { name match books { match title "The Wind in the Willows" published } }'); + + console.log('\n\n', JSON.stringify(result)); +})();