├── .eslintignore ├── .travis.yml ├── .eslintrc.js ├── .gitignore ├── webpack.config.js ├── LICENSE ├── posts ├── index.js └── BlogStorage.js ├── package.json ├── test └── posts.js ├── serverless.yml └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tmp -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4.3.2' 4 | script: 5 | - npm test 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "airbnb", 3 | "plugins": [], 4 | "rules": { 5 | "func-names": "off", 6 | 7 | // doesn't work in node v4 :( 8 | "strict": "off", 9 | "prefer-rest-params": "off", 10 | "react/require-extension" : "off", 11 | "import/no-extraneous-dependencies" : "off" 12 | }, 13 | "env": { 14 | "mocha": true 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | dist 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 29 | node_modules 30 | 31 | #IDE Stuff 32 | **/.idea 33 | 34 | #OS STUFF 35 | .DS_Store 36 | .tmp 37 | 38 | #SERVERLESS STUFF 39 | admin.env 40 | .env 41 | _meta 42 | .serverless -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 3 | const yaml = require('node-yaml'); 4 | const nodeExternals = require('webpack-node-externals'); 5 | 6 | function getFunctions() { 7 | const serverlessYml = yaml.readSync('serverless.yml'); 8 | const webPackFunctions = {}; 9 | const functionNames = Object.keys(serverlessYml.functions || {}); 10 | functionNames.forEach((name) => { 11 | const handlerFile = serverlessYml.functions[name].handler.replace(/.[^.]*$/, ''); 12 | webPackFunctions[handlerFile] = [`./${handlerFile}.js`]; 13 | }); 14 | return webPackFunctions; 15 | } 16 | 17 | module.exports = { 18 | entry: getFunctions(), 19 | target: 'node', 20 | module: { 21 | loaders: [ 22 | { test: /\.json/, loader: 'json-loader' }, 23 | ], 24 | }, 25 | plugins: [ 26 | new CopyWebpackPlugin([ 27 | { from: '.env' }, 28 | ]), 29 | ], 30 | output: { 31 | libraryTarget: 'commonjs', 32 | path: path.join(__dirname, '.webpack'), 33 | filename: '[name].js', 34 | }, 35 | externals: [nodeExternals()], 36 | }; 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 SC5 Online Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /posts/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BlogStorage = require('./BlogStorage'); 4 | const AWS = require('aws-sdk'); 5 | 6 | const config = { 7 | region: AWS.config.region || process.env.SERVERLESS_REGION || 'eu-west-1', 8 | }; 9 | 10 | const dynamodb = new AWS.DynamoDB.DocumentClient(config); 11 | 12 | module.exports.handler = (event, context, cb) => { 13 | const storage = new BlogStorage(dynamodb); 14 | 15 | switch (event.method) { 16 | case 'GET': 17 | storage.getPosts({}) 18 | .then(response => cb(null, response)) 19 | .catch(cb); 20 | break; 21 | case 'POST': 22 | storage.savePost(event.body) 23 | .then(response => cb(null, response)) 24 | .catch(cb); 25 | break; 26 | case 'PUT': 27 | storage.updatePost(event.path.id, event.body) 28 | .then(response => cb(null, response)) 29 | .catch(cb); 30 | break; 31 | case 'DELETE': 32 | storage.deletePost(event.path.id) 33 | .then(response => cb(null, response)) 34 | .catch(cb); 35 | break; 36 | default: 37 | cb(`Unknown method "${event.method}".`); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-blog-workshop", 3 | "version": "1.0.0", 4 | "description": "A boilerplate for Serverless applications by SC5 Online", 5 | "main": "fnHello/handler.js", 6 | "keywords": [ 7 | "serverless", 8 | "aws" 9 | ], 10 | "author": "Mikael Puittinen", 11 | "contributors": [ 12 | "Eetu Tuomala" 13 | ], 14 | "license": "MIT", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/SC5/mpu-serverless-blog.git" 18 | }, 19 | "dependencies": {}, 20 | "devDependencies": { 21 | "aws-sdk": "^2.7.10", 22 | "copy-webpack-plugin": "^3.0.1", 23 | "eslint": "^3.3.1", 24 | "eslint-config-airbnb": "^10.0.1", 25 | "eslint-config-airbnb-base": "^5.0.2", 26 | "eslint-plugin-import": "^1.13.0", 27 | "eslint-plugin-jsx-a11y": "^2.1.0", 28 | "eslint-plugin-react": "^6.1.1", 29 | "json-loader": "^0.5.4", 30 | "node-yaml": "^3.0.3", 31 | "serverless-mocha-plugin": "^1.3.2", 32 | "serverless-offline": "^3.2.1", 33 | "serverless-webpack": "^1.0.0-rc.3", 34 | "webpack-node-externals": "^1.5.4" 35 | }, 36 | "scripts": { 37 | "test": "SLS_DEBUG=true serverless invoke test", 38 | "lint": "eslint ." 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /posts/BlogStorage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class BlogStorage { 4 | constructor(dynamodb) { 5 | this.dynamodb = dynamodb; 6 | this.baseParams = { 7 | TableName: process.env.TABLE_NAME, 8 | }; 9 | } 10 | 11 | // Get all posts 12 | // @see: http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html#scan-property 13 | getPosts() { 14 | const params = Object.assign({}, this.baseParams, { 15 | AttributesToGet: [ 16 | 'id', 17 | 'title', 18 | 'content', 19 | 'date', 20 | ], 21 | }); 22 | 23 | return this.dynamodb.scan(params).promise(); 24 | } 25 | 26 | // Add new post 27 | // @see: http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html#put-property 28 | savePost(post) { 29 | const date = Date.now(); 30 | const id = date.toString(); 31 | const payload = Object.assign({}, post, { id, date }); 32 | const params = Object.assign({}, this.baseParams, { Item: payload }); 33 | return this.dynamodb.put(params).promise() 34 | .then(() => ({ post: payload })); 35 | } 36 | 37 | // Edit post 38 | // @see: http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html#put-property 39 | updatePost(id, post) { 40 | const payload = Object.assign({}, post, { id }); 41 | const params = Object.assign({}, this.baseParams, { Item: payload }); 42 | 43 | return this.dynamodb.put(params).promise() 44 | .then(() => ({ post: payload })); 45 | } 46 | 47 | // Delete post 48 | // @see: http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html#delete-property 49 | deletePost(id) { 50 | const params = Object.assign({}, this.baseParams, 51 | { Key: { id } } 52 | ); 53 | 54 | return this.dynamodb.delete(params).promise(); 55 | } 56 | } 57 | 58 | module.exports = BlogStorage; 59 | -------------------------------------------------------------------------------- /test/posts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // tests for posts 4 | // Generated by serverless-mocha-plugin 5 | 6 | const mod = require('../posts/index'); 7 | const mochaPlugin = require('serverless-mocha-plugin'); 8 | 9 | const wrapper = mochaPlugin.lambdaWrapper; 10 | const expect = mochaPlugin.chai.expect; 11 | 12 | const wrapped = wrapper.wrap(mod, { handler: 'handler' }); 13 | 14 | describe('posts', () => { 15 | let post; 16 | 17 | it('creates a post', () => 18 | wrapped.run({ 19 | method: 'POST', 20 | body: { 21 | title: 'Test post', 22 | content: 'Test content', 23 | }, 24 | }).then((response) => { 25 | post = response.post; 26 | expect(post.id).not.to.be.equal(null); 27 | })); 28 | 29 | it('updates the post', () => 30 | wrapped.run({ 31 | method: 'PUT', 32 | path: { 33 | id: post.id, 34 | }, 35 | body: { 36 | title: 'Test post edited', 37 | content: 'Test content edited', 38 | date: post.date, 39 | }, 40 | }).then((response) => { 41 | post = response.post; 42 | expect(post.id).not.to.be.equal(null); 43 | })); 44 | 45 | it('gets the post', () => 46 | wrapped.run({ 47 | method: 'GET', 48 | }).then((response) => { 49 | const createdPost = response.Items.filter(item => item.id === post.id)[0]; 50 | expect(createdPost.id).to.be.equal(post.id); 51 | expect(createdPost.title).to.be.equal('Test post edited'); 52 | expect(createdPost.content).to.be.equal('Test content edited'); 53 | expect(createdPost.date).to.be.equal(post.date); 54 | })); 55 | 56 | it('deletes a post', () => 57 | wrapped.run({ 58 | method: 'DELETE', 59 | path: { 60 | id: post.id, 61 | }, 62 | })); 63 | 64 | it('checks that the post is deleted', () => 65 | wrapped.run({ 66 | method: 'GET', 67 | }).then((response) => { 68 | const createdPost = response.Items.filter(item => item.id === post.id); 69 | expect(createdPost).to.be.eql([]); 70 | })); 71 | }); 72 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | frameworkVersion: ">=1.2.0 <2.0.0" 2 | 3 | service: serverless-blog-workshop-ref # NOTE: update this with your service name 4 | 5 | provider: 6 | name: aws 7 | runtime: nodejs4.3 8 | environment: 9 | SERVERLESS_STAGE: ${opt:stage, self:provider.stage} 10 | SERVERLESS_PROJECT: ${self:service} 11 | SERVERLESS_REGION: ${opt:region, self:provider.region} 12 | TABLE_NAME: ${self:provider.environment.SERVERLESS_PROJECT}-${self:provider.environment.SERVERLESS_STAGE} 13 | iamRoleStatements: 14 | - Effect: Allow 15 | Action: 16 | - dynamodb:* 17 | Resource: arn:aws:dynamodb:${self:provider.environment.SERVERLESS_REGION}:*:* 18 | - Effect: Allow 19 | Action: 20 | - SNS:* 21 | Resource: arn:aws:sns:${self:provider.environment.SERVERLESS_REGION}:*:* 22 | package: 23 | exclude: 24 | - test/** 25 | - .git/** 26 | 27 | functions: 28 | posts: 29 | handler: posts/index.handler 30 | events: 31 | - http: 32 | path: posts 33 | method: get 34 | cors: true 35 | integration: lambda 36 | - http: 37 | path: posts 38 | method: post 39 | cors: true 40 | integration: lambda 41 | - http: 42 | path: posts/{id} 43 | method: put 44 | cors: true 45 | integration: lambda 46 | - http: 47 | path: posts/{id} 48 | method: delete 49 | cors: true 50 | integration: lambda 51 | 52 | plugins: 53 | - serverless-mocha-plugin 54 | - serverless-webpack 55 | - serverless-offline 56 | 57 | custom: 58 | webpackIncludeModules: true 59 | 60 | resources: 61 | Resources: 62 | # DynamoDB Blog table for workshop 63 | BlogTable: 64 | Type: AWS::DynamoDB::Table 65 | DeletionPolicy: Retain 66 | Properties: 67 | AttributeDefinitions: 68 | - AttributeName: id 69 | AttributeType: S 70 | KeySchema: 71 | - AttributeName: id 72 | KeyType: HASH 73 | ProvisionedThroughput: 74 | ReadCapacityUnits: 1 75 | WriteCapacityUnits: 1 76 | TableName: ${self:provider.environment.TABLE_NAME} 77 | 78 | # SessionsTable: 79 | # Type: AWS::DynamoDB::Table 80 | # DeletionPolicy: Delete 81 | # Properties: 82 | # AttributeDefinitions: 83 | # - AttributeName: id 84 | # AttributeType: S 85 | # KeySchema: 86 | # - AttributeName: id 87 | # KeyType: HASH 88 | # ProvisionedThroughput: 89 | # ReadCapacityUnits: 1 90 | # WriteCapacityUnits: 1 91 | # TableName: ${self:service}-sessions-${opt:stage, self:provider.stage} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless Blog Workshop by SC5 2 | 3 | Example backend project for AWS - Serverless hackathon. 4 | 5 | Project is compatible with Serverless v1 6 | 7 | ## Step by step instructions for building the project with Serverless Framework v1.5 8 | 9 | ### Setup project 10 | 11 | * Create the service from the `sc5-serverless-boilerplate` 12 | ```bash 13 | > sls install -u https://github.com/SC5/sc5-serverless-boilerplate -n serverless-blog 14 | > cd serverless-blog 15 | > npm install 16 | ``` 17 | 18 | ### Set up storage (DynamoDB) 19 | 20 | * Un-comment `Resources:` and `resources:` in `serverless.yml`. 21 | 22 | ``` 23 | # DynamoDB Blog table for workshop 24 | BlogTable: 25 | Type: AWS::DynamoDB::Table 26 | DeletionPolicy: Retain 27 | Properties: 28 | AttributeDefinitions: 29 | - AttributeName: id 30 | AttributeType: S 31 | KeySchema: 32 | - AttributeName: id 33 | KeyType: HASH 34 | ProvisionedThroughput: 35 | ReadCapacityUnits: 1 36 | WriteCapacityUnits: 1 37 | TableName: ${self:provider.environment.TABLE_NAME} 38 | ``` 39 | 40 | ### Create function and endpoints 41 | 42 | * Create the function 43 | ```bash 44 | sls create function -f posts --handler posts/index.handler 45 | ``` 46 | 47 | * Register HTTP endpoints by adding the following to the function definition in `serverless.yml` 48 | ``` 49 | events: 50 | - http: 51 | path: posts 52 | method: get 53 | cors: true 54 | integration: lambda 55 | - http: 56 | path: posts 57 | method: posts 58 | cors: true 59 | integration: lambda 60 | - http: 61 | path: posts/{id} 62 | method: put 63 | cors: true 64 | integration: lambda 65 | - http: 66 | path: posts/{id} 67 | method: delete 68 | cors: true 69 | integration: lambda 70 | ``` 71 | 72 | ### Implement the functionality 73 | 74 | * Copy `posts/index.js` and `posts/BlogStorage.js` from this repo to your service (`posts` folder) 75 | 76 | ### Deploy and test 77 | 78 | * Deploy the resources (and functions) using 79 | 80 | ``` 81 | sls deploy 82 | ```` 83 | 84 | * Copy tests from `test/posts.js` in this repo to your service 85 | * Run `serveless-mocha-plugin` tests 86 | 87 | ``` 88 | sls invoke test --region us-east-1 --stage dev 89 | ``` 90 | 91 | ### Set up your blog application 92 | 93 | * Launch the blog application 94 | * Enter the service Url (https://..../posts). The service URL can be retrieved using 95 | ``` 96 | sls info 97 | ``` 98 | 99 | #### Enjoy, your ready to go! 100 | 101 | # Feedback 102 | mikael.puittinen@sc5.io 103 | --------------------------------------------------------------------------------