From b790338b541dd1f3701033984208ee91f3c0b727 Mon Sep 17 00:00:00 2001 From: Kayne Ruse Date: Tue, 22 Feb 2022 13:14:05 +0000 Subject: [PATCH] Implemented update --- README.md | 21 ++--- source/index.js | 15 +++ source/keywords.json | 2 +- source/parse-create-tree.js | 16 +++- source/parse-query-tree.js | 15 ++- source/parse-update-tree.js | 161 +++++++++++++++++++++++++++++++++ test/parse-create-tree.test.js | 2 +- test/parse-update-tree.test.js | 160 ++++++++++++++++++++++++++++++++ 8 files changed, 376 insertions(+), 16 deletions(-) create mode 100644 source/parse-update-tree.js create mode 100644 test/parse-update-tree.test.js diff --git a/README.md b/README.md index 8fc9a6e..5350558 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,6 @@ The fields can be altered as well, using the query language's built-in keywords: * update * delete * match -* set * typeName (this is not used in either language, but rather is used internally) `create`, `update` and `delete` are still to be defined properly, but they'll probably work as follows. @@ -202,19 +201,19 @@ create Book [ ### Update -When using `update`, `match` will find all existing records and update those using the `set` keyword: +When using `update`, `match` will find all existing records and update those using the `update` keyword: ``` update Book { match title "The Wind in the Willows" - set published "1908-4-1" + update published "1908-04-01" } ``` ``` update Book { match title "The Wind in the Willows" - set title "The Fart in the Fronds" + update title "The Fart in the Fronds" } ``` @@ -224,31 +223,31 @@ You can run multiple updates at once by surrounding them with `[]`: update Book [ { match title "The Philosopher's Kidney Stone" - set published "1997-06-26" + update published "1997-06-26" } { match title "The Chamber Pot of Secrets" - set published "1998-07-02" + update published "1998-07-02" } { match title "The Prisoner of Aunt Kazban" - set published "1999-07-08" + update published "1999-07-08" } { match title "The Goblet of the Fire Cocktail" - set published "2000-07-08" + update published "2000-07-08" } { match title "The Order for Kleenex" - set published "2003-06-21" + update published "2003-06-21" } { match title "The Half-Priced Pharmacy" - set published "2005-07-16" + update published "2005-07-16" } { match title "Yeah, I Got Nothing" - set published "2007-07-21" + update published "2007-07-21" } ] ``` diff --git a/source/index.js b/source/index.js index 4983720..46762f8 100644 --- a/source/index.js +++ b/source/index.js @@ -2,6 +2,7 @@ const buildTypeGraph = require('./build-type-graph'); const parseInput = require('./parse-input'); const parseQueryTree = require('./parse-query-tree'); const parseCreateTree = require('./parse-create-tree'); +const parseUpdateTree = require('./parse-update-tree'); //the main function to be returned (sineQL()) const sineQL = (schema, { queryHandlers, createHandlers }, options = {}) => { @@ -39,6 +40,20 @@ const sineQL = (schema, { queryHandlers, createHandlers }, options = {}) => { return [200, result]; case 'update': + if (!updateHandlers) { + return [501, 'Update handlers not implemented']; + } + + if (!updateHandlers[tokens[1]]) { + throw `Update handler not implemented for that type: ${tokens[1]}`; + } + + const updateTree = parseUpdateTree(tokens, typeGraph, options); + + const result = await updateHandlers[tokens[1]](updateTree, typeGraph); + + return [200, result]; + case 'delete': return [501, 'Keyword not yet implemented: ' + tokens[0]]; //TODO: implement these keywords diff --git a/source/keywords.json b/source/keywords.json index 76ed8ee..e08f1b0 100644 --- a/source/keywords.json +++ b/source/keywords.json @@ -1 +1 @@ -["type", "scalar", "create", "update", "delete", "set", "match", "unique", "typeName"] \ No newline at end of file +["type", "scalar", "create", "update", "delete", "match", "unique", "typeName"] \ No newline at end of file diff --git a/source/parse-create-tree.js b/source/parse-create-tree.js index 00ebd4d..8699030 100644 --- a/source/parse-create-tree.js +++ b/source/parse-create-tree.js @@ -130,9 +130,21 @@ const readBlock = (tokens, current, superType, typeGraph, options) => { //insert the block-level modifier signal if (modifier) { - result[fieldName][modifier] = tokens[current++]; + //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 { - result[fieldName]['set'] = tokens[current++]; + throw `Modifier expected for ${fieldName} (either create or match)`; } if (options.debug) { diff --git a/source/parse-query-tree.js b/source/parse-query-tree.js index b06a7b5..64c6679 100644 --- a/source/parse-query-tree.js +++ b/source/parse-query-tree.js @@ -91,8 +91,21 @@ const readBlock = (tokens, current, superType, typeGraph, options) => { //insert the block-level modifier signal if (modifier) { - result[fieldName][modifier] = tokens[current++]; + //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++]; + } } + //no else-clause, since queries don't require modifiers if (options.debug) { console.log(`${fieldName}: `, result[fieldName]); diff --git a/source/parse-update-tree.js b/source/parse-update-tree.js new file mode 100644 index 0000000..516d68d --- /dev/null +++ b/source/parse-update-tree.js @@ -0,0 +1,161 @@ +//build the tokens into a single object of types representing the initial query +const parseUpdateTree = (tokens, typeGraph, options = {}) => { + let current = 1; //primed + + //check this is a update command + if (tokens[current - 1] != 'update') { + throw 'Expected update keyword at the beginning of update 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['update'] = 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 (['update', '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} (either update or 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} (either update or match)`; + } + + if (options.debug) { + console.log(`${fieldName}: `, result[fieldName]); + } + + continue; + } + } + + return [result, current]; +}; + +module.exports = parseUpdateTree; \ No newline at end of file diff --git a/test/parse-create-tree.test.js b/test/parse-create-tree.test.js index c74da9b..616cb88 100644 --- a/test/parse-create-tree.test.js +++ b/test/parse-create-tree.test.js @@ -24,7 +24,7 @@ const simpleBookQuery = ` create Book { create title "The Wind in the Willows" - create published "1908" + create published "1908-04-01" create rating 9.5 } diff --git a/test/parse-update-tree.test.js b/test/parse-update-tree.test.js new file mode 100644 index 0000000..615525a --- /dev/null +++ b/test/parse-update-tree.test.js @@ -0,0 +1,160 @@ +const buildTypeGraph = require('../source/build-type-graph'); +const parseInput = require('../source/parse-input'); +const parseUpdateTree = require('../source/parse-update-tree'); + +//schemas +const simpleSchema = ` + +scalar Date + +type Book { + unique String title + Date published + Float rating +} + +type Author { + unique String name + Book books +} + +`; + +const simpleBookQuery = ` + +update Book { + match title "The Wind in the Willows" + update rating 9.5 +} + +`; + +const compoundBookQuery = ` + +update Book [ + { + match title "The Philosopher's Kidney Stone" + update published "1997-06-26" + } + { + match title "The Chamber Pot of Secrets" + update published "1998-07-02" + } + { + match title "The Prisoner of Aunt Kazban" + update published "1999-07-08" + } + { + match title "The Goblet of the Fire Cocktail" + update published "2000-07-08" + } + { + match title "The Order for Kleenex" + update published "2003-06-21" + } + { + match title "The Half-Priced Pharmacy" + update published "2005-07-16" + } + { + match title "Yeah, I Got Nothing" + update published "2007-07-21" + } +] + +`; + +const simpleAuthorQuery = ` + +update Author { + match name "Kenneth Grahame" + update books { + match title "The Wind in the Willows" + } +} + +`; + +const compoundAuthorQuery = ` + +update Author { + match name "J. K. Rolling" + update books [ + { 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" } + ] +} + +`; + +//do stuff +test('parseUpdateTree - update a single book', () => { + //setup + const tokens = parseInput(simpleBookQuery, true); + const graph = buildTypeGraph(simpleSchema); + + //process + const updateTree = parseUpdateTree(tokens, graph); + + //inspect + expect(updateTree.length).toEqual(1); + expect(updateTree[0].update).toEqual(true); + expect(updateTree[0].title.match).toEqual('The Wind in the Willows'); + expect(updateTree[0].rating.update).toEqual(9.5); +}); + +test('parseUpdateTree - update multiple books', () => { + //setup + const tokens = parseInput(compoundBookQuery, true); + const graph = buildTypeGraph(simpleSchema); + + //process + const updateTree = parseUpdateTree(tokens, graph); + + //inspect + expect(updateTree.length).toEqual(7); + expect(updateTree[0].update).toEqual(true); + expect(updateTree[0].title.match).toEqual('The Philosopher\'s Kidney Stone'); + expect(updateTree[0].published.update).toEqual('1997-06-26'); +}); + +test('parseUpdateTree - single join', () => { + //setup + const tokens = parseInput(simpleAuthorQuery, true); + const graph = buildTypeGraph(simpleSchema); + + //process + const updateTree = parseUpdateTree(tokens, graph); + + //inspect + expect(updateTree.length).toEqual(1); + expect(updateTree[0].typeName).toEqual('Author'); + expect(updateTree[0].update).toEqual(true); + expect(updateTree[0].name.match).toEqual('Kenneth Grahame'); + expect(updateTree[0].books.length).toEqual(1); + expect(updateTree[0].books[0].title.match).toEqual('The Wind in the Willows'); +}); + +test('parseUpdateTree - multiple join', () => { + //setup + const tokens = parseInput(compoundAuthorQuery, true); + const graph = buildTypeGraph(simpleSchema); + + //process + const updateTree = parseUpdateTree(tokens, graph); + + //inspect + expect(updateTree.length).toEqual(1); + expect(updateTree[0].typeName).toEqual('Author'); + expect(updateTree[0].update).toEqual(true); + expect(updateTree[0].name.match).toEqual('J. K. Rolling'); + expect(updateTree[0].books.length).toEqual(7); + expect(updateTree[0].books[0].title.match).toEqual('The Philosopher\'s Kidney Stone'); + expect(updateTree[0].books[6].title.match).toEqual('Yeah, I Got Nothing'); +}); +