Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DRAFT] Add Node.js feature #67

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ php/composer.lock
java/target/*
java/java.iml
java/.idea
node-js/node_modules
317 changes: 317 additions & 0 deletions node-js/controller.js
Original file line number Diff line number Diff line change
@@ -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/<id>/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/<id>` 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/<digest>` 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");
}

21 changes: 21 additions & 0 deletions node-js/package.json
Original file line number Diff line number Diff line change
@@ -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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like node-crate is unmaintained 1. What about switching to the canonical node-postgres 2 today?

Footnotes

  1. https://www.npmjs.com/package/node-crate

  2. https://www.npmjs.com/package/pg

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Contrary to my previous assessment, it looks like node-crate is well alive and maintained 1.

Footnotes

  1. https://github.com/megastef/node-crate

"sha1": "^1.1.1",
"uuid": "^3.1.0"
}
}
17 changes: 16 additions & 1 deletion tests/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand All @@ -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)
Expand Down