From f4b330eac3a6417e85ee56bc95e0a341795c8c72 Mon Sep 17 00:00:00 2001 From: Kayne Ruse Date: Tue, 6 Apr 2021 04:36:53 +1000 Subject: [PATCH] Started work on create keyword - I need unique as a keyword first --- source/index.js | 22 ++++- source/parse-create-tree.js | 142 +++++++++++++++++++++++++++++++ test/handlers/create-handlers.js | 120 ++++++++++++++++++++++---- test/index.js | 15 ++-- 4 files changed, 277 insertions(+), 22 deletions(-) create mode 100644 source/parse-create-tree.js diff --git a/source/index.js b/source/index.js index 2ffaa86..51c99e2 100644 --- a/source/index.js +++ b/source/index.js @@ -1,9 +1,10 @@ const buildTypeGraph = require('./build-type-graph'); const parseInput = require('./parse-input'); const parseQueryTree = require('./parse-query-tree'); +const parseCreateTree = require('./parse-create-tree'); //the main function to be returned (sineQL()) -const sineQL = (schema, { queryHandlers }, options = {}) => { +const sineQL = (schema, { queryHandlers, createHandlers }, options = {}) => { let typeGraph; try { @@ -23,6 +24,25 @@ const sineQL = (schema, { queryHandlers }, options = {}) => { switch(tokens[0]) { //check for leading keywords case 'create': + if (!createHandlers) { + return [501, 'Create handlers not implemented']; + } + + if (!createHandlers[tokens[1]]) { + throw `Create handler not implemented for that type: ${tokens[1]}`; + } + + const createTree = parseCreateTree(tokens, typeGraph, options); + + const result = await createHandlers[tokens[1]](createTree, typeGraph); + + if (options.debug) { + console.log('Create tree results:'); + console.dir(result, { depth: null }); + } + + return [200, result]; + case 'update': case 'delete': return [501, 'Keyword not implemented: ' + tokens[0]]; diff --git a/source/parse-create-tree.js b/source/parse-create-tree.js new file mode 100644 index 0000000..44d030a --- /dev/null +++ b/source/parse-create-tree.js @@ -0,0 +1,142 @@ +//build the tokens into a single object of types representing the initial query +const parseCreateTree = (tokens, typeGraph, options) => { + let current = 1; //primed + + //check this is a create command + if (tokens[current - 1] != 'create') { + throw 'Expected create keyword at the beginning of create 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 = []; + + if (tokens[current] == '[') { + current++; + } + + do { + //read the block of lines + const [block, pos] = readBlock(tokens, current, tokens[current - 1], typeGraph, options); + + //insert the typename into the top-level block + block['typeName'] = tokens[current - 1]; + + //insert create into the top-level block + block['create'] = 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 (['create', '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 block-level modifier signal + if (modifier) { + block[modifier] = true; + } else { + throw `Modifier expected for ${fieldName} (either create 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 block-level modifier signal + if (modifier) { + result[fieldName][modifier] = tokens[current++]; + } else { + result[fieldName]['set'] = tokens[current++]; + } + + if (options.debug) { + console.log(`${fieldName}: `, result[fieldName]); + } + + continue; + } + } + + return [result, current]; +}; + +module.exports = parseCreateTree; \ No newline at end of file diff --git a/test/handlers/create-handlers.js b/test/handlers/create-handlers.js index f739bcf..b82bb46 100644 --- a/test/handlers/create-handlers.js +++ b/test/handlers/create-handlers.js @@ -1,4 +1,4 @@ -const { Op } = require('../database'); +const { Op } = require('sequelize'); const { books, authors } = require('../database/models'); //TODO: 'unique' may be a useful modifier, but not at this stage of development @@ -10,46 +10,136 @@ const { books, authors } = require('../database/models'); //'create' also counts as a modifier, indicating that a specific value is new to the database, and returning an error if it exists already OR //'match' is used when an existing value must already exist in the database, and returning an error if it does not OR -//'set' is used when an existing value may or may not already exist in the database; first it queries, then if it fails to find, creates - -//if no modifiers are specified, 'set' is used as a fallback +//if no modifiers are specified, 'set' is used as a fallback (query for compounds, create if not found) /* possible create requests include: create Author { - create name "Sydney Sheldon" + name "Sydney Sheldon" create books [ { - create title "The Naked Face" - set published 1970 + title "The Naked Face" + published 1970 } { - create title "A Stranger in the Mirror" - set published 1976 + title "A Stranger in the Mirror" + published 1976 } ] } -create Author { - match name "Sydney Sheldon" +create Author { + name "Sydney Sheldon" create books { - create title "Bloodline" + title "Bloodline" published 1977 } } */ +/* all create arguments look like this: + +//Author array +[{ + typeName: 'Author', + name: { typeName: 'String', scalar: true, create: 'Sydney Sheldon' } + books: [{ + typeName: 'Book', + create: true, + title: { typeName: 'String', scalar: true, create: 'Bloodline' } + published: { typeName: 'Date', scalar: true, set: 1977 } + }, ...] +}, +...] + +*/ + +//higher level elements need to pass their IDs to sub-elements const createHandlers = { //complex compound Author: async (create, graph) => { - // + //apply the following to an array of authors + const promises = create.map(async author => { + //get the fields alone + const { typeName, create, match, set, ...fields } = author; + + //if we are creating a new element (default with Author as a top-level only type) + if (create) { + //check the created scalar fields (value must not exist in the database yet) + const createdOrs = Object.keys(fields).filter(field => fields[field].scalar && fields[field].create).map(field => { return { [field]: fields[field].create }; }); + + const createdFound = await authors.findOne({ + where: { + [Op.or]: createdOrs + }, + }); + + if (createdFound) { + //enter error state + Object.keys(fields).forEach(field => { + if (fields[field].create == createdFound[field]) { + throw `Cannot create Author ${field} with value ${fields[field].create} (value already exists)`; + } + }); + + //no error field found, continue? + } + + //create the element + const args = {}; + Object.keys(fields).filter(field => fields[field].scalar).forEach(field => args[field] = fields[field].create || fields[field].set); + const createdAuthor = await authors.create(args); + + //pass on to the books + Object.keys(fields).filter(field => !fields[field].scalar).forEach(nonScalar => fields[nonScalar].forEach(element => element.authorId = createdAuthor.id)); + Object.keys(fields).filter(field => !fields[field].scalar).forEach(nonScalar => { + //delegation + createHandlers[graph[typeName][nonScalar].typeName](fields[nonScalar], graph); + }); + } + + //if we are matching an existing element + else if (match) { + //check the matched scalar fields (value must exist in the database) + const matchedAnds = Object.keys(fields).filter(field => fields[field].scalar && fields[field].match).map(field => { return { [field]: fields[field].match }; }); + + //these only match one + const matchedFound = await authors.findOne({ + where: { + [Op.and]: matchedAnds + } + }); + + if (!matchedFound) { + throw `Cannot match Author (no match exists)`; + } + + //pass on to the books + Object.keys(fields).filter(field => !fields[field].scalar).forEach(nonScalar => fields[nonScalar].forEach(element => element.authorId = matchedAuthor.id)); + Object.keys(fields).filter(field => !fields[field].scalar).forEach(nonScalar => { + //delegation + createHandlers[graph[typeName][nonScalar].typeName](fields[nonScalar], graph); + }); + } + + //get the remaining scalar fields without create or match ('set' by default), not used - just an error + else if (set) { + throw 'Set not implemented for create Author'; + } + }); + + //handle promises + await Promise.all(promises).catch(e => console.error(e)); + + return null; }, //simple compound Book: async (create, graph) => { - // + //TODO: incomplete + console.log('-----', create) } }; -modules.exports = createHandlers; \ No newline at end of file +module.exports = createHandlers; \ No newline at end of file diff --git a/test/index.js b/test/index.js index 891fc89..8370239 100644 --- a/test/index.js +++ b/test/index.js @@ -5,18 +5,20 @@ const sequelize = require('./database'); const { books, authors } = require('./database/models'); //create the dummy data -sequelize.sync().then(() => { +sequelize.sync().then(async () => { //* - sequelize.query('DELETE FROM authors;'); - sequelize.query('DELETE FROM books;'); + return; //DEBUG: delete this for debugging - authors.bulkCreate([ + await sequelize.query('DELETE FROM authors;'); + await sequelize.query('DELETE FROM books;'); + + await authors.bulkCreate([ { id: 1, name: 'Diana Gabaldon' }, { id: 2, name: 'Emily Rodda' }, { id: 3, name: 'Kenneth Grahame' } ]); - books.bulkCreate([ + await books.bulkCreate([ { id: 1, authorId: 1, title: 'Outlander', published: '1991', rating: 9.5 }, { id: 2, authorId: 1, title: 'Dragonfly in Amber', published: '1992', rating: 9.5 }, { id: 3, authorId: 1, title: 'Voyager', published: '1993', rating: 9.5 }, @@ -67,9 +69,10 @@ const sineQL = require('../source/index.js'); //the arguments to the library const schema = require('./handlers/schema'); const queryHandlers = require('./handlers/query-handlers'); +const createHandlers = require('./handlers/create-handlers'); //run the setup function to create the closure (creates the type graph) -const sine = sineQL(schema, { queryHandlers }, { debug: false }); +const sine = sineQL(schema, { queryHandlers, /* createHandlers */ }, { debug: true }); //actually ask the question (async () => {