From eec7b059b6e04e1f0fc4808738dc6927b30b4bf4 Mon Sep 17 00:00:00 2001 From: Kayne Ruse Date: Tue, 22 Feb 2022 13:29:41 +0000 Subject: [PATCH] Delete implemented --- package-lock.json | 4 +- package.json | 2 +- source/index.js | 24 +++-- source/parse-delete-tree.js | 161 +++++++++++++++++++++++++++++++++ test/parse-delete-tree.test.js | 111 +++++++++++++++++++++++ 5 files changed, 292 insertions(+), 10 deletions(-) create mode 100644 source/parse-delete-tree.js create mode 100644 test/parse-delete-tree.test.js diff --git a/package-lock.json b/package-lock.json index baac9c9..d13bc38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sineql", - "version": "0.4.0", + "version": "0.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "sineql", - "version": "0.4.0", + "version": "0.5.0", "license": "ISC", "devDependencies": { "jest": "^27.5.1" diff --git a/package.json b/package.json index 2fe711e..d81aa2e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sineql", - "version": "0.4.1", + "version": "0.5.0", "description": "A simple to use graphQL clone", "main": "source/index.js", "scripts": { diff --git a/source/index.js b/source/index.js index 46762f8..90d848a 100644 --- a/source/index.js +++ b/source/index.js @@ -55,9 +55,19 @@ const sineQL = (schema, { queryHandlers, createHandlers }, options = {}) => { return [200, result]; case 'delete': - return [501, 'Keyword not yet implemented: ' + tokens[0]]; - //TODO: implement these keywords - break; + if (!deleteHandlers) { + return [501, 'Delete handlers not implemented']; + } + + if (!deleteHandlers[tokens[1]]) { + throw `Delete handler not implemented for that type: ${tokens[1]}`; + } + + const deleteTree = parseDeleteTree(tokens, typeGraph, options); + + const result = await deleteHandlers[tokens[1]](deleteTree, typeGraph); + + return [200, result]; //no leading keyword - regular query default: { @@ -65,12 +75,12 @@ const sineQL = (schema, { queryHandlers, createHandlers }, options = {}) => { return [501, 'Query handlers not implemented']; } - const queryTree = parseQueryTree(tokens, typeGraph, options); - - if (!queryHandlers[queryTree.typeName]) { - throw `Query handler not implemented for that type: ${queryTree.typeName}`; + if (!queryHandlers[tokens[0]]) { + throw `Query handler not implemented for that type: ${tokens[0]}`; } + const queryTree = parseQueryTree(tokens, typeGraph, options); + const result = await queryHandlers[queryTree.typeName](queryTree, typeGraph); if (options.debug) { diff --git a/source/parse-delete-tree.js b/source/parse-delete-tree.js new file mode 100644 index 0000000..b26e676 --- /dev/null +++ b/source/parse-delete-tree.js @@ -0,0 +1,161 @@ +//build the tokens into a single object of types representing the initial query +const parseDeleteTree = (tokens, typeGraph, options = {}) => { + let current = 1; //primed + + //check this is a update command + if (tokens[current - 1] != 'delete') { + throw 'Expected delete keyword at the beginning of delete command'; + } + + current++; + + //get a token that matches a type + if (!typeGraph[tokens[current - 1]]) { + throw `Expected a type in the type graph (found ${tokens[current - 1]})`; + } + + //check that there are the correct number of '{' and '}' + if (tokens.reduce((running, tok) => tok == '{' ? running + 1 : tok == '}' ? running - 1 : running, 0) != 0) { + throw `Unequal number of '{' and '}' found`; + } + + //check that there are the correct number of '[' and ']' + if (tokens.reduce((running, tok) => tok == '[' ? running + 1 : tok == ']' ? running - 1 : running, 0) != 0) { + throw `Unequal number of '[' and ']' found`; + } + + //the return + const result = []; + const type = tokens[current - 1]; + + if (tokens[current] == '[') { + current++; + } + + do { + //read the block of lines + const [block, pos] = readBlock(tokens, current, type, typeGraph, options); + + //insert the typename into the top-level block + block['typeName'] = type; + + //insert update into the top-level block + block['delete'] = true; + + current = pos; + + result.push(block); + } while (tokens[current] && tokens[current] != ']'); + + //finally + return result; +}; + +const readBlock = (tokens, current, superType, typeGraph, options) => { + //scan the '{' + if (tokens[current++] != '{') { + throw `Expected '{' at beginning of a block (found ${tokens[current - 1]})`; + } + + //result + const result = {}; + + //scan each "line" in this block + while(tokens[current++] && tokens[current - 1] != '}') { + //check for block-level keywords (modifiers need to form a chain from the leaf) + 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 not present in the type + if (!typeGraph[superType][fieldName]) { + throw `Unexpected field name ${fieldName} in type ${superType}`; + } + + //if the field is non-scalar, read the sub-block + if (!typeGraph[typeGraph[superType][fieldName].typeName].scalar) { + if (tokens[current] == '[') { + current++; + } + + do { + //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 the unique modifier if it's set + block['unique'] = typeGraph[superType][fieldName].unique; + + //insert the block-level modifier signal + if (modifier) { + block[modifier] = true; + } else { + throw `Modifier expected for ${fieldName} (match)`; + } + + //insert into result + result[fieldName] = result[fieldName] || []; + result[fieldName].push(block); + + current = pos; //pos points past the end of the block + + if (options.debug) { + console.log(`${fieldName}:`); + console.dir(result[fieldName], { depth: null }); + } + } while (tokens[current] && tokens[current] == '{'); + current++; + + continue; + } + + //scalar + else { + //save the typeGraph type into result + result[fieldName] = JSON.parse(JSON.stringify( typeGraph[ typeGraph[superType][fieldName].typeName ] )); + + //insert the unique modifier if it's set + result[fieldName]['unique'] = typeGraph[superType][fieldName].unique; + + //insert the block-level modifier signal + if (modifier) { + //handle integers and floats exposed to the user + switch(result[fieldName].typeName) { + case 'Integer': + result[fieldName][modifier] = parseInt(tokens[current++]); + break; + + case 'Float': + result[fieldName][modifier] = parseFloat(tokens[current++]); + break; + + default: //everything else is a string (including booleans) + result[fieldName][modifier] = tokens[current++]; + } + } else { + throw `Modifier expected for ${fieldName} (match)`; + } + + if (options.debug) { + console.log(`${fieldName}: `, result[fieldName]); + } + + continue; + } + } + + return [result, current]; +}; + +module.exports = parseDeleteTree; \ No newline at end of file diff --git a/test/parse-delete-tree.test.js b/test/parse-delete-tree.test.js new file mode 100644 index 0000000..ed1011e --- /dev/null +++ b/test/parse-delete-tree.test.js @@ -0,0 +1,111 @@ +const buildTypeGraph = require('../source/build-type-graph'); +const parseInput = require('../source/parse-input'); +const parseDeleteTree = require('../source/parse-delete-tree'); + +//schemas +const simpleSchema = ` + +scalar Date + +type Book { + unique String title + Date published + Float rating +} + +type Author { + unique String name + Book books +} + +`; + +const simpleBookQuery = ` + +delete Book { + match title "The Wind in the Willows" +} + +`; + +const compoundBookQuery = ` + +delete Book [ + { + match title "The Philosopher's Kidney Stone" + } + { + match title "The Chamber Pot of Secrets" + } + { + match title "The Prisoner of Aunt Kazban" + } + { + match title "The Goblet of the Fire Cocktail" + } + { + match title "The Order for Kleenex" + } + { + match title "The Half-Priced Pharmacy" + } + { + match title "Yeah, I Got Nothing" + } +] + +`; + +const multipleFieldsQuery = ` + +delete Book { + match title "The Wind in the Willows" + match published "1908-04-01" +} + +`; + +//do stuff +test('parseDeleteTree - delete a single book', () => { + //setup + const tokens = parseInput(simpleBookQuery, true); + const graph = buildTypeGraph(simpleSchema); + + //process + const deleteTree = parseDeleteTree(tokens, graph); + + //inspect + expect(deleteTree.length).toEqual(1); + expect(deleteTree[0].delete).toEqual(true); + expect(deleteTree[0].title.match).toEqual('The Wind in the Willows'); +}); + +test('parseDeleteTree - delete multiple books', () => { + //setup + const tokens = parseInput(compoundBookQuery, true); + const graph = buildTypeGraph(simpleSchema); + + //process + const deleteTree = parseDeleteTree(tokens, graph); + + //inspect + expect(deleteTree.length).toEqual(7); + expect(deleteTree[0].delete).toEqual(true); + expect(deleteTree[0].title.match).toEqual('The Philosopher\'s Kidney Stone'); +}); + +test('parseDeleteTree - delete a book based on multiple match conditions', () => { + //setup + const tokens = parseInput(multipleFieldsQuery, true); + const graph = buildTypeGraph(simpleSchema); + + //process + const deleteTree = parseDeleteTree(tokens, graph); + + //inspect + expect(deleteTree.length).toEqual(1); + expect(deleteTree[0].delete).toEqual(true); + expect(deleteTree[0].title.match).toEqual('The Wind in the Willows'); + expect(deleteTree[0].published.match).toEqual('1908-04-01'); +}); +