diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +node_modules diff --git a/.gitignore b/.gitignore index da23d0d..dae851e 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ build/Release # Deployed apps should consider commenting this line out: # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git node_modules + +key.json +*~ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..0574049 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: node_js +node_js: +- '0.10' +before_install: +- openssl aes-256-cbc -K $encrypted_8bfe06cf1581_key -iv $encrypted_8bfe06cf1581_iv + -in key.json.enc -out key.json -d +before_script: +- npm run-script lint +env: +- DATASET_ID=proppy-mvm-dogfood diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0e104db --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,20 @@ +# How to become a contributor and submit your own code + +## Contributor License Agreements + +We'd love to accept your patches! Before we can take them, we have to jump a couple of legal hurdles. + +Please fill out either the individual or corporate Contributor License Agreement (CLA). + + * If you are an individual writing original source code and you're sure you own the intellectual property, then you'll need to sign an [individual CLA](http://code.google.com/legal/individual-cla-v1.0.html). + * If you work for a company that wants to allow you to contribute your work, then you'll need to sign a [corporate CLA](http://code.google.com/legal/corporate-cla-v1.0.html). + +Follow either of the two links above to access the appropriate CLA and instructions for how to sign and return it. Once we receive it, we'll be able to accept your pull requests. + +## Contributing A Patch + +1. Submit an issue describing your proposed change to the repo in question. +1. The repo owner will respond to your issue promptly. +1. If your proposed change is accepted, and you haven't already done so, sign a Contributor License Agreement (see details above). +1. Fork the desired repo, develop and test your code changes. +1. Submit a pull request. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3fd436d --- /dev/null +++ b/Dockerfile @@ -0,0 +1 @@ +FROM google/nodejs-runtime diff --git a/Gulpfile.js b/Gulpfile.js new file mode 100644 index 0000000..22aec97 --- /dev/null +++ b/Gulpfile.js @@ -0,0 +1,21 @@ +var gulp = require('gulp'); +var Dredd = require('dredd'); +var app = require('./todos.js'); + +gulp.task('dredd', function(cb) { + var server = app.listen(8080, function() { + var dredd = new Dredd({ + blueprintPath: 'todos.apib', + server: 'http://localhost:8080', + options: { + hookfiles: 'test_hooks.js' + } + }); + dredd.run(function(error, stats){ + server.close(); + cb(); + }); + }); +}); + +gulp.task('test', ['dredd']); diff --git a/README.md b/README.md index dc6c41b..e41a96c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,88 @@ gcloud-node-todos ================= -TodoMVC sample for gcloud-node +[![Build Status](https://travis-ci.org/GoogleCloudPlatform/gcloud-node-todos.svg?branch=master)](https://travis-ci.org/GoogleCloudPlatform/gcloud-node-todos) -See https://github.com/GoogleCloudPlatform/gcloud-node-todos/pull/1 +TodoMVC backend using [gcloud-node](//github.com/GoogleCloudPlatform/gcloud-node). + +# API + +- Insert a todo + + curl -X POST -d '{text: "do this"}' http://localhost:8080/todos + +- Get a todo + + curl -X GET http://localhost:8080/todos/{{todo id}} + +- Mark a todo as done + + curl -X PUT -d '{text: "do this", "done": true}' http://localhost:8080/todos/{{todo id}} + +- Delete a todo + + curl -X DELETE http://localhost:8080/todos/{{todo id}} + +- Get all todos + + curl -X GET http://localhost:8080/todos + +- Clear all `done` todos + + curl -X DELETE http://localhost:8080/todos + +# Prerequisites + + - Create a new cloud project on [console.developers.google.com](http://console.developers.google.com) + - [Enable](https://console.developers.google.com/flows/enableapi?apiid=datastore) the [Google Cloud Datastore API](https://developers.google.com/datastore) + - Create a new service account and copy the `JSON` credentials to `key.json` + - Export your project id + + export PROJECT_ID= + +# Run locally + + # set your default dataset + export DATASET_ID=$PROJECT_ID + # fetch the dependencies + npm install + # start the app + npm start + # run acceptance test + dredd todos.apib http://localhost:8080 --hookfiles test_hooks.js + +# Run in docker + + # check that docker is running + boot2docker up + export DOCKER_HOST=$(boot2docker shellinit) + + # build your docker image + docker build -t app . + # start a new docker container + docker run -e DATASET_ID=$PROJECT_ID -p 8080:8080 app + + # test the app + curl -X GET http://$(boot2docker ip):8080 + +# Run w/ [Managed VMs](https://developers.google.com/appengine/docs/managed-vms/) + + # get gcloud + curl https://sdk.cloud.google.com | bash + # authorize gcloud and set your default project + gcloud auth login + gcloud config set project $PROJECT_ID + + # get managed vms component + gcloud components update app-engine-managed-vms + + # check that docker is running + boot2docker up + + # run the app locally + gcloud preview app run . + curl -X GET http://localhost:8080 + + # deploy the app to production + gcloud preview app deploy . + curl -X GET http://$PROJECT_ID.appspot.com diff --git a/app.yaml b/app.yaml new file mode 100644 index 0000000..57ca8e7 --- /dev/null +++ b/app.yaml @@ -0,0 +1,12 @@ +application: gcloud-node-todos +module: default +version: 1 +runtime: custom +api_version: 1 +vm: true +manual_scaling: + instances: 1 + +handlers: + - url: .* + script: dynamic diff --git a/key.json.enc b/key.json.enc new file mode 100644 index 0000000..06660b7 Binary files /dev/null and b/key.json.enc differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..d3e2fad --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "gcloud-node-todos", + "description": "todomvc sample for gcloud-node", + "version": "0.0.1", + "license": "Apache 2.0", + "dependencies": { + "express": "^4.5.1", + "body-parser": "^1.4.3", + "gcloud": "^0.6.0" + }, + "devDependencies": { + "jshint": "^2.5.2", + "dredd": "^0.3.9", + "request": "^2.42.0", + "gulp": "^3.8.8" + }, + "scripts": { + "lint": "jshint *.js", + "test": "gulp test" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..65540a3 --- /dev/null +++ b/server.js @@ -0,0 +1 @@ +require('./todos.js').listen(8080); diff --git a/test_hooks.js b/test_hooks.js new file mode 100644 index 0000000..525c009 --- /dev/null +++ b/test_hooks.js @@ -0,0 +1,43 @@ +var request = require('request'); +// imports the hooks module _injected_ by dredd. +var hooks = require('hooks'); + +hooks.before('Todos > Todo > Get a Todo', function(transaction, done) { + request.post({ + uri: 'http://localhost:8080/todos', + json: {'text': 'do that'} + }, function(err, res, todo) { + transaction.fullPath = '/todos/' + todo.id; + return done(); + }); +}); + +hooks.before('Todos > Todo > Delete a Todo', function(transaction, done) { + request.post({ + uri: 'http://localhost:8080/todos', + json: {'text': 'delete me'} + }, function(err, res, todo) { + transaction.fullPath = '/todos/' + todo.id; + return done(); + }); +}); + +hooks.after('Todos > Todo > Delete a Todo', function(transaction, done) { + request.get({ + uri: 'http://localhost:8080' + transaction.fullPath, + }, function(err, res, body) { + console.assert(res.statusCode == 404); + return done(); + }); +}); + +hooks.after('Todos > Todos Collection > Archive done Todos', function(transaction, done) { + request.get({ + uri: 'http://localhost:8080/todos' + }, function(err, res, body) { + JSON.parse(body).forEach(function(todo) { + console.assert(!todo.done); + }); + return done(); + }); +}); diff --git a/todos.apib b/todos.apib new file mode 100644 index 0000000..da47ea7 --- /dev/null +++ b/todos.apib @@ -0,0 +1,77 @@ +FORMAT: 1A + +# Todos API + +Todos API is a todo storage backend for [TodoMVC](//todomvc.com). + +# Group Todos + +# Todos Collection [/todos] + +## Create a Todo [POST] + ++ Request (application/json) + + { + "text": "do this" + } + ++ Response 201 (application/json; charset=utf-8) + + { + "id": 42, + "text": "do this", + "done": false + } + +## List all Todos [GET] + ++ Response 200 (application/json; charset=utf-8) + + [{ + "id": 42, + "text": "do this", + "done": false + }] + +## Archive done Todos [DELETE] + ++ Response 204 + +# Todo [/todos/{id}] + ++ Parameters + + id (required, number, `42`) + +## Get a Todo [GET] + ++ Response 200 (application/json; charset=utf-8) + + { + "id": 42, + "text": "do this", + "done": false + } + + +## Update a Todo [PUT] + ++ Request (application/json) + + { + "id": 42, + "text": "do this", + "done": true + } + ++ Response 200 (application/json; charset=utf-8) + + { + "id": 42, + "text": "do this", + "done": true + } + +## Delete a Todo [DELETE] + ++ Response 204 diff --git a/todos.js b/todos.js new file mode 100644 index 0000000..9456c1b --- /dev/null +++ b/todos.js @@ -0,0 +1,130 @@ +var express = require('express'), + bodyParser = require('body-parser'), + app = express(); + +var gcloud = require('gcloud'), + datastore = gcloud.datastore; + +var ds = new datastore.Dataset({ + projectId: process.env.GAE_LONG_APP_ID || process.env.DATASET_ID, + keyFilename: 'key.json' +}); + +app.use(bodyParser.json()); + +var todoListName = 'default-list'; + +app.get('/', function(req, res) { + res.set('Content-Type', 'text/plain') + .status(200) + .send('hello world 2'); +}); + +app.get('/todos', function(req, res) { + var q = ds.createQuery('Todo') + .hasAncestor(ds.key('TodoList', todoListName)); + ds.runQuery(q, function(err, items) { + if (err) { + console.error(err); + res.status(500).send(err.message); + return; + } + res.json(items.map(function(obj, i) { + obj.data.id = obj.key.path.pop(); + return obj.data; + })); + }); +}); + +app.get('/todos/:id', function(req, res) { + var id = req.param('id'); + ds.get(ds.key('TodoList', todoListName, 'Todo', id), function(err, obj) { + if (err) { + console.error(err); + res.status(500).send(err.message); + return; + } + if (!obj) { + return res.status(404).send(); + } + obj.data.id = obj.key.path.pop(); + res.json(obj.data); + }); +}); + +app.post('/todos', function(req, res) { + var todo = req.body; + todo.done = false; + ds.save({ + key: ds.key('TodoList', todoListName, 'Todo'), + data: todo + }, function(err, key) { + if (err) { + console.error(err); + res.status(500).send(err.message); + return; + } + todo.id = key.path.pop(); + res.status(201).json(todo); + }); +}); + +app.put('/todos/:id', function(req, res) { + var id = req.param('id'); + var todo = req.body; + ds.save({ + key: ds.key('TodoList', todoListName, 'Todo', id), + data: todo + }, function(err, key) { + if (err) { + console.error(err); + res.status(500).send(err.message); + return; + } + todo.id = id; + res.json(todo); + }); +}); + +app.delete('/todos/:id', function(req, res) { + var id = req.param('id'); + ds.delete(ds.key('TodoList', todoListName, 'Todo', id), function(err) { + if (err) { + console.error(err); + res.status(500).send(err.message); + return; + } + res.status(204).send(); + }); +}); + +app.delete('/todos', function(req, res) { + ds.runInTransaction(function(t, done) { + var q = ds.createQuery('Todo') + .hasAncestor(ds.key('TodoList', todoListName)) + .filter('done =', true); + t.runQuery(q, function(err, items) { + if (err) { + t.rollback(done); + console.error(err); + res.status(500).send(err.message); + return; + } + var keys = items.map(function(obj) { + return obj.key; + }); + t.delete(keys, function(err) { + if (err) { + t.rollback(done); + console.error(err); + res.status(500).send(err.message); + return; + } + done(); + res.status(204).send(); + }); + }); + }); +}); + +module.exports = app;