├── .travis.yml ├── public └── images │ └── favicon.ico ├── lib ├── error-handler.js ├── route-loader.js └── monetdb-interface.js ├── routes ├── index.js ├── _database │ ├── index.js │ ├── _document │ │ ├── index.js │ │ └── find.js │ ├── _schema │ │ ├── index.js │ │ ├── _table │ │ │ ├── index.js │ │ │ └── table-operations.js │ │ ├── table.js │ │ └── view.js │ ├── function.js │ ├── query.js │ ├── schema.js │ └── document.js └── database.js ├── .gitignore ├── package.json ├── server.js ├── TUTORIAL.md ├── README.md └── LICENSE.md /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "0.12" 5 | -------------------------------------------------------------------------------- /public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MonetDB/monetdb-rest/HEAD/public/images/favicon.ico -------------------------------------------------------------------------------- /lib/error-handler.js: -------------------------------------------------------------------------------- 1 | exports.logAndReturnError = function(res, err) { 2 | throw err; 3 | res.status(500).send(err.message); 4 | }; 5 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | // Load `*.js` under current directory as properties 2 | require('fs').readdirSync(__dirname + '/').forEach(function(file) { 3 | if (file.match(/.+\.js/g) !== null && file !== 'index.js') { 4 | var name = file.replace('.js', ''); 5 | exports[name] = require('./' + file); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /routes/_database/index.js: -------------------------------------------------------------------------------- 1 | // Load `*.js` under current directory as properties 2 | require('fs').readdirSync(__dirname + '/').forEach(function(file) { 3 | if (file.match(/.+\.js/g) !== null && file !== 'index.js') { 4 | var name = file.replace('.js', ''); 5 | exports[name] = require('./' + file); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /routes/_database/_document/index.js: -------------------------------------------------------------------------------- 1 | // Load `*.js` under current directory as properties 2 | require('fs').readdirSync(__dirname + '/').forEach(function(file) { 3 | if (file.match(/.+\.js/g) !== null && file !== 'index.js') { 4 | var name = file.replace('.js', ''); 5 | exports[name] = require('./' + file); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /routes/_database/_schema/index.js: -------------------------------------------------------------------------------- 1 | // Load `*.js` under current directory as properties 2 | require('fs').readdirSync(__dirname + '/').forEach(function(file) { 3 | if (file.match(/.+\.js/g) !== null && file !== 'index.js') { 4 | var name = file.replace('.js', ''); 5 | exports[name] = require('./' + file); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /routes/_database/_schema/_table/index.js: -------------------------------------------------------------------------------- 1 | // Load `*.js` under current directory as properties 2 | require('fs').readdirSync(__dirname + '/').forEach(function(file) { 3 | if (file.match(/.+\.js/g) !== null && file !== 'index.js') { 4 | var name = file.replace('.js', ''); 5 | exports[name] = require('./' + file); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /lib/route-loader.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | 3 | /** 4 | * Loads all the files indexed (by index.js) the given directory and adds them as routes to the router. 5 | * 6 | * @param path Location from which all files will be loaded as routes 7 | * @param router Router to add the new routes to 8 | * @param options Configuration options 9 | * @returns {Array} The routes as an array 10 | */ 11 | exports.addRoutes = function(path, router, options) { 12 | // Add the routes in the directory 13 | var routes = require(path); 14 | 15 | // Loop over all keys to pass the router 16 | var allRoutes = Object.keys(routes); 17 | allRoutes.forEach(function(key) { 18 | routes[key](router, options); 19 | }); 20 | 21 | // Return the array 22 | return allRoutes; 23 | }; 24 | -------------------------------------------------------------------------------- /routes/_database/function.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | 4 | var MonetDBInterface = require(path.resolve('./lib/monetdb-interface')); 5 | 6 | var router = express.Router(); 7 | 8 | module.exports = function(app, options) { 9 | // Create a db interface 10 | var dbInterface = new MonetDBInterface(options); 11 | 12 | // Prefix all routes with /function 13 | app.use('/function', router); 14 | 15 | // Execute a function 16 | router.get('/:id', function(req, res) { 17 | // Get only the values 18 | var values = new Array; 19 | for(var key in req.query) { 20 | values.push(req.query[key]); 21 | } 22 | dbInterface.executeFunction(req.params.id, values) 23 | .then(function (result) { 24 | res.status(200).send(result.data); 25 | }, function (err) { 26 | res.status(500).send(err); 27 | }); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monetdb-rest", 3 | "version": "0.1.1", 4 | "description": "MonetDB RESTful Proxy", 5 | "keywords": [ 6 | "monetdb", 7 | "rest" 8 | ], 9 | "main": "server.js", 10 | "scripts": { 11 | "start": "node server.js" 12 | }, 13 | "dependencies": { 14 | "body-parser": "^1.14.1", 15 | "express": "^4.13.3", 16 | "lodash": ">=4.17.11", 17 | "monetdb": "^1.0.7", 18 | "node-uuid": "^1.4.7", 19 | "serve-favicon": "^2.3.0" 20 | }, 21 | "author": { 22 | "name": "Dimitar Nedev", 23 | "email": "dimitar.nedev@monetdbsolutions.com" 24 | }, 25 | "contributors": [ 26 | { 27 | "name": "Robin Cijvat", 28 | "email": "robin.cijvat@monetdbsolutions.com" 29 | } 30 | ], 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/MonetDB/monetdb-rest" 34 | }, 35 | "license": "MPL-2.0" 36 | } 37 | -------------------------------------------------------------------------------- /routes/_database/_document/find.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | 4 | var MonetDBInterface = require(path.resolve('./lib/monetdb-interface')); 5 | 6 | module.exports = function(app, options) { 7 | var schema = 'documents'; 8 | var table = 'documents'; 9 | 10 | // Create a db interface 11 | var dbInterface = new MonetDBInterface(options); 12 | 13 | // Find a document 14 | app.get('/_find?', function(req, res) { 15 | var key = Object.keys(req.query)[0]; 16 | var value = req.query[key]; 17 | dbInterface.executeQuery('SELECT * from ' + schema + '.' + table + ' ' + 18 | 'WHERE json.filter(body, \'$.' + key + '\') LIKE \'%' + value + '%\'') 19 | .then(function(result) { 20 | res.send(result.data); 21 | }, function(err) { 22 | res.status(500).send(err); 23 | }); 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var favicon = require('serve-favicon'); 3 | var path = require('path'); 4 | var routeLoader = require('./lib/route-loader'); 5 | 6 | var app = express(); 7 | var router = express.Router(); 8 | 9 | // Start the server 10 | var port = process.env.port || 8888; 11 | app.listen(port, function() { 12 | console.log('MonetDB RESTful Proxy started on port ' + port); 13 | }); 14 | // MonetDB favicon 15 | app.use(favicon(path.join(__dirname, 'public', 'images', 'favicon.ico'))); 16 | 17 | // Attached the router to the root 18 | app.use('/', router); 19 | // Welcome message 20 | router.get('/', function(req, res) { 21 | res.send('MonetDB RESTful Proxy'); 22 | }); 23 | 24 | // Add all routes 25 | routeLoader.addRoutes(path.resolve('./routes'), router, {}); 26 | 27 | // Return 404 for non-existing routes 28 | router.use(function(req, res) { 29 | res.sendStatus(404); 30 | }); 31 | 32 | exports = app; 33 | -------------------------------------------------------------------------------- /routes/_database/query.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var bodyParser = require('body-parser'); 4 | 5 | var MonetDBInterface = require(path.resolve('./lib/monetdb-interface')); 6 | 7 | var router = express.Router(); 8 | 9 | module.exports = function(app, options) { 10 | // Create a db interface 11 | var dbInterface = new MonetDBInterface(options); 12 | 13 | // Prefix all routes with /query 14 | app.use('/query', router); 15 | 16 | // Execute a query 17 | router.get('/?', function(req, res) { 18 | if (!req.query.q) { 19 | return res.status(400).send('No query provided'); 20 | } 21 | dbInterface.executeQuery(req.query.q) 22 | .then(function (result) { 23 | res.status(200).send(result.data); 24 | }, function (err) { 25 | res.status(500).send(err); 26 | }); 27 | }); 28 | 29 | // Execute a query 30 | router.post('/', bodyParser.text(), function(req, res) { 31 | if (!req.body) { 32 | return res.status(400).send('No query provided'); 33 | } 34 | dbInterface.executeQuery(req.body) 35 | .then(function (result) { 36 | res.status(200).send(result.data); 37 | }, function (err) { 38 | res.status(500).send(err); 39 | }); 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /routes/_database/schema.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var _ = require('lodash'); 4 | 5 | var routeLoader = require(path.resolve('./lib/route-loader')); 6 | var MonetDBInterface = require(path.resolve('./lib/monetdb-interface')); 7 | 8 | var router = express.Router(); 9 | 10 | var schemas = []; 11 | 12 | module.exports = function(app, options) { 13 | // Create a db interface 14 | var dbInterface = new MonetDBInterface(options); 15 | 16 | function schemaExists(req, res, next) { 17 | dbInterface.getAllSchemas().then(function(result) { 18 | // Flatten the array 19 | schemas = _.flatten(result.data); 20 | if (schemas.indexOf(req.params.schema) < 0) { 21 | return res.status(404).send('Schema ' + req.params.schema + ' not found'); 22 | } 23 | // All good, move on 24 | next(); 25 | }, function(err) { 26 | res.status(500).send(err); 27 | }); 28 | } 29 | 30 | // Prefix all routes with /schema 31 | app.use('/schema', router); 32 | 33 | // List all schemas 34 | router.get('/_all', function(req, res) { 35 | dbInterface.getAllSchemas().then(function(result) { 36 | schemas = _.flatten(result.data); 37 | res.send(schemas); 38 | }, function(err) { 39 | res.status(500).send(err); 40 | }); 41 | }); 42 | router.get('/', function(req, res) { 43 | dbInterface.getAllSchemas().then(function(result) { 44 | schemas = _.flatten(result.data); 45 | res.send(schemas); 46 | }, function(err) { 47 | res.status(500).send(err); 48 | }); 49 | }); 50 | 51 | // Add all sub-routes 52 | // Prefix with /:schema 53 | var subRouter = express.Router(); 54 | router.use('/:schema', schemaExists, function(req, res, next) { 55 | options.schema = req.params.schema; 56 | next(); 57 | }, subRouter); 58 | // Load all routes in the directory 59 | var allRoutes = routeLoader.addRoutes(path.join(__dirname, '_schema'), subRouter, options); 60 | subRouter.get('/_api', function(req, res) { 61 | // List the possible sub endpoints 62 | res.send(allRoutes); 63 | }); 64 | subRouter.get('/', function(req, res) { 65 | // List the possible sub endpoints 66 | res.send(allRoutes); 67 | }); 68 | }; 69 | -------------------------------------------------------------------------------- /routes/_database/_schema/table.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var bodyParser = require('body-parser'); 3 | var path = require('path'); 4 | var _ = require('lodash'); 5 | 6 | var routeLoader = require(path.resolve('./lib/route-loader')); 7 | var MonetDBInterface = require(path.resolve('./lib/monetdb-interface')); 8 | 9 | var router = express.Router(); 10 | 11 | var tables = []; 12 | 13 | module.exports = function(app, options) { 14 | // Create a db interface 15 | var dbInterface = new MonetDBInterface(options); 16 | 17 | function tableExists(req, res, next) { 18 | dbInterface.getAllTables(options.schema, 0).then(function(result) { 19 | tables = _.flatten(result.data); 20 | if (tables.indexOf(req.params.table) < 0) { 21 | return res.status(404).send('Table ' + req.params.table + ' not found'); 22 | } 23 | // All god, move on 24 | next(); 25 | }, function(err) { 26 | res.status(500).send(err); 27 | }); 28 | } 29 | 30 | // Prefix all routes with /table 31 | app.use('/table', router); 32 | 33 | // Create application/json parser 34 | router.use(bodyParser.json()); 35 | 36 | // List all tables 37 | router.get('/_all', function(req, res) { 38 | dbInterface.getAllTables(options.schema, 0).then(function(result) { 39 | tables = _.flatten(result.data); 40 | res.send(tables); 41 | }, function(err) { 42 | res.status(500).send(err); 43 | }); 44 | }); 45 | router.get('/', function(req, res) { 46 | dbInterface.getAllTables(options.schema, 0).then(function(result) { 47 | tables = _.flatten(result.data); 48 | res.send(tables); 49 | }, function(err) { 50 | res.status(500).send(err); 51 | }); 52 | }); 53 | 54 | // Create a table 55 | router.post('/', function(req, res) { 56 | if (!req.body || !req.body.table || !req.body.columns) { 57 | return res.sendStatus(400); 58 | } 59 | 60 | var columnsString = ''; 61 | for (var key in req.body.columns) { 62 | if (columnsString.length > 0) { 63 | columnsString = columnsString + ', '; 64 | } 65 | columnsString = columnsString + key + ' ' + req.body.columns[key]; 66 | } 67 | 68 | dbInterface.createTable(options.schema, req.body.table, { 69 | columns : columnsString 70 | }).then(function (result) { 71 | tables.push(req.body.table); 72 | res.status(201).send('Table ' + req.body.table + ' created'); 73 | }, function (err) { 74 | res.status(500).send(err); 75 | }); 76 | }); 77 | 78 | // Delete a table 79 | router.delete('/:table$', tableExists, function(req, res) { 80 | dbInterface.dropTable(options.schema, req.params.table).then(function(result) { 81 | _.remove(tables, req.params.table); 82 | res.status(204).send('Table ' + req.params.table + ' dropped'); 83 | }, function(err) { 84 | res.status(500).send(err); 85 | }); 86 | }); 87 | 88 | // Add all sub-routes for table item operations 89 | // Prefix with /:table 90 | var subRouter = express.Router(); 91 | router.use('/:table', tableExists, function(req, res, next) { 92 | options.table = req.params.table; 93 | next(); 94 | }, subRouter); 95 | // Load all routes in the directory 96 | routeLoader.addRoutes(path.join(__dirname, '_table'), subRouter, options); 97 | }; 98 | -------------------------------------------------------------------------------- /routes/_database/_schema/_table/table-operations.js: -------------------------------------------------------------------------------- 1 | var bodyParser = require('body-parser'); 2 | var path = require('path'); 3 | var _ = require('lodash'); 4 | 5 | var MonetDBInterface = require(path.resolve('./lib/monetdb-interface')); 6 | 7 | module.exports = function(app, options) { 8 | // Create a db interface 9 | var dbInterface = new MonetDBInterface(options); 10 | 11 | function listColumns(req, res) { 12 | dbInterface.getAllColumns(options.schema, options.table).then(function(result) { 13 | var table = { 14 | "table": options.table, 15 | "columns" : { 16 | } 17 | }; 18 | for (var i = 0; i < result.data.length; i++) { 19 | table.columns[result.data[i][0]] = result.data[i][1]; 20 | } 21 | return res.send(table); 22 | }, function(err) { 23 | return res.status(500).send(err); 24 | }); 25 | } 26 | 27 | // Create application/json parser 28 | app.use(bodyParser.json()); 29 | 30 | // Show row(s) 31 | app.get('/?', function(req, res) { 32 | if (!req.query) { 33 | return res.sendStatus(400); 34 | } 35 | if (req.query.columns) { 36 | var columns = req.query.columns.split(',') 37 | } 38 | dbInterface.select(options.schema, options.table, { 39 | columns : columns, 40 | filter : req.query 41 | }).then(function(result) { 42 | res.send(result.data); 43 | }, function(err) { 44 | res.status(500).send(err); 45 | }); 46 | }); 47 | 48 | // Add a new row 49 | app.post('/', function(req, res) { 50 | if (!req.body || !req.body.values) { 51 | return res.sendStatus(400); 52 | } 53 | 54 | var values = []; 55 | if (Array.isArray(req.body.values)) { 56 | values = req.body.values; 57 | } else { 58 | var columns = Object.keys(req.body.values); 59 | values = _.values(req.body.values); 60 | } 61 | 62 | dbInterface.insert(options.schema, options.table, { 63 | columns : columns, 64 | values : values 65 | }).then(function (result) { 66 | res.status(201).send(result); 67 | }, function (err) { 68 | res.status(500).send(err); 69 | }); 70 | }); 71 | 72 | // Update row(s) 73 | app.put('/?', function(req, res) { 74 | if (!req.body) { 75 | return res.sendStatus(400); 76 | } 77 | dbInterface.update(options.schema, options.table, { 78 | values : req.body, 79 | filter : req.query 80 | }).then(function (result) { 81 | res.send(result); 82 | }, function (err) { 83 | res.status(500).send(err); 84 | }); 85 | }); 86 | 87 | // Delete a row 88 | app.delete('/?', function(req, res) { 89 | dbInterface.deleteRow(options.schema, options.table, { 90 | filter : req.query 91 | }).then(function(result) { 92 | res.status(204).send(result); 93 | }, function(err) { 94 | res.status(500).send(err); 95 | }); 96 | }); 97 | 98 | // List column types 99 | app.get('/_info', function(req, res) { 100 | listColumns(req, res); 101 | }); 102 | app.get('/', function(req, res) { 103 | listColumns(req, res); 104 | }); 105 | 106 | // List rows 107 | app.get('/_all', function(req, res) { 108 | dbInterface.select(options.schema, options.table, {}).then(function(result) { 109 | res.send(result.data); 110 | }, function(err) { 111 | res.status(500).send(err); 112 | }); 113 | }); 114 | }; 115 | -------------------------------------------------------------------------------- /routes/database.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var bodyParser = require('body-parser'); 4 | 5 | var routeLoader = require(path.resolve('./lib/route-loader')); 6 | 7 | var router = express.Router(); 8 | 9 | var databases = {}; 10 | 11 | function dbExists(req, res, next) { 12 | if (!databases[req.params.db]) { 13 | return res.status(404).send('Database ' + req.params.db + ' not found'); 14 | } 15 | next(); 16 | } 17 | 18 | module.exports = function(app, options) { 19 | // Create application/json parser 20 | app.use(bodyParser.json()); 21 | 22 | // Prefix all routes with /database 23 | app.use('/database', router); 24 | 25 | // List all databases 26 | router.get('/_all', function(req, res) { 27 | res.send(databases); 28 | }); 29 | router.get('/', function(req, res) { 30 | res.send(databases); 31 | }); 32 | 33 | // Add or update database (to connect to) 34 | router.put('/:db', function(req, res) { 35 | // Create a new DB object 36 | var database = { 37 | // If no connections options are provided, we are going to use the defaults 38 | host : process.env.dbhost || 'localhost', 39 | port : process.env.dbport || 50000, 40 | dbname : process.env.dbname || 'demo', 41 | user : process.env.dbuser || 'monetdb', 42 | password : process.env.dbpass || 'monetdb' 43 | }; 44 | // Get options set in the request body 45 | if (req.body) { 46 | if (req.body.host) { 47 | database.host = req.body.host; 48 | } 49 | if (req.body.port) { 50 | database.port = req.body.port; 51 | } 52 | if (req.body.dbname) { 53 | database.dbname = req.body.dbname; 54 | } 55 | if (req.body.user) { 56 | database.user = req.body.user; 57 | } 58 | if (req.body.password) { 59 | database.password = req.body.password; 60 | } 61 | } 62 | 63 | databases[req.params.db] = database; 64 | console.log('Database connection added: ' + req.params.db + '{mapi://' + database.user + '@' + database.host + ':' + database.port + '/' + database.dbname + '}'); 65 | res.send(database); 66 | }); 67 | 68 | // Delete a database connection 69 | router.delete('/:db', dbExists, function(req, res) { 70 | delete databases[req.params.db]; 71 | console.log('Database connection deleted: ' + req.params.db); 72 | res.sendStatus(204); 73 | }); 74 | 75 | router.get('/:db/_connection', function(req, res) { 76 | // List the possible sub endpoints 77 | res.send(databases[req.params.db]); 78 | }); 79 | 80 | // Add all sub-routes 81 | // Prefix with /:db 82 | var subRouter = express.Router(); 83 | router.use('/:db', dbExists, function(req, res, next) { 84 | options = databases[req.params.db]; 85 | 86 | // !!! We need to do this deliberately here to avoid pre-loading of the sub-routes is empty options 87 | // Load all routes in the directory 88 | var allRoutes = routeLoader.addRoutes(path.join(__dirname, '_database'), subRouter, options); 89 | subRouter.get('/_api', function(req, res) { 90 | // List the possible sub endpoints 91 | res.send(allRoutes); 92 | }); 93 | subRouter.get('/', function(req, res) { 94 | // List the possible sub endpoints 95 | res.send(allRoutes); 96 | }); 97 | next(); 98 | }, subRouter); 99 | }; 100 | -------------------------------------------------------------------------------- /routes/_database/_schema/view.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var bodyParser = require('body-parser'); 3 | var path = require('path'); 4 | var _ = require('lodash'); 5 | 6 | var MonetDBInterface = require(path.resolve('./lib/monetdb-interface')); 7 | 8 | var router = express.Router(); 9 | 10 | var views = []; 11 | 12 | module.exports = function(app, options) { 13 | // Create a db interface 14 | var dbInterface = new MonetDBInterface(options); 15 | 16 | function viewExists(req, res, next) { 17 | dbInterface.getAllTables(options.schema, 1).then(function (result) { 18 | views = _.flatten(result.data); 19 | if (views.indexOf(req.params.view) < 0) { 20 | return res.status(404).send('View ' + req.params.view + ' not found'); 21 | } 22 | // All god, move on 23 | next(); 24 | }, function (err) { 25 | res.status(500).send(err); 26 | }); 27 | } 28 | 29 | function getViewQuery(req, res) { 30 | return dbInterface.executeQuery('SELECT query FROM sys.tables ' + 31 | 'WHERE name = \'' + req.params.view + '\' AND schema_id IN (SELECT id FROM sys.schemas WHERE name = \'' + options.schema + '\')'); 32 | } 33 | 34 | // Prefix all routes with /view 35 | app.use('/view', router); 36 | 37 | // Create application/json parser 38 | router.use(bodyParser.json()); 39 | 40 | // List all views 41 | router.get('/_all', function (req, res) { 42 | dbInterface.getAllTables(options.schema, 1).then(function (result) { 43 | views = _.flatten(result.data); 44 | res.send(views); 45 | }, function (err) { 46 | res.status(500).send(err); 47 | }); 48 | }); 49 | router.get('/', function (req, res) { 50 | dbInterface.getAllTables(options.schema, 1).then(function (result) { 51 | views = _.flatten(result.data); 52 | res.send(views); 53 | }, function (err) { 54 | res.status(500).send(err); 55 | }); 56 | }); 57 | 58 | // Show row(s) 59 | router.get('/:view/?', viewExists, function(req, res) { 60 | if (req.query.columns) { 61 | var columns = req.query.columns.split(',') 62 | } 63 | dbInterface.select(options.schema, req.params.view, { 64 | columns : columns 65 | }).then(function(result) { 66 | res.send(result.data); 67 | }, function(err) { 68 | res.status(500).send(err); 69 | }); 70 | }); 71 | 72 | // Show view columns and query 73 | router.get('/:view/_info', viewExists, function(req, res) { 74 | var table = { 75 | "view": req.params.view, 76 | "columns": { 77 | } 78 | }; 79 | 80 | dbInterface.getAllColumns(options.schema, req.params.view).then(function(result) { 81 | for (var i = 0; i < result.data.length; i++) { 82 | table.columns[result.data[i][0]] = result.data[i][1]; 83 | } 84 | }, function(err) { 85 | return res.status(500).send(err); 86 | }).then(function(result) { 87 | getViewQuery(req, res).then(function(result) { 88 | var query = result.data[0][0]; 89 | // Cut query 90 | table.query = query.substring(query.indexOf('as ') + 3); 91 | return res.send(table); 92 | }, function(err) { 93 | return res.status(500).send(err); 94 | }) 95 | }); 96 | }); 97 | 98 | // Create a view 99 | router.post('/', function (req, res) { 100 | if (!req.body || !req.body.view || !req.body.query) { 101 | res.sendStatus(400); 102 | } 103 | dbInterface.createView(options.schema, req.body.view, { 104 | query: req.body.query 105 | }).then(function (result) { 106 | views.push(req.body.view); 107 | res.status(201).send('View ' + req.body.view + ' created'); 108 | }, function (err) { 109 | res.status(500).send(err); 110 | }); 111 | }); 112 | 113 | // Create a view 114 | router.put('/:view/', function (req, res) { 115 | if (!req.body || !req.body.query) { 116 | res.sendStatus(400); 117 | } 118 | dbInterface.createView(options.schema, req.params.view, { 119 | query: req.body.query 120 | }).then(function (result) { 121 | views.push(req.params.view); 122 | res.status(201).send('View ' + req.params.view + ' created'); 123 | }, function (err) { 124 | res.status(500).send(err); 125 | }); 126 | }); 127 | 128 | // Delete a view 129 | router.delete('/:view', viewExists, function (req, res) { 130 | dbInterface.dropView(options.schema, req.params.view).then(function (result) { 131 | _.remove(views, req.params.view); 132 | res.status(204).send('View ' + req.params.view + ' dropped'); 133 | }, function (err) { 134 | res.status(500).send(err); 135 | }); 136 | }); 137 | }; 138 | -------------------------------------------------------------------------------- /routes/_database/document.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var bodyParser = require('body-parser'); 4 | var uuid = require('node-uuid'); 5 | var _ = require('lodash'); 6 | 7 | var routeLoader = require(path.resolve('./lib/route-loader')); 8 | var MonetDBInterface = require(path.resolve('./lib/monetdb-interface')); 9 | var errorHandler = require(path.resolve('./lib/error-handler')); 10 | 11 | var router = express.Router(); 12 | 13 | module.exports = function(app, options) { 14 | var schema = 'documents'; 15 | var table = 'documents'; 16 | 17 | // Create a db interface 18 | var dbInterface = new MonetDBInterface(options); 19 | 20 | function documentExists(id) { 21 | return dbInterface.select(schema, table, { 22 | columns : ['_id'], 23 | filter : { 24 | filter__id: id 25 | } 26 | }); 27 | } 28 | 29 | // Prefix all routes with /document 30 | app.use('/document', router); 31 | 32 | // Create application/json parser 33 | router.use(bodyParser.json()); 34 | 35 | // Add all routes for document item operations 36 | routeLoader.addRoutes(path.join(__dirname, '_document'), router, options); 37 | 38 | // Initialise the document schema and table 39 | router.post('/_init', function(req, res) { 40 | dbInterface.getAllSchemas().then(function(result) { 41 | var schemas = _.flatten(result.data); 42 | if (schemas.indexOf(schema) < 0) { 43 | dbInterface.executeQuery('CREATE SCHEMA documents'); 44 | } 45 | }, function(err) { 46 | res.status(500).send(err); 47 | }).then(function (result) { 48 | dbInterface.getAllTables(schema, 0).then(function(result) { 49 | var tables = _.flatten(result.data); 50 | if (tables.indexOf(table) < 0) { 51 | dbInterface.createTable(schema, table, { 52 | columns : '_id string, body json' 53 | } 54 | ) 55 | } 56 | // All god, move on 57 | }, function(err) { 58 | res.status(500).send(err); 59 | }); 60 | }).then(function (result) { 61 | res.status(201).send('Document schema and table initialised'); 62 | }); 63 | }); 64 | 65 | // List all documents 66 | router.get('/_all', function(req, res) { 67 | dbInterface.select(schema, table, { 68 | columns : [] 69 | }).then(function(result) { 70 | res.send(result.data); 71 | }, function(err) { 72 | res.status(500).send(err); 73 | }); 74 | }); 75 | router.get('/', function(req, res) { 76 | dbInterface.select(schema, table, { 77 | columns : [] 78 | }).then(function(result) { 79 | res.send(result.data); 80 | }, function(err) { 81 | res.status(500).send(err); 82 | }); 83 | }); 84 | 85 | // Return a single document 86 | router.get('/:id', function(req, res) { 87 | dbInterface.select(schema, table, { 88 | columns : [], 89 | filter : { 90 | filter__id: req.params.id 91 | } 92 | }).then(function(result) { 93 | if (result.rows > 0) { 94 | res.send(documentOps.parseDocumentsArray(result.data)[0]); 95 | } else { 96 | res.status(400).send('Document ' + req.params.id + ' not found'); 97 | } 98 | }, function(err) { 99 | res.status(500).send(err); 100 | }); 101 | }); 102 | 103 | // Create a new document 104 | router.post('/', function(req, res) { 105 | if (!req.body) { 106 | return res.sendStatus(400); 107 | } 108 | var id = uuid.v4(); 109 | dbInterface.insert(schema, table, { 110 | columns : ['_id', 'body'], 111 | values : [id, req.body] 112 | }).then(function (result) { 113 | res.status(201).send('Document ' + id + ' created'); 114 | }, function (err) { 115 | res.status(500).send(err); 116 | }); 117 | }); 118 | 119 | // Update or create a document 120 | router.put('/:id', function(req, res) { 121 | if (!req.body) { 122 | res.sendStatus(400); 123 | } 124 | documentExists(req.params.id).then(function (result) { 125 | if (result.data.length > 0) { 126 | dbInterface.update(schema, table, { 127 | values: { 128 | body : req.body 129 | }, 130 | filter: { 131 | filter__id: req.params.id 132 | } 133 | }).then(function (result) { 134 | res.send(result); 135 | }, function (err) { 136 | res.status(500).send(err); 137 | }); 138 | } else { 139 | dbInterface.insert(schema, table, { 140 | columns : ['_id', 'body'], 141 | values : [req.params.id, req.body] 142 | }).then(function (result) { 143 | res.status(201).send('Document ' + req.params.id + ' created'); 144 | }, function (err) { 145 | res.status(500).send(err); 146 | }); 147 | } 148 | }, function (err) { 149 | res.status(500).send(err); 150 | }); 151 | }); 152 | 153 | // Delete a document 154 | router.delete('/:id', function(req, res) { 155 | documentExists(req.params.id).then(function (result) { 156 | if (result.data.length > 0) { 157 | dbInterface.deleteRow(schema, table, { 158 | filter: { 159 | filter__id: req.params.id 160 | } 161 | }).then(function (result) { 162 | res.status(204).send(result); 163 | }, function (err) { 164 | res.status(500).send(err); 165 | }); 166 | } else { 167 | res.status(400).send('Document ' + req.params.id + ' not found'); 168 | } 169 | }, function (err) { 170 | res.status(500).send(err); 171 | }); 172 | }); 173 | }; 174 | -------------------------------------------------------------------------------- /lib/monetdb-interface.js: -------------------------------------------------------------------------------- 1 | var MonetDB = require('monetdb')(); 2 | var _ = require('lodash'); 3 | 4 | var connectionOptions = null; 5 | var connection = null; 6 | 7 | function MonetDBInterface(opt) { 8 | // Pass the connectionOptions 9 | connectionOptions = opt; 10 | } 11 | 12 | function connect() { 13 | // Check if we already have a connection 14 | if (connection) { 15 | return connection; 16 | } 17 | 18 | connection = new MonetDB(connectionOptions); 19 | connection.connect().fail(function(err) { 20 | console.log(err); 21 | }); 22 | 23 | return connection; 24 | } 25 | 26 | function keyToStringArray(array, str) { 27 | var result = []; 28 | for (var key in array) { 29 | result.push(key + ' = ' + str); 30 | } 31 | 32 | return result; 33 | } 34 | 35 | function filterToString(array) { 36 | var values = []; 37 | 38 | var filter_arr = []; 39 | var orderBy_arr = []; 40 | var groupBy_arr = []; 41 | var limit = ''; 42 | for (var key in array) { 43 | if (key.substring(0, 7) == 'filter_') { 44 | // TODO: sign different from = 45 | filter_arr.push(key.substring(key.indexOf('_') + 1) + ' = ?'); 46 | if (isNaN(array[key])) { 47 | values.push(array[key]); 48 | } else { 49 | values.push(+array[key]); 50 | } 51 | } else if (key == 'orderBy') { 52 | orderBy_arr.push(array[key]); 53 | } else if (key == 'groupBy') { 54 | groupBy_arr.push(array[key]); 55 | } else if (key == 'limit') { 56 | limit = 'LIMIT ' + array[key]; 57 | } 58 | } 59 | 60 | var filter = ''; 61 | if (filter_arr.length > 0) { 62 | filter = 'WHERE ' + filter_arr.join(' AND '); 63 | } 64 | var orderBy = ''; 65 | if (orderBy_arr.length > 0) { 66 | orderBy = 'ORDER BY ' + orderBy_arr.join(', '); 67 | } 68 | var groupBy = ''; 69 | if (groupBy_arr.length > 0) { 70 | groupBy = 'GROUP BY ' + groupBy_arr.join(', '); 71 | } 72 | 73 | var query_string = filter + ' ' + orderBy + ' ' + groupBy + ' ' + limit; 74 | 75 | return { 76 | query_string: query_string.trim(), 77 | values: values 78 | } 79 | } 80 | 81 | MonetDBInterface.prototype.select = function(schema, table, options) { 82 | var conn = connect(); 83 | 84 | var columns = ''; 85 | if (!options.columns || options.columns.length == 0) { 86 | columns = '*'; 87 | } else { 88 | columns = options.columns.join(', '); 89 | } 90 | 91 | if (!options.filter) { 92 | return conn.query('SELECT ' + columns + ' FROM ' + schema + '.' + table); 93 | } 94 | var filter = filterToString(options.filter); 95 | return conn.query('SELECT ' + columns + ' FROM ' + schema + '.' + table + 96 | ' ' + filter.query_string, 97 | filter.values); 98 | }; 99 | 100 | MonetDBInterface.prototype.insert = function(schema, table, options) { 101 | var conn = connect(); 102 | 103 | if (!options.columns) { 104 | return conn.query('INSERT INTO ' + schema + '.' + table + 105 | // Crete an string of comma-separated ? the size of the number of values 106 | ' VALUES ( ' + _.range(options.values.length).map(function() {return '?'}).join(', ') + ' )', 107 | options.values); 108 | } 109 | return conn.query('INSERT INTO ' + schema + '.' + table + 110 | ' ( ' + options.columns.join(', ') + ' ) ' + 111 | // Crete an string of comma-separated ? the size of the number of values 112 | ' VALUES ( ' + _.range(options.values.length).map(function() {return '?'}).join(', ') + ' )', 113 | options.values); 114 | }; 115 | 116 | MonetDBInterface.prototype.update = function(schema, table, options) { 117 | var conn = connect(); 118 | 119 | if (!options.filter) { 120 | return conn.query('UPDATE ' + schema + '.' + table + 121 | // Crete an string of comma-separated 'key = ?' the size of the number of values 122 | ' SET ' + keyToStringArray(options.values, '?').join(', '), 123 | _.values(options.values)); 124 | } 125 | var filter = filterToString(options.filter); 126 | return conn.query('UPDATE ' + schema + '.' + table + 127 | // Crete an string of comma-separated 'key = ?' the size of the number of values 128 | ' SET ' + keyToStringArray(options.values, '?').join(', ') + 129 | ' ' + filter.query_string, 130 | _.values(options.values).concat(filter.values)); 131 | }; 132 | 133 | MonetDBInterface.prototype.deleteRow = function(schema, table, options) { 134 | var conn = connect(); 135 | 136 | if (!options.filter) { 137 | return conn.query('DELETE FROM ' + schema + '.' + table); 138 | } 139 | var filter = filterToString(options.filter); 140 | return conn.query('DELETE FROM ' + schema + '.' + table + 141 | ' ' + filter.query_string, 142 | filter.values); 143 | }; 144 | 145 | MonetDBInterface.prototype.createTable = function(schema, table, options) { 146 | var conn = connect(); 147 | 148 | return conn.query('CREATE TABLE ' + schema + '.' + table + ' ( ' + options.columns + ' )'); 149 | }; 150 | 151 | MonetDBInterface.prototype.dropTable = function(schema, table) { 152 | var conn = connect(); 153 | 154 | return conn.query('DROP TABLE ' + schema + '.' + table); 155 | }; 156 | 157 | MonetDBInterface.prototype.createView = function(schema, view, options) { 158 | var conn = connect(); 159 | 160 | return conn.query('CREATE VIEW ' + schema + '.' + view + ' AS ' + options.query); 161 | }; 162 | 163 | MonetDBInterface.prototype.dropView = function(schema, view) { 164 | var conn = connect(); 165 | 166 | return conn.query('DROP VIEW ' + schema + '.' + view); 167 | }; 168 | 169 | MonetDBInterface.prototype.executeQuery = function(query, options) { 170 | var conn = connect(); 171 | 172 | return conn.query(query, options); 173 | }; 174 | 175 | MonetDBInterface.prototype.executeFunction = function(name, params) { 176 | var conn = connect(); 177 | 178 | if (params.length > 0) { 179 | var parameters = params[0]; 180 | for (i = 1; i < params.length; ++i) { 181 | parameters = parameters + ', ' + params[i]; 182 | } 183 | return conn.query('CALL ' + name + ' ( ' + parameters + ' )'); 184 | } 185 | return conn.query('CALL ' + name + '()'); 186 | }; 187 | 188 | MonetDBInterface.prototype.getAllSchemas = function() { 189 | return this.select('sys', 'schemas', { 190 | columns : ['name'] 191 | }); 192 | }; 193 | 194 | MonetDBInterface.prototype.getAllTables = function(schema, type) { 195 | return this.executeQuery('SELECT name from sys.tables ' + 196 | 'WHERE system = false AND type = ? ' + 197 | 'AND EXISTS ( SELECT schema_id FROM sys.schemas WHERE schema_id = schemas.id AND schemas.name = ? )', [type, schema]); 198 | }; 199 | 200 | MonetDBInterface.prototype.getAllColumns = function(schema, table) { 201 | return this.executeQuery('SELECT name, type FROM sys.columns ' + 202 | 'WHERE table_id IN (SELECT id FROM sys.tables ' + 203 | 'WHERE name = ? AND schema_id IN (SELECT id FROM sys.schemas WHERE name = ? ))', [table, schema]); 204 | }; 205 | 206 | module.exports = MonetDBInterface; 207 | -------------------------------------------------------------------------------- /TUTORIAL.md: -------------------------------------------------------------------------------- 1 | Tutorial 2 | To start, install MonetDB, start the daemon and create a new dbfarm and database for the REST called `test`: 3 | 4 | ``` 5 | shell> monetdbd create /path/to/mydbfarm 6 | shell> monetdbd start /path/to/mydbfarm 7 | shell> monetdb create test 8 | shell> monetdb release test 9 | shell> monetdb start test 10 | ``` 11 | Now install the REST proxy via NPM, or clone sources the REST proxy in a new directory and install the dependencies with NPM, and start the app: 12 | ``` 13 | shell> npm install monetdb-rest 14 | shell> npm start monetdb-rest 15 | ``` 16 | The proxy will start on port `8888`. By default no database connection are attached, so let's do that next: 17 | ``` 18 | shell> curl -X PUT -H "Content-Type: application/json" -d '{"host" : "localhost", "port" : 50000, "dbname" : "test", "user" : "monetdb", "password" : "monetdb"}' localhost:8888/database/test 19 | ``` 20 | Now, let's check what actions we can execute of the database: 21 | ``` 22 | shell> curl -X GET localhost:8888/database/test/ 23 | ["document","function","query","schema"] 24 | ``` 25 | The above indicates that we can access documents, functions, queries and schemas. For now let's check the schemas out. 26 | ``` 27 | shell> curl -X GET localhost:8888/database/test/schema/ 28 | ["sys","tmp","json"] 29 | ``` 30 | We have three schemas. Since sys is the default schema in MonetDB let's see what actions we can perform there: 31 | ``` 32 | shell> curl -X GET localhost:8888/database/test/schema/sys/ 33 | ["table","view"] 34 | ``` 35 | OK, so we've got table and view actions. Let's list what tables we have: 36 | ``` 37 | shell> curl -X GET localhost:8888/database/test/schema/sys/table/ 38 | [] 39 | ``` 40 | No (non-system) tables found, as expected. Time to create one: 41 | ``` 42 | shell> curl -X POST -H "Content-Type: application/json" -d '{"table" : "skynet", "columns" : {"id": "int", "first_name": "string", "last_name": "string"}}' localhost:8888/database/test/schema/sys/table/ 43 | Table skynet created 44 | ``` 45 | Next, load some data: 46 | ``` 47 | shell> curl -X POST -H "Content-Type: application/json" -d '{"values": [1, "John", "Connor"]}' localhost:8888/database/test/schema/sys/table/skynet 48 | shell> curl -X POST -H "Content-Type: application/json" -d '{"values": [2, null, "Connor"]}' localhost:8888/database/test/schema/sys/table/skynet 49 | shell> curl -X POST -H "Content-Type: application/json" -d '{"values" : {"id": 3, "first_name": "Kyle Reese"}}' localhost:8888/database/test/schema/sys/table/skynet 50 | ``` 51 | Let's see what we have loaded in the tables. 52 | ``` 53 | shell> curl -X GET localhost:8888/database/test/schema/sys/table/skynet/ 54 | [[1,"John","Connor"],[2,null,"Connor"],[3,"Kyle Reese",null]] 55 | ``` 56 | We've got a few records, but there seem to be a few issues. Let's add a first name to the record with id 2: 57 | ``` 58 | shell> curl -X PUT -H "Content-Type: application/json" -d '{"first_name": "Sarah"}' localhost:8888/database/test/schema/sys/table/skynet/?filter_id=2 59 | ``` 60 | Now let's fix the first and last name the 'Kyle Reese' (which has to be URL-encoded version of the string): 61 | ``` 62 | shell> curl -X PUT -H "Content-Type: application/json" -d '{"first_name": "Kyle", "last_name": "Reese"}' 'localhost:8888/database/test/schema/sys/table/skynet/?filter_first_name=Kyle%20Reese' 63 | ``` 64 | Next, check how well we've done by listing all records ordered by last name: 65 | ``` 66 | shell> curl -X GET localhost:8888/database/test/schema/sys/table/skynet/?orderBy=last_name 67 | [[2,"Sarah","Connor"],[1,"John","Connor"],[3,"Kyle","Reese"]] 68 | ``` 69 | Now remove all records with last name 'Connor': 70 | ``` 71 | shell> curl -X DELETE localhost:8888/database/test/schema/sys/table/skynet/?filter_last_name=Connor 72 | ``` 73 | This should've left us only one record. 74 | ``` 75 | shell> curl -X GET localhost:8888/database/test/schema/sys/table/skynet/ 76 | [[3,"Kyle","Reese"]] 77 | ``` 78 | Good, enough of tables. Next, let's store a few documents in our database. First thing, let's initialize the documents schema and table: 79 | ``` 80 | shell> curl -X POST localhost:8888/database/test/document/_init 81 | Document schema and table initialised 82 | ``` 83 | Next, create a new document storing information about a classic Asimov book: 84 | ``` 85 | shell> curl -X POST -H "Content-Type: application/json" -d '{"type": "book", "title": "Foundation", "authors": ["Isaac Asimov"], "year": 1951, "genre": "sci-fi"}' localhost:8888/database/test/document/ 86 | Document 9ef729da-2669-426c-9004-c661c6010810 created 87 | shell> curl -X POST -H "Content-Type: application/json" -d '{"type": "book", "title": "Dune", "authors": ["Frank Herbert"], "year": 1965, "genre": "sci-fi"}' localhost:8888/database/test/document/ 88 | Document b564796c-5783-47cc-98eb-5e576fab924f created 89 | ``` 90 | The responses you get is probably slightly different, this is because the documents were stored with under generated UUID. Now, let's store another one, giving it an explicit id: 91 | ``` 92 | shell> curl -X PUT -H "Content-Type: application/json" -d '{"type": "book", "title": "Second Foundation", "authors": ["Isaac Asimov"], "genre": "sci-fi"}' localhost:8888/database/test/document/foundation2/ 93 | Document foundation2 created 94 | ``` 95 | Let's see what we have stored so far: 96 | ``` 97 | shell> curl -X GET localhost:8888/database/test/document/_all 98 | [{"id":"9ef729da-2669-426c-9004-c661c6010810","body":{"type":"book","title":"Foundation","authors":["Isaac Asimov"],"year":1951,"genre":"sci-fi"}}, {"id":"b564796c-5783-47cc-98eb-5e576fab924f","body":{"type":"book","title":"Dune","authors":["Frank Herbert"],"year":1965,"genre":"sci-fi"}}{"id":"foundation2","body":{"type":"book","title":"Second Foundation","authors":["Isaac Asimov"],"genre":"sci-fi"}}] 99 | ``` 100 | Looks right, but the 'Second Foundation' is missing publication year. We can fix that by overwriting the document - PUT-ing a new one with all properties in place. 101 | ``` 102 | shell> curl -X PUT -H "Content-Type: application/json" -d '{"type": "book", "title": "Second Foundation", "authors": ["Isaac Asimov"], "year": 1953, "genre": "sci-fi"}' localhost:8888/database/test/document/foundation2/ 103 | ``` 104 | Let's check if the stored document is now correct: 105 | ``` 106 | shell> curl -GET localhost:8888/database/test/document/foundation2 107 | {"id":"foundation2","body":{"type":"book","title":"Second Foundation","authors":["Isaac Asimov"],"year":1953,"genre":"sci-fi"}} 108 | ``` 109 | Looks good. Finally, let's search for all books that have a title containing 'Foundation': 110 | ``` 111 | shell> curl -X GET localhost:8888/database/test/document/_find?title=Foundation 112 | [{"id":"9ef729da-2669-426c-9004-c661c6010810","body":{"type":"book","title":"Foundation","authors":["Isaac Asimov"],"year":1951,"genre":"sci-fi"}},{"id":"foundation2","body":{"type":"book","title":"Second Foundation","authors":["Isaac Asimov"],"year":1953,"genre":"sci-fi"}}] 113 | ``` 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MonetDB RESTful Proxy 2 | [![Build Status](https://travis-ci.org/MonetDB/monetdb-rest.svg)](https://travis-ci.org/MonetDB/monetdb-rest) 3 | [![npm version](https://badge.fury.io/js/monetdb-rest.svg)](http://badge.fury.io/js/monetdb-rest) 4 | [![Dependency Status](https://david-dm.org/MonetDB/monetdb-rest.svg)](https://david-dm.org/MonetDB/monetdb-rest) 5 | 6 | A proxy for MonetDB that provides a RESTful interface and extended documents support. It currently intended for as an experiment, rather than a complete replacement of the MonetDB SQL interface and MAPI protocol. Having said that, the proxy is meant to be extensible (see below). 7 | 8 | # Installation and start 9 | Run `npm install [-g] monetdb-rest` to install any dependencies 10 | 11 | Run `npm start` to start the MonetDB RESTful Proxy. The Node.js process will start on port `8888`. 12 | 13 | # API 14 | ## Database management 15 | HTTP method | URI path | Body | Description 16 | --- | --- | --- | --- 17 | GET | /database/_all | | List all attached databases 18 | PUT | /database/`` | Content-Type: application/json
{
"host" : "``",
"port" : ``,
"user" : "``",
"password" : "``"
} | Attach a database with the specified name and connection 19 | DELETE | /database/`` | | Detach the database with the specified name 20 | GET | /database/``/_connection | | Show the database connection info 21 | GET | /database/``/_api | | List all available endpoints at database level 22 | 23 | ## Data management 24 | HTTP method | URI path | Body | Description 25 | --- | --- | --- | --- 26 | GET | /database/``/schema/_all | | List all schemas 27 | GET | /database/``/schema/``/_api | | List all available endpoints at schema level 28 | GET | /database/``/schema/``/table/_all | | List all available tables for the schema 29 | POST | /database/``/schema/``/table/ | Content-Type: application/json
{
"table" : "``",
"columns" : {
"``": "``",
"``": "``"
}
} | Create a new table with the specified name and columns 30 | GET | /database/``/schema/``/table/``/_info | | Show table info 31 | GET | /database/``/schema/``/table/``/_all | | Get all rows and columns 32 | POST | /database/``/schema/``/table/`` | Content-Type: application/json
{
"values": [``, ``]
} | Insert values into table 33 | POST | /database/``/schema/``/table/`` | Content-Type: application/json
{
"values" : {
"``": ``,
"``": ``
}
}| Insert values into the specified columns on a table 34 | GET | /database/``/schema/``/table/``?columns=``,`` | | Get all values for the specified columns 35 | GET | /database/``/schema/``/table/``?filter_``=`` | | Get all matching the filter 36 | GET | /database/``/schema/``/table/``?orderBy=`` | | Order the results but a column-name 37 | GET | /database/``/schema/``/table/``?limit=`` | | Limit the results to the specified number of first values 38 | PUT | /database/``/schema/``/table/``?filter_``=`` | Content-Type: application/json
{
"``": ``,
"``": ``
} | Update the records matching the filter with the specified values 39 | DELETE | /database/``/schema/``/table/`` | | Delete the records matching the filter 40 | GET | /database/``/schema/``/view/_all | | Shows all views for the specified schema 41 | POST | /database/``/schema/``/view/ | Content-Type: application/json
{
"view": "``",
"query": "``"
} | Create a view with the specified name and SQL query 42 | GET | /database/``/schema/``/view/``/_info | | Get the view info 43 | GET | /database/``/schema/``/view/`` | | Get the view records 44 | DELETE | /database/``/schema/``/view/`` | | Delete the view 45 | GET | /database/``/query?q=`` | | Executes the URI-encoded SQL query 46 | POST | /database/``/query | Content-Type: text/plain
`SQL Query` | Executes the raw SQL query in the body 47 | GET | /database/``/function/``?a=``&b=`` | | Calls the SQL function with the provided parameters 48 | 49 | ### Strings, spaces and nulls 50 | * String values with white space in a path parameters need to be URL encoded, e.g. `/database/demo/schema/sys/table/cities?filter_city=Amsterdam%20Zuid` 51 | * JSON `null` values are passed to the database as null values. 52 | 53 | ## Document management 54 | To start working with document one must first create a schema `documents` and a table `documents` with the following columns: `_id int`, `body json`. If the schema and table are no found in the attached database, the proxy can create them with a `POST` to `/database//document/_init`. 55 | 56 | HTTP method | URI path | Body | Description 57 | --- | --- | --- | --- 58 | POST | /database/``/document/_init | | Initialise the document storage schema and table 59 | GET | /database/``/document/_all | | List all documents 60 | POST | /database/``/document/ | Content-Type: application/json
Document body | Store a document with a auto-generated ID 61 | PUT | /database/``/document/`` | Content-Type: application/json
Document body | Store or update a document with the specified document ID. If the document exits, replace it 62 | GET | /database/``/document/`` | | Get the document with the specified document ID 63 | DELETE | /database/``/document/`` | | Delete the document with the specified document ID 64 | GET | /database/``/document/``/_find?``=`` | | Get all documents matching the specified filter 65 | 66 | # Extensions 67 | The MonetDB RESTful Proxy is designed to be extensible, allowing developers to write their own endpoints and interfaces and dropping in place. 68 | 69 | Routes are handed by the _Express_ Node.js framework router. 70 | The `routes` directory contains all the URL path routes and endpoints in hierarchical order. Each directory contains an `index.js` file which scans the directory contents and registers all files as routes, ignoring only the entries prefixed with `_`. Endpoints or sub-routes are defined in each of the files or sub-directories. If you want to add another level to the route, use the `addRoutes` function from `lib/router-loader.js`, to add a new set of endpoints at the desired level. 71 | 72 | For example, the `routes` directory contains an `index.js` that scans the directory and a `database.js` file (added by the index) for database level operations - like creating a new database connection. In `database.js` a new rotue is added using `addRoutes`, referring to `routes/_database` directory and its contents. This adds the document, function, query and schema routes, which all appear as URL paths for specific database, as seen in the API documentation above. 73 | 74 | If you want to add a new route, e.g. _join_, you need to write a `join.js`, following the example of another 'query.js', and drop the file in `routes/_database`. After restarting the database you will be able to execute queries to the defined endpoints at `/database//join`. 75 | 76 | Alternatively, if you want to extend the search operations that can be performed on all documents, you need only work on the `routes/_database/_document/find.js` file. 77 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Mozilla Public License, version 2.0 2 | 3 | 1. Definitions 4 | 5 | 1.1. "Contributor" 6 | 7 | means each individual or legal entity that creates, contributes to the 8 | creation of, or owns Covered Software. 9 | 10 | 1.2. "Contributor Version" 11 | 12 | means the combination of the Contributions of others (if any) used by a 13 | Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | 17 | means Covered Software of a particular Contributor. 18 | 19 | 1.4. "Covered Software" 20 | 21 | means Source Code Form to which the initial Contributor has attached the 22 | notice in Exhibit A, the Executable Form of such Source Code Form, and 23 | Modifications of such Source Code Form, in each case including portions 24 | thereof. 25 | 26 | 1.5. "Incompatible With Secondary Licenses" 27 | means 28 | 29 | a. that the initial Contributor has attached the notice described in 30 | Exhibit B to the Covered Software; or 31 | 32 | b. that the Covered Software was made available under the terms of 33 | version 1.1 or earlier of the License, but not also under the terms of 34 | a Secondary License. 35 | 36 | 1.6. "Executable Form" 37 | 38 | means any form of the work other than Source Code Form. 39 | 40 | 1.7. "Larger Work" 41 | 42 | means a work that combines Covered Software with other material, in a 43 | separate file or files, that is not Covered Software. 44 | 45 | 1.8. "License" 46 | 47 | means this document. 48 | 49 | 1.9. "Licensable" 50 | 51 | means having the right to grant, to the maximum extent possible, whether 52 | at the time of the initial grant or subsequently, any and all of the 53 | rights conveyed by this License. 54 | 55 | 1.10. "Modifications" 56 | 57 | means any of the following: 58 | 59 | a. any file in Source Code Form that results from an addition to, 60 | deletion from, or modification of the contents of Covered Software; or 61 | 62 | b. any new file in Source Code Form that contains any Covered Software. 63 | 64 | 1.11. "Patent Claims" of a Contributor 65 | 66 | means any patent claim(s), including without limitation, method, 67 | process, and apparatus claims, in any patent Licensable by such 68 | Contributor that would be infringed, but for the grant of the License, 69 | by the making, using, selling, offering for sale, having made, import, 70 | or transfer of either its Contributions or its Contributor Version. 71 | 72 | 1.12. "Secondary License" 73 | 74 | means either the GNU General Public License, Version 2.0, the GNU Lesser 75 | General Public License, Version 2.1, the GNU Affero General Public 76 | License, Version 3.0, or any later versions of those licenses. 77 | 78 | 1.13. "Source Code Form" 79 | 80 | means the form of the work preferred for making modifications. 81 | 82 | 1.14. "You" (or "Your") 83 | 84 | means an individual or a legal entity exercising rights under this 85 | License. For legal entities, "You" includes any entity that controls, is 86 | controlled by, or is under common control with You. For purposes of this 87 | definition, "control" means (a) the power, direct or indirect, to cause 88 | the direction or management of such entity, whether by contract or 89 | otherwise, or (b) ownership of more than fifty percent (50%) of the 90 | outstanding shares or beneficial ownership of such entity. 91 | 92 | 93 | 2. License Grants and Conditions 94 | 95 | 2.1. Grants 96 | 97 | Each Contributor hereby grants You a world-wide, royalty-free, 98 | non-exclusive license: 99 | 100 | a. under intellectual property rights (other than patent or trademark) 101 | Licensable by such Contributor to use, reproduce, make available, 102 | modify, display, perform, distribute, and otherwise exploit its 103 | Contributions, either on an unmodified basis, with Modifications, or 104 | as part of a Larger Work; and 105 | 106 | b. under Patent Claims of such Contributor to make, use, sell, offer for 107 | sale, have made, import, and otherwise transfer either its 108 | Contributions or its Contributor Version. 109 | 110 | 2.2. Effective Date 111 | 112 | The licenses granted in Section 2.1 with respect to any Contribution 113 | become effective for each Contribution on the date the Contributor first 114 | distributes such Contribution. 115 | 116 | 2.3. Limitations on Grant Scope 117 | 118 | The licenses granted in this Section 2 are the only rights granted under 119 | this License. No additional rights or licenses will be implied from the 120 | distribution or licensing of Covered Software under this License. 121 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 122 | Contributor: 123 | 124 | a. for any code that a Contributor has removed from Covered Software; or 125 | 126 | b. for infringements caused by: (i) Your and any other third party's 127 | modifications of Covered Software, or (ii) the combination of its 128 | Contributions with other software (except as part of its Contributor 129 | Version); or 130 | 131 | c. under Patent Claims infringed by Covered Software in the absence of 132 | its Contributions. 133 | 134 | This License does not grant any rights in the trademarks, service marks, 135 | or logos of any Contributor (except as may be necessary to comply with 136 | the notice requirements in Section 3.4). 137 | 138 | 2.4. Subsequent Licenses 139 | 140 | No Contributor makes additional grants as a result of Your choice to 141 | distribute the Covered Software under a subsequent version of this 142 | License (see Section 10.2) or under the terms of a Secondary License (if 143 | permitted under the terms of Section 3.3). 144 | 145 | 2.5. Representation 146 | 147 | Each Contributor represents that the Contributor believes its 148 | Contributions are its original creation(s) or it has sufficient rights to 149 | grant the rights to its Contributions conveyed by this License. 150 | 151 | 2.6. Fair Use 152 | 153 | This License is not intended to limit any rights You have under 154 | applicable copyright doctrines of fair use, fair dealing, or other 155 | equivalents. 156 | 157 | 2.7. Conditions 158 | 159 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in 160 | Section 2.1. 161 | 162 | 163 | 3. Responsibilities 164 | 165 | 3.1. Distribution of Source Form 166 | 167 | All distribution of Covered Software in Source Code Form, including any 168 | Modifications that You create or to which You contribute, must be under 169 | the terms of this License. You must inform recipients that the Source 170 | Code Form of the Covered Software is governed by the terms of this 171 | License, and how they can obtain a copy of this License. You may not 172 | attempt to alter or restrict the recipients' rights in the Source Code 173 | Form. 174 | 175 | 3.2. Distribution of Executable Form 176 | 177 | If You distribute Covered Software in Executable Form then: 178 | 179 | a. such Covered Software must also be made available in Source Code Form, 180 | as described in Section 3.1, and You must inform recipients of the 181 | Executable Form how they can obtain a copy of such Source Code Form by 182 | reasonable means in a timely manner, at a charge no more than the cost 183 | of distribution to the recipient; and 184 | 185 | b. You may distribute such Executable Form under the terms of this 186 | License, or sublicense it under different terms, provided that the 187 | license for the Executable Form does not attempt to limit or alter the 188 | recipients' rights in the Source Code Form under this License. 189 | 190 | 3.3. Distribution of a Larger Work 191 | 192 | You may create and distribute a Larger Work under terms of Your choice, 193 | provided that You also comply with the requirements of this License for 194 | the Covered Software. If the Larger Work is a combination of Covered 195 | Software with a work governed by one or more Secondary Licenses, and the 196 | Covered Software is not Incompatible With Secondary Licenses, this 197 | License permits You to additionally distribute such Covered Software 198 | under the terms of such Secondary License(s), so that the recipient of 199 | the Larger Work may, at their option, further distribute the Covered 200 | Software under the terms of either this License or such Secondary 201 | License(s). 202 | 203 | 3.4. Notices 204 | 205 | You may not remove or alter the substance of any license notices 206 | (including copyright notices, patent notices, disclaimers of warranty, or 207 | limitations of liability) contained within the Source Code Form of the 208 | Covered Software, except that You may alter any license notices to the 209 | extent required to remedy known factual inaccuracies. 210 | 211 | 3.5. Application of Additional Terms 212 | 213 | You may choose to offer, and to charge a fee for, warranty, support, 214 | indemnity or liability obligations to one or more recipients of Covered 215 | Software. However, You may do so only on Your own behalf, and not on 216 | behalf of any Contributor. You must make it absolutely clear that any 217 | such warranty, support, indemnity, or liability obligation is offered by 218 | You alone, and You hereby agree to indemnify every Contributor for any 219 | liability incurred by such Contributor as a result of warranty, support, 220 | indemnity or liability terms You offer. You may include additional 221 | disclaimers of warranty and limitations of liability specific to any 222 | jurisdiction. 223 | 224 | 4. Inability to Comply Due to Statute or Regulation 225 | 226 | If it is impossible for You to comply with any of the terms of this License 227 | with respect to some or all of the Covered Software due to statute, 228 | judicial order, or regulation then You must: (a) comply with the terms of 229 | this License to the maximum extent possible; and (b) describe the 230 | limitations and the code they affect. Such description must be placed in a 231 | text file included with all distributions of the Covered Software under 232 | this License. Except to the extent prohibited by statute or regulation, 233 | such description must be sufficiently detailed for a recipient of ordinary 234 | skill to be able to understand it. 235 | 236 | 5. Termination 237 | 238 | 5.1. The rights granted under this License will terminate automatically if You 239 | fail to comply with any of its terms. However, if You become compliant, 240 | then the rights granted under this License from a particular Contributor 241 | are reinstated (a) provisionally, unless and until such Contributor 242 | explicitly and finally terminates Your grants, and (b) on an ongoing 243 | basis, if such Contributor fails to notify You of the non-compliance by 244 | some reasonable means prior to 60 days after You have come back into 245 | compliance. Moreover, Your grants from a particular Contributor are 246 | reinstated on an ongoing basis if such Contributor notifies You of the 247 | non-compliance by some reasonable means, this is the first time You have 248 | received notice of non-compliance with this License from such 249 | Contributor, and You become compliant prior to 30 days after Your receipt 250 | of the notice. 251 | 252 | 5.2. If You initiate litigation against any entity by asserting a patent 253 | infringement claim (excluding declaratory judgment actions, 254 | counter-claims, and cross-claims) alleging that a Contributor Version 255 | directly or indirectly infringes any patent, then the rights granted to 256 | You by any and all Contributors for the Covered Software under Section 257 | 2.1 of this License shall terminate. 258 | 259 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user 260 | license agreements (excluding distributors and resellers) which have been 261 | validly granted by You or Your distributors under this License prior to 262 | termination shall survive termination. 263 | 264 | 6. Disclaimer of Warranty 265 | 266 | Covered Software is provided under this License on an "as is" basis, 267 | without warranty of any kind, either expressed, implied, or statutory, 268 | including, without limitation, warranties that the Covered Software is free 269 | of defects, merchantable, fit for a particular purpose or non-infringing. 270 | The entire risk as to the quality and performance of the Covered Software 271 | is with You. Should any Covered Software prove defective in any respect, 272 | You (not any Contributor) assume the cost of any necessary servicing, 273 | repair, or correction. This disclaimer of warranty constitutes an essential 274 | part of this License. No use of any Covered Software is authorized under 275 | this License except under this disclaimer. 276 | 277 | 7. Limitation of Liability 278 | 279 | Under no circumstances and under no legal theory, whether tort (including 280 | negligence), contract, or otherwise, shall any Contributor, or anyone who 281 | distributes Covered Software as permitted above, be liable to You for any 282 | direct, indirect, special, incidental, or consequential damages of any 283 | character including, without limitation, damages for lost profits, loss of 284 | goodwill, work stoppage, computer failure or malfunction, or any and all 285 | other commercial damages or losses, even if such party shall have been 286 | informed of the possibility of such damages. This limitation of liability 287 | shall not apply to liability for death or personal injury resulting from 288 | such party's negligence to the extent applicable law prohibits such 289 | limitation. Some jurisdictions do not allow the exclusion or limitation of 290 | incidental or consequential damages, so this exclusion and limitation may 291 | not apply to You. 292 | 293 | 8. Litigation 294 | 295 | Any litigation relating to this License may be brought only in the courts 296 | of a jurisdiction where the defendant maintains its principal place of 297 | business and such litigation shall be governed by laws of that 298 | jurisdiction, without reference to its conflict-of-law provisions. Nothing 299 | in this Section shall prevent a party's ability to bring cross-claims or 300 | counter-claims. 301 | 302 | 9. Miscellaneous 303 | 304 | This License represents the complete agreement concerning the subject 305 | matter hereof. If any provision of this License is held to be 306 | unenforceable, such provision shall be reformed only to the extent 307 | necessary to make it enforceable. Any law or regulation which provides that 308 | the language of a contract shall be construed against the drafter shall not 309 | be used to construe this License against a Contributor. 310 | 311 | 312 | 10. Versions of the License 313 | 314 | 10.1. New Versions 315 | 316 | Mozilla Foundation is the license steward. Except as provided in Section 317 | 10.3, no one other than the license steward has the right to modify or 318 | publish new versions of this License. Each version will be given a 319 | distinguishing version number. 320 | 321 | 10.2. Effect of New Versions 322 | 323 | You may distribute the Covered Software under the terms of the version 324 | of the License under which You originally received the Covered Software, 325 | or under the terms of any subsequent version published by the license 326 | steward. 327 | 328 | 10.3. Modified Versions 329 | 330 | If you create software not governed by this License, and you want to 331 | create a new license for such software, you may create and use a 332 | modified version of this License if you rename the license and remove 333 | any references to the name of the license steward (except to note that 334 | such modified license differs from this License). 335 | 336 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 337 | Licenses If You choose to distribute Source Code Form that is 338 | Incompatible With Secondary Licenses under the terms of this version of 339 | the License, the notice described in Exhibit B of this License must be 340 | attached. 341 | 342 | Exhibit A - Source Code Form License Notice 343 | 344 | This Source Code Form is subject to the 345 | terms of the Mozilla Public License, v. 346 | 2.0. If a copy of the MPL was not 347 | distributed with this file, You can 348 | obtain one at 349 | http://mozilla.org/MPL/2.0/. 350 | 351 | If it is not possible or desirable to put the notice in a particular file, 352 | then You may include the notice in a location (such as a LICENSE file in a 353 | relevant directory) where a recipient would be likely to look for such a 354 | notice. 355 | 356 | You may add additional accurate notices of copyright ownership. 357 | 358 | Exhibit B - "Incompatible With Secondary Licenses" Notice 359 | 360 | This Source Code Form is "Incompatible 361 | With Secondary Licenses", as defined by 362 | the Mozilla Public License, v. 2.0. 363 | --------------------------------------------------------------------------------