mirror of
https://github.com/Ratstail91/sineQL.git
synced 2025-11-29 02:34:28 +11:00
Split test into separate files
This commit is contained in:
232
test/index.js
232
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
|
||||
|
||||
103
test/mock-models.js
Normal file
103
test/mock-models.js
Normal file
@@ -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
|
||||
};
|
||||
8
test/mock-sequelize.js
Normal file
8
test/mock-sequelize.js
Normal file
@@ -0,0 +1,8 @@
|
||||
//mock tools
|
||||
const Op = {
|
||||
eq: 'eq'
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
Op
|
||||
};
|
||||
115
test/query-handlers.js
Normal file
115
test/query-handlers.js
Normal file
@@ -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;
|
||||
15
test/schema.js
Normal file
15
test/schema.js
Normal file
@@ -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
|
||||
}
|
||||
`;
|
||||
Reference in New Issue
Block a user