mirror of
https://github.com/Ratstail91/sineQL.git
synced 2025-11-29 02:34:28 +11:00
Adjusted handler API
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
53
test-server-books/database.js
Normal file
53
test-server-books/database.js
Normal 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,
|
||||||
|
};
|
||||||
180
test-server-books/handler.js
Normal file
180
test-server-books/handler.js
Normal 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
1807
test-server-books/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
test-server-books/schema.js
Normal file
14
test-server-books/schema.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
module.exports = `
|
||||||
|
scalar Date
|
||||||
|
|
||||||
|
type Author {
|
||||||
|
String name
|
||||||
|
Book books
|
||||||
|
}
|
||||||
|
|
||||||
|
type Book {
|
||||||
|
String title
|
||||||
|
Date published
|
||||||
|
Author authors
|
||||||
|
}
|
||||||
|
`;
|
||||||
27
test-server-books/server.js
Normal file
27
test-server-books/server.js
Normal 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}`);
|
||||||
|
});
|
||||||
20
test-server-pokemon/package.json
Normal file
20
test-server-pokemon/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user