diff --git a/.gitignore b/.gitignore index 4bf344f..5594f47 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ php/composer.lock java/target/* java/java.iml java/.idea +node-js/node_modules diff --git a/node-js/controller.js b/node-js/controller.js new file mode 100644 index 0000000..1353bdc --- /dev/null +++ b/node-js/controller.js @@ -0,0 +1,317 @@ +//requirements +var uuid = require('uuid/v1'); +var sha1 = require('sha1'); +var crate = require('node-crate'); +var app = require('express')(); +var http = require('http'); +var bodyParser = require('body-parser'); + +//configurables +var crate_host = 'localhost'; +var crate_port_number = 4200; +var crate_port = ''+crate_port_number; +var service_port = 8080; + +//bootstrap +crate.connect(crate_host, crate_port); +app.all('/*', function(req, res, next) { + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Credentials", "true"); + res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, UPDATE, DELETE, OPTION"); + res.setHeader("Access-Control-Allow-Headers", "Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers"); + next(); +}) +app.use(bodyParser.json({limit: '5mb'})); // for parsing application/json +app.use(bodyParser.raw({limit: '5mb'})); +app.use(bodyParser.urlencoded({limit: '5mb', extended: true })); // for parsing application/x-www-form-urlencoded + +//rest-service-functions +//############POST-RESOURCE########################## + +//POST /posts - Create a new post. +app.post('/posts', function(req, res){ + res.setHeader('Content-Type', 'application/json'); + + //parsed data is found in req.body + + //check for required data + if(!req.body.text) { + res.status(400).json({ + error: 'Argument "text" is required', + status: 400 + }); + return; + } + if(!req.body.user.location) { + res.status(400).json({ + error: 'Argument "location" is required', + status: 400 + }) + return; + } + + var tablename = 'guestbook.posts'; + var id = uuid(); + //insert + crate.insert( + tablename, + { + "id": id, + "user": req.body.user, + "text": req.body.text, + "created": new Date().getTime(), + "image_ref": req.body.image_ref, + "like_count": 0 + }).then(() => { + //refresh table to make sure new record is immediately available + refreshTable().then(() => { + //fetch new record + getPost(id).then((response) => { + res.status(201).json(response.json); + }) + }) + }); +}); + +//GET /posts - Retrieve a list of all posts. +app.get('/posts', function(req, res) { + res.setHeader('Content-Type', 'application/json'); + + getPosts().then((response) => { + res.status(200).json(response.json); + }); +}); + +//GET /post/:id - Retrieves the post with the corresponding id. +app.get('/post/:id', function(req, res) { + res.setHeader('Content-Type', 'application/json'); + + var id = req.params.id; + + getPost(id).then((response) => { + if(response.rowcount > 0){ + res.status(200).json(response.json[0]); + }else { + res.status(404).json({ + error: 'Post with id="'+id+'" not found', + status: 404 + }); + } + }); +}); + +//PUT /post/{id} - Updates text property of given id. +app.put('/post/:id', function(req, res) { + res.setHeader('Content-Type', 'application/json'); + + var id = req.params.id; + var text = req.body.text; + + if(!text) { + res.status(400).json({ + error: 'Argument "text" is required', + status: 400 + }) + return; + } + + updatePost(id, text).then((_) => { + refreshTable().then((_) => { + getPost(id).then((response) => { + res.status(200).json(response.json[0]); + }) + }) + }).catch(() => { + res.status(404).end(); + }) +}); + + + +//### `PUT /post//like` Increments the like count for a given post by one. +app.put('/post/:id/like', function(req, res){ + res.setHeader('Content-Type', 'application/json'); + + var id = req.params.id + getPost(id).then((response) => { + var newLikes = response.json[0].like_count + 1; + likePost(id, newLikes).then(() => { + response.json[0].like_count += 1; + refreshTable().then(()=>{ + res.status(200).json(response.json[0]); + }) + }) + }).catch(() => { + res.status(404).json({ + error: 'Post with id="'+id+'" not found', + status: 404 + }) + }) +}); + +//### `DELETE /post/` Delete a post with given `id`. +app.delete('/post/:id', function(req, res){ + + var id = req.params.id; + + deletePost(id).then((response) => { + if(response.rowcount>0) { + res.status(204).json(response.json); + } + else { + res.status(404).json({ + error: 'Post with id="'+id+'" not found', + status: 404 + }); + } + }) +}); + +//### `POST /search` Issue a search request to fetch a list of posts whose ``text`` matches a given query string. +app.post('/search', function(req, res){ + res.setHeader('Content-Type', 'application/json'); + + var searchText = req.body.query_string; + + if(!searchText) { + res.status(400).json({ + error: 'Argument "query_string" is required', + status: 400 + }) + return; + } + + var query = ("SELECT p.*, p._score AS _score, c.name AS country, c.geometry AS area FROM guestbook.posts AS p, guestbook.countries AS c WHERE within(p.user['location'], c.geometry) AND match(text, ?) ORDER BY _score DESC"); + + crate.execute(query, [searchText]).then((response) => { + res.status(200).json(response.json); + }) +}); + +//############IMAGE-RESOURCE########################## +app.post('/images', function(req, res){ + res.setHeader('Content-Type', 'application/json'); + + if(!req.body.blob){ + res.status(400).json({ + error: 'Argument "blob" is required', + status: 400 + }); + return; + } + + var b64String = req.body.blob; + var buf = Buffer.from(b64String, 'base64'); + + var encrypted = sha1(buf); + + var urlValue = '/image/'+encrypted; + + var result = {}; + result['url'] = urlValue; + result['digest'] = encrypted; + + crate.insertBlob('guestbook_images', buf).then(() => { + res.status(201).json(result); + }).catch(() => { + res.status(409).json(result); + }); +}); + +app.get('/images', function(req, res) { + res.setHeader('Content-Type', 'application/json'); + + getImages().then((response) => { + res.status(200).json(response.json); + }) +}) + +app.get('/image/:digest', function(req, res) { + console.log('GET /images was called with digest: '+req.params.digest); + crate.getBlob('guestbook_images', req.params.digest).then((data) => { + console.log(req.params.digest + ' has a length of ' + data.length); + if(data.length>0) { + res.setHeader('Content-Type', 'image/gif'); + res.setHeader('Content-Length', ''+data.length+''); + res.status(200).end(data); + } + else { + res.setHeader('Content-Type', 'application/json'); + res.status(404).json({ + error: 'Image with digest="'+req.params.digest+'" not found', + status: 404 + }); + } + }) +}); + +//### `DELETE /image/` Delete an image with given `digest`. +app.delete('/image/:digest', function(req, res){ + var digest = req.params.digest; + + var options = { + host: crate_host, + port: crate_port_number, + path: '/_blobs/guestbook_images/'+digest, + method: 'DELETE' + }; + http.request(options, (response) => { + res.status(response.statusCode); + if(response.statusCode!=204) { + res.setHeader('Content-Type', 'application/json'); + res.json({ + error: 'Image with digest="'+digest+'" not found', + status: 404 + }); + } + else { + res.end(); + } + }).end(); +}); + +//launch +var server = app.listen(service_port, function() { + var host = server.address().address; + + console.log("app listening at http://%s:%s", host, service_port); +}) + +//helper +function getPosts() { + return crate.execute("SELECT p.*, c.name as country, c.geometry as area FROM guestbook.posts AS p, guestbook.countries AS c WHERE within(p.user['location'], c.geometry) ORDER BY p.created DESC"); +} + +function getPost(id) { + var query = ("SELECT p.*, c.name as country, c.geometry as area FROM guestbook.posts AS p, guestbook.countries AS c WHERE within(p.user['location'], c.geometry) AND p.id=?"); + return crate.execute(query, [id]); +} + +function updatePost(id, newText) { + var where = "id='"+id+"'"; + return crate.update('guestbook.posts', { text: newText }, where); +} + +function likePost(id, newLikes) { + var where = "id='"+id+"'"; + return crate.update('guestbook.posts', { like_count: newLikes }, where); +} + +function deletePost(id) { + var query = ("DELETE FROM guestbook.posts WHERE id=?"); + return crate.execute(query, [id]); +} + +function getImages(){ + return crate.execute("SELECT digest, last_modified " + "FROM Blob.guestbook_images " + "ORDER BY 2 DESC"); +} + +function getImage(digest) { + var query = "SELECT digest FROM Blob.guestbook_images WHERE digest='?'"; + return crate.execute(query, [digest]); +} + +function refreshTable() { + return crate.execute("REFRESH TABLE guestbook.posts"); +} + diff --git a/node-js/package.json b/node-js/package.json new file mode 100644 index 0000000..62e03be --- /dev/null +++ b/node-js/package.json @@ -0,0 +1,21 @@ +{ + "name": "crate-sample-apps", + "version": "1.0.0", + "description": "node.js support for crate-sample-apps", + "main": "controller.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "crate", + "sample" + ], + "author": "Alex, Fabian", + "license": "Apache-2.0", + "dependencies": { + "express": "^4.15.3", + "node-crate": "^2.0.1", + "sha1": "^1.1.1", + "uuid": "^3.1.0" + } +} diff --git a/tests/test.py b/tests/test.py index 9d7fef1..d38d23c 100755 --- a/tests/test.py +++ b/tests/test.py @@ -25,6 +25,7 @@ import json import base64 import unittest +import time from argparse import ArgumentParser from urllib.request import urlopen, Request from urllib.error import HTTPError @@ -286,7 +287,11 @@ def _create_image(self): return d['digest'], d['url'] def _retrieve_image(self, digest): - with urlopen(self.get_url('/image/{}'.format(digest))) as response: + timeout=5 + interval=.2 + path=self.get_url('/image/{}'.format(digest)) + expectedStatusCode=200 + with self._wait_for_image_response(timeout, interval, path, expectedStatusCode) as response: self.assertEqual(response.status, 200) h = response.headers self.assertEqual(h['Content-Type'], 'image/gif') @@ -300,6 +305,16 @@ def _retrieve_image(self, digest): res, code = self.req('GET', '/image/{}'.format(invalid_digest)) self.assertImageNotFound(res, code, digest=invalid_digest) + def _wait_for_image_response(self, timeout, interval, path, expectedStatusCode): + timeLeft=timeout + while timeLeft > 0: + try: + return urlopen(path) + except: + time.sleep(interval) + timeLeft-=interval + self.assertTrue("Time waiting exceeded",False) + def _delete_image(self, digest): res, code = self.req('DELETE', '/image/{}'.format(digest)) self.assertEqual(code, 204)