Adjusted handler API

This commit is contained in:
2020-09-22 11:10:12 +10:00
parent 7f04a656c2
commit 0d245fdeb6
15 changed files with 2137 additions and 28 deletions

View File

@@ -2,6 +2,7 @@
* ~~Debugging options~~ * ~~Debugging options~~
* N + 1 problem solved * N + 1 problem solved
* Change "match" to "filter"
* Full documentation * Full documentation
* Graphical tool * Graphical tool
* GitHub CI testing * GitHub CI testing

View File

@@ -4,7 +4,7 @@
"description": "A simple to use graphQL clone", "description": "A simple to use graphQL clone",
"main": "source/index.js", "main": "source/index.js",
"scripts": { "scripts": {
"test": "cd test-server && npm install && npm run node" "test": "cd test-server-books && npm install && npm run node"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@@ -2,36 +2,44 @@ const keywords = require('./keywords.json');
const { eatBlock } = require('./utils'); const { eatBlock } = require('./utils');
//returns an object result from handler for all custom types //returns an object result from handler for all custom types
const parseQuery = async (handler, tokens, pos, typeGraph, parent = null) => { const parseQuery = async (handler, tokens, pos, typeGraph, parent = null, superMatching = false) => {
//only read past tokens //only read past tokens
pos++; pos++;
//determine this query's supertype //determine this query's supertype
let queryType; let superType;
if (typeGraph[tokens[pos - 1]] && typeGraph[tokens[pos - 1]].scalar) { if (!parent) { //top-level
queryType = tokens[pos - 1]; superType = tokens[pos - 1];
} }
else if (parent && typeGraph[parent.typeName][tokens[pos - 1]]) { else if (typeGraph[parent.typeName][ tokens[pos-1] ]) {
queryType = typeGraph[parent.typeName][tokens[pos - 1]].typeName; superType = typeGraph[parent.typeName][ tokens[pos-1] ].typeName;
} else { }
queryType = tokens[pos - 1];
else {
throw `Missing supertype in type graph (pos = ${pos})`;
}
//error handling
if (!handler[superType]) {
throw 'Unrecognized type ' + superType;
} }
if (tokens[pos++] != '{') { if (tokens[pos++] != '{') {
throw 'Expected \'{\' after queried type'; throw 'Expected \'{\' after supertype';
} }
//the scalars to pass to the handler - these are NEIGHBOURS in the hierarchy //the scalars to pass to the handler - components of the compound types
const scalarFields = []; const scalarFields = [];
const deferredCalls = []; //functions (promises) that will be called at the end of this function const deferredCalls = []; //functions (promises) that will be called at the end of this function
while(tokens[pos++] && tokens[pos - 1] !== '}') { //while not at the end of this block while(tokens[pos++] && tokens[pos - 1] !== '}') { //while not at the end of this block
let match = false; //check for matching flag
let matching = false;
if (tokens[pos - 1] === 'match') { if (tokens[pos - 1] === 'match') {
match = true; matching = true;
pos++; pos++;
} }
@@ -40,24 +48,25 @@ const parseQuery = async (handler, tokens, pos, typeGraph, parent = null) => {
throw 'Unexpected keyword ' + tokens[pos - 1]; throw 'Unexpected keyword ' + tokens[pos - 1];
} }
//type is a scalar, and can be queried //type is a scalar
if (typeGraph[queryType] && typeGraph[queryType][tokens[pos - 1]] && typeGraph[typeGraph[queryType][tokens[pos - 1]].typeName].scalar) { if (typeGraph[superType] && typeGraph[superType][tokens[pos - 1]] && typeGraph[typeGraph[superType][tokens[pos - 1]].typeName].scalar) {
//push the scalar object to the queryFields //push the scalar object to the queryFields
scalarFields.push({ typeName: typeGraph[queryType][tokens[pos - 1]].typeName, name: tokens[pos - 1], match: match ? tokens[pos++] : null }); scalarFields.push({ typeName: typeGraph[superType][tokens[pos - 1]].typeName, name: tokens[pos - 1], filter: matching ? tokens[pos++] : null });
//if I am a scalar child of a match amd I do not match //if I am a scalar child of a match amd I do not match
if (parent && parent.match && !match) { if (parent && superMatching && !matching) {
throw 'Broken match chain in scalar type ' + tokens[pos - 1]; throw 'Broken match chain in scalar type ' + tokens[pos - 1];
} }
} }
else if (typeGraph[queryType] && typeGraph[queryType][tokens[pos - 1]] && !typeGraph[typeGraph[queryType][tokens[pos - 1]].typeName].scalar) { //type is a compound, and must be recursed
else if (typeGraph[superType] && typeGraph[superType][tokens[pos - 1]]) {
const pos2 = pos; //cache the value to keep it from changing const pos2 = pos; //cache the value to keep it from changing
//recurse //recurse
deferredCalls.push(async (result) => { deferredCalls.push(async (result) => {
//if I am a compound child of a match amd I do not match //if I am a compound child of a match amd I do not match
if (parent && parent.match && !match) { if (parent && superMatching && !matching) {
throw 'Broken match chain in compound type ' + tokens[pos2 - 1]; throw 'Broken match chain in compound type ' + tokens[pos2 - 1];
} }
@@ -66,10 +75,11 @@ const parseQuery = async (handler, tokens, pos, typeGraph, parent = null) => {
tokens, tokens,
pos2 - 1, pos2 - 1,
typeGraph, typeGraph,
{ typeName: queryType, scalars: scalarFields, context: result, match: match } //parent object (this one) { typeName: superType, scalars: scalarFields, context: result }, //parent object (this one)
matching
); );
return [tokens[pos2 - 1], queryResult, match]; //HACK: match piggybacking on the tuple return [tokens[pos2 - 1], queryResult, matching]; //HACK: match piggybacking on the tuple
}); });
pos = eatBlock(tokens, pos + 2); pos = eatBlock(tokens, pos + 2);
@@ -84,13 +94,9 @@ const parseQuery = async (handler, tokens, pos, typeGraph, parent = null) => {
throw 'Expected \'}\' at the end of query (found ' + tokens[pos - 1] + ')'; throw 'Expected \'}\' at the end of query (found ' + tokens[pos - 1] + ')';
} }
if (!handler[queryType]) { let results = handler[superType](parent, scalarFields, superMatching);
throw 'Unrecognized type ' + queryType;
}
let results = handler[queryType](parent, scalarFields); //WTF: related to the recusion above (turning the results inside out?)
//WTF: related to the recusion above
results = await Promise.all(results.map(async res => { results = await Promise.all(results.map(async res => {
const tuples = await Promise.all(deferredCalls.map(async call => await call(res))); const tuples = await Promise.all(deferredCalls.map(async call => await call(res)));
@@ -103,7 +109,8 @@ const parseQuery = async (handler, tokens, pos, typeGraph, parent = null) => {
return res; return res;
})); }));
results = results.filter(r => Array.isArray(r) && r.length == 0 ? false : true); results = results.filter(r => !Array.isArray(r) || r.length > 0);
return [results, pos]; return [results, pos];
}; };

View File

@@ -0,0 +1,53 @@
//the authors
let authors = [
{
name: 'J.K. Roaring',
books: [
{ title: 'The Philosepher\'s Kidney Stone' },
{ title: 'The Chamber Pot of Secrets' },
{ title: 'The Prisoner of Aunt Kazban' },
{ title: 'The Goblet of the Fire Cocktail' },
{ title: 'The Order for Kleenex' },
{ title: 'The Half-Priced Pharmacy' },
{ title: 'Yeah, I Got Nothing' },
]
},
{
name: 'Kenneth Grahame',
books: [
{ title: 'The Wind in the Willows', published: '1 April 1908' }
]
},
{
name: 'Kayne Ruse',
books: [
{ title: 'alpha', published: "1" },
{ title: 'beta', published: "2" },
{ title: 'gamma', published: "3" },
{ title: 'delta', published: "4" },
{ title: 'epsilon', published: "5" },
]
},
];
//insert the authors into the books (relationship)
authors = authors.map(a => {
a.books = a.books.map(b => {
b.authors = [a];
return b;
});
return a;
});
//get the books array
let books = [];
authors.forEach(a => books = books.concat(a.books));
//the fake database
module.exports = {
authors,
books,
};

View File

@@ -0,0 +1,180 @@
/* DOCS: parameter types
parent: Type | null
scalars: [{ typeName: String, name: String, filter: any | null }, ...]
matching: Boolean
*/
const database = require('./database.js');
//the handler routines
const handler = {
//type queries
Author: (parent, scalars, matching) => {
console.log("Author", parent ? parent.context : null);
let authors;
//check parentage
if (parent) {
//find the author(s) of the parent Book object
authors = database.authors.filter(author => author.books.some(b => b.title == parent.context.title));
} else {
authors = database.authors;
}
//I am being matched (if true, ALL present scalars will have a filter field)
if (matching) {
//check every scalar to every author - a single false match is a miss on that author
authors = authors.filter(author => {
return scalars.every(scalar => {
//handle each type of scalar
switch (scalar.typeName) {
case 'String':
case 'Integer':
case 'Float':
case 'Boolean':
return author[scalar.name] == scalar.filter; //dumb comparison for now
//custom handling
//NOTE: Only books used the `Date` scalar
default:
throw `Unknown scalar typeName in handler: ${scalar.typeName} (${scalar.name})`;
}
});
});
//if there are no authors left, then the book's filters missed matches
if (authors.length == 0) {
return [];
}
}
//scalars are being matched on their own
if (scalars.some(s => s.filter)) {
//check every scalar to every author - a single match is a hit
authors = authors.filter(author => {
return scalars.some(scalar => {
//handle each type of scalar
switch (scalar.typeName) {
case 'String':
case 'Integer':
case 'Float':
case 'Boolean':
return author[scalar.name] == scalar.filter; //dumb comparison for now
//custom handling
//NOTE: Only books used the `Date` scalar
default:
throw `Unknown scalar typeName in handler: ${scalar.typeName} (${scalar.name})`;
}
});
});
//if there are no authors left, then the book's filters missed matches
if (authors.length == 0) {
return [];
}
}
//process (filter out unwanted fields) and return the array of authors
return authors.map(author => {
let ret = {};
//that's a big O(damn)
scalars.forEach(scalar => {
ret[scalar.name] = author[scalar.name];
});
return ret;
});
},
Book: (parent, scalars, matching) => {
console.log("Book", parent ? parent.context : null);
let books;
//check parentage
if (parent) {
//find the books of the parent author object
books = database.books.filter(book => book.authors.some(a => a.name == parent.context.name));
} else {
books = database.books;
}
//I am being matched (if true, ALL present scalars will have a filter field)
if (matching) {
//check every scalar to every book - a single false match is a miss on that book
books = books.filter(book => {
return scalars.every(scalar => {
//handle each type of scalar
switch (scalar.typeName) {
case 'String':
case 'Integer':
case 'Float':
case 'Boolean':
return book[scalar.name] == scalar.filter; //dumb comparison for now
//custom handling
case 'Date':
return book[scalar.name] == scalar.filter; //could have a more complex comparator function, like date-ranges
default:
throw `Unknown scalar typeName in handler: ${scalar.typeName} (${scalar.name})`;
}
});
});
//if there are no books left, then the authos's filters missed matches
if (books.length == 0) {
return [];
}
}
//scalars are being matched on their own
if (scalars.some(s => s.filter)) {
//check every scalar to every author - a single match is a hit
books = books.filter(author => {
return scalars.some(scalar => {
//handle each type of scalar
switch (scalar.typeName) {
case 'String':
case 'Integer':
case 'Float':
case 'Boolean':
return author[scalar.name] == scalar.filter; //dumb comparison for now
//custom handling
//NOTE: Only books used the `Date` scalar
default:
throw `Unknown scalar typeName in handler: ${scalar.typeName} (${scalar.name})`;
}
});
});
//if there are no authors left, then the book's filters missed matches
if (books.length == 0) {
return [];
}
}
//process (filter out unwanted fields) and return the array of books
return books.map(book => {
let ret = {};
//that's a big O(damn)
scalars.forEach(scalar => {
ret[scalar.name] = book[scalar.name];
});
return ret;
});
},
};
module.exports = handler;

1807
test-server-books/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
module.exports = `
scalar Date
type Author {
String name
Book books
}
type Book {
String title
Date published
Author authors
}
`;

View File

@@ -0,0 +1,27 @@
//express for testing
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const app = express();
app.use(cors());
app.use(bodyParser.text());
//test the library
const schema = require('./schema.js');
const handler = require('./handler.js');
const sineQL = require('../source/index.js');
const sine = sineQL(schema, handler, { debug: false });
//open the end
app.post('/sineql', async (req, res) => {
const [code, result] = await sine(req.body);
res.status(code).send(result);
});
//startup
app.listen(process.env.WEB_PORT || 3100, err => {
console.log(`listening to *:${process.env.WEB_PORT || 3100}`);
});

View File

@@ -0,0 +1,20 @@
{
"name": "test-server",
"version": "0.2.1",
"description": "",
"main": "server.js",
"scripts": {
"start": "pm2 start server.js",
"restart": "pm2 restart server.js",
"stop": "pm2 stop server.js",
"list": "pm2 list",
"node": "node server.js"
},
"author": "Kayne Ruse",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.17.1",
"pm2": "^4.4.1"
}
}