From 82bf61a88dd2b358b90b61c5a068ca9a46437a32 Mon Sep 17 00:00:00 2001 From: Kayne Ruse Date: Mon, 5 Apr 2021 20:46:39 +1000 Subject: [PATCH] Split test into separate files --- test/index.js | 232 ++--------------------------------------- test/mock-models.js | 103 ++++++++++++++++++ test/mock-sequelize.js | 8 ++ test/query-handlers.js | 115 ++++++++++++++++++++ test/schema.js | 15 +++ 5 files changed, 247 insertions(+), 226 deletions(-) create mode 100644 test/mock-models.js create mode 100644 test/mock-sequelize.js create mode 100644 test/query-handlers.js create mode 100644 test/schema.js diff --git a/test/index.js b/test/index.js index a62eae3..aae7ba7 100644 --- a/test/index.js +++ b/test/index.js @@ -1,228 +1,4 @@ -//mock tools -const Op = { - eq: 'eq' -}; - -const books = { - findAll: async args => { - let arr = [ - { id: 1, title: 'Outlander', published: '1991', rating: 9.5 }, - { id: 2, title: 'Dragonfly in Amber', published: '1992', rating: 9.5 }, - { id: 3, title: 'Voyager', published: '1993', rating: 9.5 }, - { id: 4, title: 'Drums of Autumn', published: '1996', rating: 9.5 }, - { id: 5, title: 'The Fiery Cross', published: '2000', rating: 9.5 }, //Incorrect, the correct publish date is 2001 - { id: 6, title: 'The Breath of Snow and Ashes', published: '2005', rating: 9.5 }, - { id: 7, title: 'An Echo in the Bone', published: '2009', rating: 9.5 }, - { id: 8, title: 'Written in my Own Heart\'s Blood', published: '2014', rating: 9.5 }, - { id: 9, title: 'Go Tell the Bees That I Am Gone', published: null, rating: 9.5 }, - - { id: 10, title: 'The Forest of Silence', published: '2000', rating: 9.5 }, - { id: 11, title: 'The Lake of Tears', published: '2000', rating: 9.5 }, - { id: 12, title: 'The City of Rats', published: '2000', rating: 9.5 }, - { id: 13, title: 'The Shifting Sands', published: '2000', rating: 9.5 }, - { id: 14, title: 'Dread Mountain', published: '2000', rating: 9.5 }, - { id: 15, title: 'The Maze of the Beast', published: '2000', rating: 9.5 }, - { id: 16, title: 'The Valley of the Lost', published: '2000', rating: 9.5 }, - { id: 17, title: 'Return to Del', published: '2000', rating: 9.5 }, - - { id: 18, title: 'The Wind in the Willows', published: '1908', rating: 9.5 }, - ]; - - const { attributes, where } = args; - - //filter out non-matching elements - arr = arr.filter(element => { - if (Object.keys(where).length == 0) { - return true; - } - - return Object.keys(where).reduce((result, key) => { - return result && (element[key] || 'null').toString() == where[key].eq.toString(); - }, true); - }); - - //filter out non-used attributes - arr = arr.map(element => { - //only they element keys that are in attributes - const keys = Object.keys(element).filter(key => attributes.includes(key)); - - //determine which fields to carry over - const ret = {}; - keys.forEach(key => ret[key] = element[key]); - - return ret; - }); - - return arr; - } -} - -const authors = { - findAll: async args => { - let arr = [ - { id: 1, name: 'Diana Gabaldon', books: [1, 2, 3, 4, 5, 6, 7, 8, 9] }, - { id: 2, name: 'Emily Rodda', books: [10, 11, 12, 13, 14, 15, 16, 17] }, - { id: 3, name: 'Kenneth Grahame', books: [18] } - ]; - - const { attributes, where } = args; - - //filter out non-matching elements - arr = arr.filter(element => { - if (Object.keys(where).length == 0) { - return true; - } - - return Object.keys(where).reduce((result, key) => { - return result && (element[key] || 'null').toString() == where[key].eq.toString(); - }, true); - }); - - //filter out non-used attributes - arr = arr.map(element => { - //only they element keys that are in attributes - const keys = Object.keys(element).filter(key => attributes.includes(key)); - - //determine which fields to carry over - const ret = {}; - keys.forEach(key => ret[key] = element[key]); - - return ret; - }); - - return arr; - } -} - -//the handler functions return arrays for each type, containing every element that satisfies the queries - -//the "query" argument contains the object built from the sineQL query -//the "graph" argument contains the typeGraph - -//the task of the handler functions is to query the database, and return the correct results - -/* possible values for "query" include: - -{ - typeName: 'Author', - name: { typeName: 'String', scalar: true, match: 'Kenneth Grahame' }, - books: { typeName: 'Book', match: { - typeName: 'Book', - title: { typeName: 'String', scalar: true, match: 'The wind in the Willows' } - published: { typeName: 'Date', scalar: true } - } -} - -*/ - -//depth-first search seems to be the best option -//Each query shouldn't know if it's a sub-query - -const queryHandlers = { - //complex compound - Author: async (query, graph) => { - //DEBUGGING - // console.log('Author():', query); - - //get the fields alone - const { typeName, ...fields } = query; - - //hack the id into the fields list (if it's not there already) - fields['id'] = fields['id'] || { typeName: 'Integer', scalar: true }; - - //get the names of matched fields (fields to find) - 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 => { - if (query[mn].match !== true) { //true means it's a compound type - where[mn] = { [Op.eq]: query[mn].match }; - } - }); - } - - //these are field names - const scalars = Object.keys(fields).filter(field => graph[fields[field].typeName].scalar); - const nonScalars = Object.keys(fields).filter(field => !graph[fields[field].typeName].scalar); - - let authorResults = await authors.findAll({ - attributes: Object.keys(fields), //fields to find (keys) - where: where - }); //sequelize ORM model - - const promiseArray = nonScalars.map(async nonScalar => { - //delegate to a deeper part of the tree - 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)); //using the hacked-in ID field (JOIN) - }); - - //prune the authors when matching, but their results are empty - authorResults = authorResults.filter(author => { - return !(fields[nonScalar].match && author[nonScalar].length == 0); - }); - }); - - await Promise.all(promiseArray); - - //finally, return the results - return authorResults; - }, - - //simple compound - Book: async (query, graph) => { - // console.log('Book():', query); - - //get the fields alone - const { typeName, ...fields } = query; - - //hack the id into the fields list (if it's not there already) - fields['id'] = fields['id'] || { typeName: 'Integer', scalar: true }; //TODO: should this be automatic? - - //get the names of matched fields - 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 => { - if (query[mn].match !== true) { - where[mn] = { [Op.eq]: query[mn].match }; - } - }); - } - - //return the result - return await books.findAll({ - attributes: Object.keys(fields), //fields to find - where: where - }); //sequelize ORM model - } -}; - -//the matching schema -const schema = ` -scalar Date - -type Book { - String title - Date published - Float rating -} - -type Author { - String name - Book books -} -`; - -//input tool +//input tools const readline = require('readline'); const rl = readline.createInterface({ @@ -247,7 +23,11 @@ const question = (prompt, def = null) => { //the library to test const sineQL = require('../source/index.js'); -//run the function in debug mode (builds type graph) +//the arguments to the library +const schema = require('./schema'); +const queryHandlers = require('./query-handlers'); + +//run the setup function to create the closure (creates the type graph) const sine = sineQL(schema, { queryHandlers }, { debug: false }); //actually ask the question diff --git a/test/mock-models.js b/test/mock-models.js new file mode 100644 index 0000000..790cbb1 --- /dev/null +++ b/test/mock-models.js @@ -0,0 +1,103 @@ +const booksData = [ + { id: 1, title: 'Outlander', published: '1991', rating: 9.5 }, + { id: 2, title: 'Dragonfly in Amber', published: '1992', rating: 9.5 }, + { id: 3, title: 'Voyager', published: '1993', rating: 9.5 }, + { id: 4, title: 'Drums of Autumn', published: '1996', rating: 9.5 }, + { id: 5, title: 'The Fiery Cross', published: '2000', rating: 9.5 }, //Incorrect, the correct publish date is 2001 + { id: 6, title: 'The Breath of Snow and Ashes', published: '2005', rating: 9.5 }, + { id: 7, title: 'An Echo in the Bone', published: '2009', rating: 9.5 }, + { id: 8, title: 'Written in my Own Heart\'s Blood', published: '2014', rating: 9.5 }, + { id: 9, title: 'Go Tell the Bees That I Am Gone', published: null, rating: 9.5 }, + + { id: 10, title: 'The Forest of Silence', published: '2000', rating: 9.5 }, + { id: 11, title: 'The Lake of Tears', published: '2000', rating: 9.5 }, + { id: 12, title: 'The City of Rats', published: '2000', rating: 9.5 }, + { id: 13, title: 'The Shifting Sands', published: '2000', rating: 9.5 }, + { id: 14, title: 'Dread Mountain', published: '2000', rating: 9.5 }, + { id: 15, title: 'The Maze of the Beast', published: '2000', rating: 9.5 }, + { id: 16, title: 'The Valley of the Lost', published: '2000', rating: 9.5 }, + { id: 17, title: 'Return to Del', published: '2000', rating: 9.5 }, + + { id: 18, title: 'The Wind in the Willows', published: '1908', rating: 9.5 }, +]; + +const authorsData = [ + { id: 1, name: 'Diana Gabaldon', books: [1, 2, 3, 4, 5, 6, 7, 8, 9] }, + { id: 2, name: 'Emily Rodda', books: [10, 11, 12, 13, 14, 15, 16, 17] }, + { id: 3, name: 'Kenneth Grahame', books: [18] } +]; + +const books = { + findAll: async args => { + const { attributes, where } = args; + + //returning + return booksData + + //filter out non-matching elements + .filter(element => { + //don't filter if no 'where' parameters + if (Object.keys(where).length == 0) { + return true; + } + + //filter out by the 'where' values + return Object.keys(where).reduce((result, key) => { + return result && (element[key] || 'null').toString() == where[key].eq.toString(); + }, true); + }) + + //filter out non-used attributes + .map(element => { + //only they element keys that are in attributes + const keys = Object.keys(element).filter(key => attributes.includes(key)); + + //determine which fields to carry over + const ret = {}; + keys.forEach(key => ret[key] = element[key]); + + return ret; + }) + ; + } +}; + +const authors = { + findAll: async args => { + const { attributes, where } = args; + + //returning + return authorsData + + //filter out non-matching elements + .filter(element => { + //don't filter if no 'where' parameters + if (Object.keys(where).length == 0) { + return true; + } + + //filter out by the 'where' values + return Object.keys(where).reduce((result, key) => { + return result && (element[key] || 'null').toString() == where[key].eq.toString(); + }, true); + }) + + //filter out non-used attributes + .map(element => { + //only they element keys that are in attributes + const keys = Object.keys(element).filter(key => attributes.includes(key)); + + //determine which fields to carry over + const ret = {}; + keys.forEach(key => ret[key] = element[key]); + + return ret; + }) + ; + } +}; + +module.exports = { + books, + authors +}; \ No newline at end of file diff --git a/test/mock-sequelize.js b/test/mock-sequelize.js new file mode 100644 index 0000000..6db6a71 --- /dev/null +++ b/test/mock-sequelize.js @@ -0,0 +1,8 @@ +//mock tools +const Op = { + eq: 'eq' +}; + +module.exports = { + Op +}; \ No newline at end of file diff --git a/test/query-handlers.js b/test/query-handlers.js new file mode 100644 index 0000000..7f7a071 --- /dev/null +++ b/test/query-handlers.js @@ -0,0 +1,115 @@ +const { Op } = require('./mock-sequelize'); +const { books, authors } = require('./mock-models'); + +//the handler functions return arrays for each type, containing every element that satisfies the queries + +//the "query" argument contains the object built from the sineQL query +//the "graph" argument contains the typeGraph + +//the task of the handler functions is to query the database, and return the correct results + +/* possible values for "query" include: + +{ + id: 1, + typeName: 'Author', + name: { typeName: 'String', scalar: true, match: 'Kenneth Grahame' }, + books: { typeName: 'Book', match: { + id: 2, + typeName: 'Book', + title: { typeName: 'String', scalar: true, match: 'The Wind in the Willows' } + published: { typeName: 'Date', scalar: true } + } +} + +*/ + +//depth-first search seems to be the best option +//Each query shouldn't know if it's a sub-query + +const queryHandlers = { + //complex compound + Author: async (query, graph) => { + //get the fields alone + const { typeName, ...fields } = query; + + //hack the id into the fields list (if it's not there already) + fields['id'] = fields['id'] || { typeName: 'Integer', scalar: true }; //TODO: should this be default? + + //get the names of matched fields (fields to find) + 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 => { + if (query[mn].match !== true) { //true means it's a compound type + where[mn] = { [Op.eq]: query[mn].match }; + } + }); + } + + //these are field names + const scalars = Object.keys(fields).filter(field => graph[fields[field].typeName].scalar); + const nonScalars = Object.keys(fields).filter(field => !graph[fields[field].typeName].scalar); + + let authorResults = await authors.findAll({ + attributes: Object.keys(fields), //fields to find (keys) + where: where + }); //sequelize ORM model + + const promiseArray = nonScalars.map(async nonScalar => { + //delegate to a deeper part of the tree + 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)); //using the hacked-in ID field (JOIN) + }); + + //prune the authors when matching, but their results are empty + authorResults = authorResults.filter(author => { + return !(fields[nonScalar].match && author[nonScalar].length == 0); + }); + }); + + await Promise.all(promiseArray); + + //finally, return the results + return authorResults; + }, + + //simple compound + Book: async (query, graph) => { + // console.log('Book():', query); + + //get the fields alone + const { typeName, ...fields } = query; + + //hack the id into the fields list (if it's not there already) + fields['id'] = fields['id'] || { typeName: 'Integer', scalar: true }; //TODO: should this be automatic? + + //get the names of matched fields + 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 => { + if (query[mn].match !== true) { + where[mn] = { [Op.eq]: query[mn].match }; + } + }); + } + + //return the result + return await books.findAll({ + attributes: Object.keys(fields), //fields to find + where: where + }); //sequelize ORM model + } +}; + +module.exports = queryHandlers; \ No newline at end of file diff --git a/test/schema.js b/test/schema.js new file mode 100644 index 0000000..36c74c3 --- /dev/null +++ b/test/schema.js @@ -0,0 +1,15 @@ +//the matching schema +module.exports = ` +scalar Date + +type Book { + String title + Date published + Float rating +} + +type Author { + String name + Book books +} +`; \ No newline at end of file