mirror of
https://github.com/Ratstail91/sineQL.git
synced 2025-11-29 02:34:28 +11:00
Started work on create keyword - I need unique as a keyword first
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
const buildTypeGraph = require('./build-type-graph');
|
const buildTypeGraph = require('./build-type-graph');
|
||||||
const parseInput = require('./parse-input');
|
const parseInput = require('./parse-input');
|
||||||
const parseQueryTree = require('./parse-query-tree');
|
const parseQueryTree = require('./parse-query-tree');
|
||||||
|
const parseCreateTree = require('./parse-create-tree');
|
||||||
|
|
||||||
//the main function to be returned (sineQL())
|
//the main function to be returned (sineQL())
|
||||||
const sineQL = (schema, { queryHandlers }, options = {}) => {
|
const sineQL = (schema, { queryHandlers, createHandlers }, options = {}) => {
|
||||||
let typeGraph;
|
let typeGraph;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -23,6 +24,25 @@ const sineQL = (schema, { queryHandlers }, options = {}) => {
|
|||||||
switch(tokens[0]) {
|
switch(tokens[0]) {
|
||||||
//check for leading keywords
|
//check for leading keywords
|
||||||
case 'create':
|
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 'update':
|
||||||
case 'delete':
|
case 'delete':
|
||||||
return [501, 'Keyword not implemented: ' + tokens[0]];
|
return [501, 'Keyword not implemented: ' + tokens[0]];
|
||||||
|
|||||||
142
source/parse-create-tree.js
Normal file
142
source/parse-create-tree.js
Normal file
@@ -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;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
const { Op } = require('../database');
|
const { Op } = require('sequelize');
|
||||||
const { books, authors } = require('../database/models');
|
const { books, authors } = require('../database/models');
|
||||||
|
|
||||||
//TODO: 'unique' may be a useful modifier, but not at this stage of development
|
//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
|
//'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
|
//'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 (query for compounds, create if not found)
|
||||||
|
|
||||||
//if no modifiers are specified, 'set' is used as a fallback
|
|
||||||
|
|
||||||
/* possible create requests include:
|
/* possible create requests include:
|
||||||
|
|
||||||
create Author {
|
create Author {
|
||||||
create name "Sydney Sheldon"
|
name "Sydney Sheldon"
|
||||||
create books [
|
create books [
|
||||||
{
|
{
|
||||||
create title "The Naked Face"
|
title "The Naked Face"
|
||||||
set published 1970
|
published 1970
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
create title "A Stranger in the Mirror"
|
title "A Stranger in the Mirror"
|
||||||
set published 1976
|
published 1976
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
create Author {
|
create Author {
|
||||||
match name "Sydney Sheldon"
|
name "Sydney Sheldon"
|
||||||
create books {
|
create books {
|
||||||
create title "Bloodline"
|
title "Bloodline"
|
||||||
published 1977
|
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 = {
|
const createHandlers = {
|
||||||
//complex compound
|
//complex compound
|
||||||
Author: async (create, graph) => {
|
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
|
//simple compound
|
||||||
Book: async (create, graph) => {
|
Book: async (create, graph) => {
|
||||||
//
|
//TODO: incomplete
|
||||||
|
console.log('-----', create)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
modules.exports = createHandlers;
|
module.exports = createHandlers;
|
||||||
@@ -5,18 +5,20 @@ const sequelize = require('./database');
|
|||||||
const { books, authors } = require('./database/models');
|
const { books, authors } = require('./database/models');
|
||||||
|
|
||||||
//create the dummy data
|
//create the dummy data
|
||||||
sequelize.sync().then(() => {
|
sequelize.sync().then(async () => {
|
||||||
//*
|
//*
|
||||||
sequelize.query('DELETE FROM authors;');
|
return; //DEBUG: delete this for debugging
|
||||||
sequelize.query('DELETE FROM books;');
|
|
||||||
|
|
||||||
authors.bulkCreate([
|
await sequelize.query('DELETE FROM authors;');
|
||||||
|
await sequelize.query('DELETE FROM books;');
|
||||||
|
|
||||||
|
await authors.bulkCreate([
|
||||||
{ id: 1, name: 'Diana Gabaldon' },
|
{ id: 1, name: 'Diana Gabaldon' },
|
||||||
{ id: 2, name: 'Emily Rodda' },
|
{ id: 2, name: 'Emily Rodda' },
|
||||||
{ id: 3, name: 'Kenneth Grahame' }
|
{ id: 3, name: 'Kenneth Grahame' }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
books.bulkCreate([
|
await books.bulkCreate([
|
||||||
{ id: 1, authorId: 1, title: 'Outlander', published: '1991', rating: 9.5 },
|
{ 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: 2, authorId: 1, title: 'Dragonfly in Amber', published: '1992', rating: 9.5 },
|
||||||
{ id: 3, authorId: 1, title: 'Voyager', published: '1993', 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
|
//the arguments to the library
|
||||||
const schema = require('./handlers/schema');
|
const schema = require('./handlers/schema');
|
||||||
const queryHandlers = require('./handlers/query-handlers');
|
const queryHandlers = require('./handlers/query-handlers');
|
||||||
|
const createHandlers = require('./handlers/create-handlers');
|
||||||
|
|
||||||
//run the setup function to create the closure (creates the type graph)
|
//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
|
//actually ask the question
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user