diff --git a/.eslintrc b/.eslintrc index 92e4ff7..627e81b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1 +1,7 @@ extends: airbnb +rules: + func-names: off + import/no-extraneous-dependencies: off + object-shorthand: off + require-yield: off + strict: off diff --git a/.gitignore b/.gitignore index 225e6ca..5199c68 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ /.s3-website.json +/.serverless /app/config.json /elm-stuff/ /node_modules/ +/npm-debug.log /public/ /env.sh diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..c60fb3b --- /dev/null +++ b/.npmignore @@ -0,0 +1,7 @@ +# package directories +elm-stuff +node_modules +jspm_packages + +# Serverless directories +.serverless diff --git a/api/challenge.js b/api/challenge.js new file mode 100644 index 0000000..534281b --- /dev/null +++ b/api/challenge.js @@ -0,0 +1,102 @@ +'use strict'; + +const AWS = require('aws-sdk'); +const base64 = require('base64'); // eslint-disable-line import/no-unresolved +const BPromise = require('bluebird'); +const genId = require('./gen-id'); +const router = require('koa-router'); + +AWS.config.setPromisesDependency(BPromise); +const dynamodb = new AWS.DynamoDB({ apiVersion: '2012-08-10' }); +const api = module.exports = router({ prefix: '/challenge' }); +const table = 'wordsnake_challenge'; + +const toChallenge = data => ({ + id: data.Item.id.S, + board: data.Item.board.S, + shape: data.Item.shape.S, + results: data.Item.results.L.map(result => ({ + player: result.M.player.S, + score: parseInt(result.M.score.N, 10), + words: result.M.words.SS, + })), +}); + +const getById = id => dynamodb.getItem({ + TableName: table, + Key: { id: { S: id } }, +}).promise().then(toChallenge); + +api.get('/:id', function* () { + this.body = yield getById(this.params.id); +}); + +const validateResult = (context) => { + const player = context.request.body.player; + const score = context.request.body.score; + const encodedToken = context.request.body.token; + if (!player || !score || !encodedToken) { + context.throw(400, 'Missing parameter'); + return null; + } + let token; + try { + token = JSON.parse(base64.atob(encodedToken)); + } catch (err) { + context.throw(400, 'Invalid token encoding'); + return null; + } + const board = token.b; + const shape = token.s; + const words = token.w; + if (!board || !shape || !words + || !/^[A-Z]{9,}$/.test(board) + || !/^(\d+)x(\d+)/.test(shape) + || !/^(([A-Z]{3,})(,[A-Z]{3,})*)?$/.test(words) + ) { + context.throw(400, 'Invalid token'); + return null; + } + return { + board: board, + shape: shape, + result: { + M: { + player: { S: player.toString() }, + score: { N: parseInt(score.toString(), 10).toString() }, + words: { SS: words.split(',') }, + }, + }, + }; +}; + +api.post('/', function* () { + const id = genId(); + const v = validateResult(this); + if (v) { + this.body = yield dynamodb.putItem({ + TableName: table, + Item: { + id: { S: id }, + board: { S: v.board }, + shape: { S: v.shape }, + results: { L: [v.result] }, + }, + }).promise().then(() => getById(id)); + } +}); + +api.post('/:id', function* () { + const id = this.params.id; + const v = validateResult(this); + this.body = yield dynamodb.updateItem({ + TableName: table, + Key: { id: { S: id } }, + AttributeUpdates: { + results: { + Action: 'ADD', + Value: { L: [v.result] }, + }, + }, + }).promise().then(() => getById(id)); +}); diff --git a/api/conf.js b/api/conf.js new file mode 100644 index 0000000..2a380de --- /dev/null +++ b/api/conf.js @@ -0,0 +1,9 @@ +'use strict'; + +const rc = require('rc'); + +const conf = rc('wordsnake', { + stage: 'dev', +}); + +module.exports = conf; diff --git a/api/gen-id.js b/api/gen-id.js new file mode 100644 index 0000000..c0d82aa --- /dev/null +++ b/api/gen-id.js @@ -0,0 +1,9 @@ +const randomstring = require('randomstring'); + +// 57 ** 7 = 1,954,897,493,193 +const genId = () => randomstring.generate({ + length: 7, + charset: 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789', +}); + +module.exports = genId; diff --git a/api/index.js b/api/index.js new file mode 100644 index 0000000..c312404 --- /dev/null +++ b/api/index.js @@ -0,0 +1,35 @@ +'use strict'; + +const bodyParser = require('koa-bodyparser'); +const koa = require('koa'); +const cors = require('koa-cors'); +const logger = require('./logger'); +const logging = require('@microservice/koa-logging'); +const middleware = require('./middleware'); +const router = require('koa-router'); +const serverless = require('serverless-http'); +const challenge = require('./challenge'); + +const routes = router(); +routes.use(middleware.error); +routes.use(logging(logger)); +routes.use(bodyParser()); + +routes.use(challenge.routes()); +routes.use(challenge.allowedMethods()); + +routes.get('/', function* () { + this.body = 'WordSnake API'; +}); + +module.exports.routes = routes; + +const app = koa(); +app.use(cors({ + origin: true, + credentials: true, + methods: ['GET', 'HEAD', 'OPTIONS'], +})); +app.use(routes.routes()); +app.use(routes.allowedMethods()); +module.exports.handler = serverless(app); diff --git a/api/logger.js b/api/logger.js new file mode 100644 index 0000000..65b130f --- /dev/null +++ b/api/logger.js @@ -0,0 +1,8 @@ +'use strict'; + +const conf = require('./conf'); + +module.exports = require('@microservice/logger')({ + name: 'wordsnake', + environment: conf.stage, +}); diff --git a/api/middleware.js b/api/middleware.js new file mode 100644 index 0000000..f199b1a --- /dev/null +++ b/api/middleware.js @@ -0,0 +1,13 @@ +'use strict'; + +const m = module.exports = {}; + +m.error = function* (next) { + try { + yield next; + } catch (err) { + this.status = err.status || err.statusCode || 500; + this.body = err.cause || (err.message ? { message: err.message } : err); + this.app.emit('error', err, this); + } +}; diff --git a/app/Challenge/Challenge.elm b/app/Challenge/Challenge.elm new file mode 100644 index 0000000..84dc8f3 --- /dev/null +++ b/app/Challenge/Challenge.elm @@ -0,0 +1,129 @@ +module Challenge.Challenge exposing (..) + +import Http +import Json.Decode as D exposing (at) +import Json.Encode as J +import Regex exposing (regex, HowMany(..)) +import Routing.Shape as Shape exposing (..) +import Task exposing (Task) +import Routing.Token as Token exposing (..) +import UrlParser exposing (..) + + +type alias Endpoint = + String + + +type alias Challenge = + { id : Id + , board : String + , shape : Shape + , results : List GameResult + } + + +type alias Id = + String + + +type alias GameResult = + { player : String + , score : Int + , words : List String + } + + +type Error + = HttpError Http.Error + | Error String + + +idParser : Parser (Id -> a) a +idParser = + UrlParser.custom "CHALLENGE_ID" + (\segment -> + case parseId segment of + Just id -> + Ok id + + Nothing -> + Err ("Bad id: " ++ segment) + ) + + +parseId : String -> Maybe String +parseId w = + w + |> Regex.find (AtMost 1) (regex "^([0-9A-Za-z]{7})$") + |> List.head + |> Maybe.andThen + (\m -> + m.submatches + |> List.head + |> Maybe.andThen (\id -> id) + ) + + + +-- API + + +get : Endpoint -> Id -> Task Error Challenge +get endpoint id = + Http.get (endpoint ++ "/challenge/" ++ id) challengeDecoder + |> Http.toTask + |> Task.mapError HttpError + + +create : Endpoint -> ( String, Int, Token ) -> Task Error Challenge +create endpoint gameResult = + Http.request + { method = "POST" + , headers = [] + , url = endpoint ++ "/challenge" + , body = Http.jsonBody (gameResult |> gameResultEncoder) + , expect = Http.expectJson challengeDecoder + , timeout = Nothing + , withCredentials = False + } + |> Http.toTask + |> Task.mapError HttpError + + +update : Endpoint -> Id -> ( String, Int, Token ) -> Task Error Challenge +update endpoint id gameResult = + Task.fail (Error "not implemented") + + + +-- ENCODER + + +gameResultEncoder : ( String, Int, Token ) -> J.Value +gameResultEncoder ( player, score, token ) = + J.object + [ ( "player", J.string player ) + , ( "score", J.int score ) + , ( "token", token |> Token.toPathComponent |> J.string ) + ] + + + +-- DECODER + + +challengeDecoder : D.Decoder Challenge +challengeDecoder = + D.map4 Challenge + (at [ "id" ] D.string) + (at [ "board" ] D.string) + (at [ "shape" ] Shape.decoder) + (at [ "results" ] (D.list gameResultDecoder)) + + +gameResultDecoder : D.Decoder GameResult +gameResultDecoder = + D.map3 GameResult + (at [ "player" ] D.string) + (at [ "score" ] D.int) + (at [ "words" ] (D.list D.string)) diff --git a/app/Config/Config.elm b/app/Config/Config.elm index b19d4ac..11cfd6d 100644 --- a/app/Config/Config.elm +++ b/app/Config/Config.elm @@ -8,8 +8,14 @@ import Window -- MODEL +type alias Endpoints = + { api : String + , dictionaryApi : String + } + + type alias Model = - { apiEndpoint : String + { endpoints : Endpoints , isMobile : Bool , language : Lang , forkMe : Maybe String @@ -19,7 +25,7 @@ type alias Model = empty : Model empty = - Model "" + Model (Endpoints "" "") False Lang.default Nothing @@ -27,8 +33,13 @@ empty = isEmpty : Model -> Bool -isEmpty = - .apiEndpoint >> String.isEmpty +isEmpty model = + model.endpoints.api |> String.isEmpty + + +setEndpoints : Endpoints -> Model -> Model +setEndpoints endpoints model = + { model | endpoints = endpoints } setWindowSize : Window.Size -> Model -> Model @@ -41,10 +52,10 @@ sizeDidChange size model = size.width /= model.windowSize.width || size.height /= model.windowSize.height -decoder : D.Decoder Model -decoder = +decoder : Endpoints -> D.Decoder Model +decoder endpoints = D.map5 Model - (at [ "apiEndpoint" ] D.string) + (D.succeed endpoints) (at [ "isMobile" ] D.bool) (at [ "language" ] Lang.decoder) (D.maybe <| at [ "forkMe" ] D.string) diff --git a/app/GameMode.elm b/app/GameMode.elm index e6cacdc..eafe08f 100644 --- a/app/GameMode.elm +++ b/app/GameMode.elm @@ -8,7 +8,6 @@ type GameMode | Waiting | Playing | Reviewing - | Comparing toString : GameMode -> String @@ -25,6 +24,3 @@ toString gameMode = Reviewing -> "reviewing" - - Comparing -> - "comparing" diff --git a/app/Main.elm b/app/Main.elm index 3c3f7e2..40cee42 100644 --- a/app/Main.elm +++ b/app/Main.elm @@ -4,6 +4,7 @@ import Board.Board as Board import Board.Cell as Cell import Board.Rand as Rand exposing (..) import Board.Snake as Snake +import Challenge.Challenge as Challenge exposing (..) import Config.Config as Config import Dom import GameMode exposing (..) @@ -32,9 +33,9 @@ import Word.Score as Score import Window -main : Program Never Model Msg +main : Program Config.Endpoints Model Msg main = - Navigation.program UrlChange + Navigation.programWithFlags UrlChange { init = init Config.empty , update = update , view = view @@ -49,6 +50,7 @@ main = type alias Model = { config : Config.Model , configError : String + , challenge : Maybe Challenge , board : Board.Model , gameMode : GameMode , seed : Random.Seed @@ -70,6 +72,7 @@ reset config = Model config "" + Nothing (Board.reset Shape.default (cellWidth config Shape.default) []) Loading (Random.initialSeed 0) @@ -77,7 +80,7 @@ reset config = Share.reset Snake.reset (Timer.reset Shape.default) - (Word.List.reset config.apiEndpoint) + (Word.List.reset config.endpoints.dictionaryApi) @@ -86,6 +89,7 @@ reset config = type Msg = CellClicked Cell.Model Cell.DisplayType + | ChallengeResult (Result Challenge.Error Challenge) | FocusResult (Result Dom.Error ()) | GotoBoard Shape Seed | KeyDown KeyCode @@ -98,11 +102,11 @@ type Msg | WindowSize (Result String Window.Size) -init : Config.Model -> Navigation.Location -> ( Model, Cmd Msg ) -init config location = +init : Config.Model -> Config.Endpoints -> Navigation.Location -> ( Model, Cmd Msg ) +init config endpoints location = let model = - reset config + reset (Config.setEndpoints endpoints config) route = Routing.routeFromLocation location @@ -151,6 +155,12 @@ init config location = |> Cmd.batch ) + ChallengeRoute id -> + ( model + , Challenge.get endpoints.api id + |> Task.attempt ChallengeResult + ) + ReviewRoute token -> refreshWords model.config (updateBoardSize @@ -180,6 +190,22 @@ update msg model = Cell.HighlightTail -> ( { model | snake = Snake.reset }, Cmd.none ) + ChallengeResult r -> + case r of + Ok challenge -> + ( updateBoardSize + { model + | board = Board.fromToken challenge.shape challenge.board + , challenge = Just challenge + , shape = challenge.shape + , gameMode = Waiting + } + , Cmd.none + ) + + Err err -> + ( model, Cmd.none ) + FocusResult error -> ( model, Cmd.none ) @@ -200,7 +226,7 @@ update msg model = LoadConfig data -> let result = - decodeValue Config.decoder data + decodeValue (Config.decoder model.config.endpoints) data in case result of Ok config -> @@ -210,7 +236,11 @@ update msg model = ( { model | configError = err }, Cmd.none ) ShareMsg shareMsg -> - Share.updateOne ShareMsg shareMsg model + Share.updateOne + ( model.config.endpoints.api, model.wordList |> Word.List.totalScore, model |> toToken ) + ShareMsg + shareMsg + model Shuffle change -> ( model @@ -223,7 +253,7 @@ update msg model = tick model time UrlChange location -> - init model.config location + init model.config model.config.endpoints location WordListMessage cMsg -> case cMsg of @@ -301,7 +331,7 @@ refreshWords config model = let ( newWordList, cmd ) = Word.List.validate - config.apiEndpoint + config.endpoints.dictionaryApi (Snake.findBonus model.board.layer) model.wordList in @@ -316,6 +346,14 @@ refreshWords config model = ) +toToken : Model -> Token +toToken model = + Token + (model.board |> Board.toToken) + model.shape + model.wordList + + tick : Model -> Time -> ( Model, Cmd Msg ) tick model time = let @@ -333,10 +371,8 @@ tick model time = | timer = Timer.zero , gameMode = Reviewing } - , Token - (model.board |> Board.toToken) - model.shape - model.wordList + , model + |> toToken |> Routing.reviewUrl FocusResult ) else @@ -449,9 +485,6 @@ view model = Loading -> h1 [ class "m4" ] [ text "Loading..." ] - Comparing -> - h1 [ class "m4" ] [ text "Comparing..." ] - _ -> boardView model ] diff --git a/app/Routing/Routing.elm b/app/Routing/Routing.elm index 0860d80..7b849f9 100644 --- a/app/Routing/Routing.elm +++ b/app/Routing/Routing.elm @@ -1,6 +1,7 @@ module Routing.Routing exposing (..) import Board.Rand as Rand exposing (..) +import Challenge.Challenge as Challenge exposing (..) import Dom import Navigation import Routing.Shape as Shape exposing (..) @@ -14,6 +15,7 @@ type Route | RandomPlayRoute Shape | PlayRoute Shape Seed | ReviewRoute Token + | ChallengeRoute Challenge.Id randomPlayUrl : Shape -> Cmd msg @@ -42,10 +44,18 @@ reviewUrl focusHandler token = ] +challengeUrl : Challenge.Id -> Cmd msg +challengeUrl id = + "#play/" + ++ id + |> Navigation.newUrl + + route : Parser (Route -> a) a route = oneOf - [ map PlayRoute (s "play" Shape.parser int) + [ map ChallengeRoute (s "play" Challenge.idParser) + , map PlayRoute (s "play" Shape.parser int) , map RandomPlayRoute (s "play" Shape.parser) , map (RandomPlayRoute Shape.default) (s "play") , map (RandomPlayRoute Shape.default) UrlParser.top diff --git a/app/Routing/Shape.elm b/app/Routing/Shape.elm index 4819a34..5e120b9 100644 --- a/app/Routing/Shape.elm +++ b/app/Routing/Shape.elm @@ -10,19 +10,6 @@ import UrlParser exposing (..) -- MODEL -type Route - = BlogList (Maybe String) (Maybe String) - | BlogPost Int - - -route : Parser (Route -> a) a -route = - oneOf - [ map BlogList (s "blog" stringParam "search" stringParam "page") - , map BlogPost (s "blog" int) - ] - - type Shape = Shape Int Int diff --git a/app/Routing/Token.elm b/app/Routing/Token.elm index 716f2f6..123a1b6 100644 --- a/app/Routing/Token.elm +++ b/app/Routing/Token.elm @@ -22,11 +22,8 @@ type alias Token = toPathComponent : Token -> String toPathComponent token = case - J.object - [ ( "b", J.string token.board ) - , ( "s", J.string (token.shape |> Shape.toPathComponent) ) - , ( "w", Word.List.toJsonValue token.words ) - ] + token + |> encoder |> J.encode 0 |> Base64.encode of @@ -37,6 +34,15 @@ toPathComponent token = "error" +encoder : Token -> D.Value +encoder token = + J.object + [ ( "b", J.string token.board ) + , ( "s", J.string (token.shape |> Shape.toPathComponent) ) + , ( "w", Word.List.toJsonValue token.words ) + ] + + decoder : D.Decoder Token decoder = D.map3 Token diff --git a/app/Share.elm b/app/Share.elm index c4f41d2..407403e 100644 --- a/app/Share.elm +++ b/app/Share.elm @@ -1,21 +1,26 @@ module Share exposing (..) +import Challenge.Challenge as Challenge exposing (..) import ChildUpdate import Html exposing (..) import Html.Attributes exposing (..) +import Html.Events exposing (onClick, onInput) +import Routing.Token as Token exposing (..) +import Task -- MODEL type alias Share = - { username : String + { player : String + , challengeId : String } reset : Share reset = - Share "" + Share "" "" @@ -26,9 +31,9 @@ type alias HasOne model = { model | share : Share } -updateOne : (Msg -> msg) -> Msg -> HasOne m -> ( HasOne m, Cmd msg ) -updateOne = - ChildUpdate.updateOne update .share (\m x -> { m | share = x }) +updateOne : ( String, Int, Token ) -> (Msg -> msg) -> Msg -> HasOne m -> ( HasOne m, Cmd msg ) +updateOne extra = + ChildUpdate.updateOne (update extra) .share (\m x -> { m | share = x }) @@ -37,13 +42,31 @@ updateOne = type Msg = Challenge + | ChallengeResult (Result Challenge.Error Challenge) + | UpdatePlayer String -update : Msg -> Share -> ( Share, Cmd Msg ) -update msg model = +update : ( String, Int, Token ) -> Msg -> Share -> ( Share, Cmd Msg ) +update ( apiEndpoint, score, token ) msg model = case msg of Challenge -> - ( model, Cmd.none ) + ( model + , Challenge.create + apiEndpoint + ( model.player, score, token ) + |> Task.attempt ChallengeResult + ) + + ChallengeResult r -> + case r of + Ok challenge -> + ( { model | challengeId = challenge.id }, Cmd.none ) + + Err err -> + ( model, Cmd.none ) + + UpdatePlayer name -> + ( { model | player = name }, Cmd.none ) @@ -52,40 +75,57 @@ update msg model = view : Bool -> Share -> Html Msg -> Html Msg view isMobile share timer = - div [ class "share clearfix" ] - (if isMobile then - [ div [ class "box rounded" ] [ mobileShareView share ] ] - else - [ div [ class "col col-5" ] [ timer ] - , div [ class "box rounded px2 col col-7" ] [ shareView share ] - ] - ) + let + parts = + ( [ div [] [ text "Enter your name" ] + , input + [ id "username" + , onInput UpdatePlayer + , type_ "text" + ] + [] + ] + , [ challengeButton share + , if String.isEmpty share.challengeId then + span [] [] + else + input [ value share.challengeId ] [] + ] + ) + in + div [ class "share clearfix" ] + (if isMobile then + [ div [ class "box rounded" ] [ mobileShareView share parts ] ] + else + [ div [ class "col col-5" ] [ timer ] + , div [ class "box rounded px2 col col-7" ] [ shareView share parts ] + ] + ) + +challengeButton : Share -> Html Msg +challengeButton share = + let + attr = + if share.player |> String.isEmpty then + [ class "button disabled" ] + else + [ class "button", onClick Challenge ] + in + a attr [ text "Challenge a friend" ] -shareView : Share -> Html Msg -shareView share = + +shareView : Share -> ( List (Html Msg), List (Html Msg) ) -> Html Msg +shareView share ( player, challenge ) = div [ class "clearfix" ] - [ div [ class "col col-5" ] - [ div [] [ text "Enter your name" ] - , input [ id "username", type_ "text" ] [] - ] + [ div [ class "col col-5" ] player , div [ class "col col-2" ] [ div [ class "chevron fa fa-chevron-right" ] [] ] - , div [ class "col col-5" ] - [ div [] [ text "Challenge a friend" ] - , div [ class "gray coming-soon" ] - [ text "Coming soon..." - ] - ] + , div [ class "col col-5" ] challenge ] -mobileShareView : Share -> Html Msg -mobileShareView share = - div [] - [ div [] [ text "Enter your name" ] - , input [ id "username", type_ "text" ] [] - , div [] [ text "Challenge a friend" ] - , div [ class "gray coming-soon" ] [ text "Coming soon..." ] - ] +mobileShareView : Share -> ( List (Html Msg), List (Html Msg) ) -> Html Msg +mobileShareView share ( player, challenge ) = + div [] (List.concat [ player, challenge ]) diff --git a/app/Timer.elm b/app/Timer.elm index 96d2176..7b6d345 100644 --- a/app/Timer.elm +++ b/app/Timer.elm @@ -84,9 +84,6 @@ view gameMode timer = Reviewing -> 0 - Comparing -> - 0 - _ -> (timer.timeAllowed - timer.timeElapsed) / 1000 diff --git a/app/Word/List.elm b/app/Word/List.elm index 908b6b8..c0df626 100644 --- a/app/Word/List.elm +++ b/app/Word/List.elm @@ -25,8 +25,8 @@ type alias Model = reset : String -> Model -reset apiEndpoint = - Model (Eng.Config apiEndpoint) [] +reset dictionaryApiEndpoint = + Model (Eng.Config dictionaryApiEndpoint) [] @@ -147,10 +147,10 @@ addWord model text bonus = validate : String -> (String -> Bonus) -> Model -> ( Model, Cmd Msg ) -validate apiEndpoint bonusFinder model = +validate dictionaryApiEndpoint bonusFinder model = let engConfig = - Eng.Config apiEndpoint + Eng.Config dictionaryApiEndpoint ( newWords, cmds ) = model.words diff --git a/app/index.js b/app/index.js index 51c35ae..22f33c0 100644 --- a/app/index.js +++ b/app/index.js @@ -16,7 +16,10 @@ font.setAttribute('rel', 'stylesheet'); doc.head.appendChild(font); const mountNode = doc.getElementById('main'); -const app = Elm.Main.embed(mountNode); +const app = Elm.Main.embed(mountNode, { + api: config.apiEndpoint, + dictionaryApi: config.dictionaryApiEndpoint, +}); setTimeout(() => app.ports.config.send(config), diff --git a/app/style.scss b/app/style.scss index 3447ed8..010e4d2 100644 --- a/app/style.scss +++ b/app/style.scss @@ -10,7 +10,7 @@ body { position: relative; } -.shuffle { +.shuffle, .button { color: #666; cursor: pointer; font-size: 2.5vh; diff --git a/local-server.js b/local-server.js new file mode 100644 index 0000000..7d759fd --- /dev/null +++ b/local-server.js @@ -0,0 +1,24 @@ +const koa = require('koa'); +const api = require('./api').routes; +const cors = require('koa-cors'); + +const port = 6413; + +const app = koa(); +app.use(cors({ + origin: true, + credentials: true, + methods: ['GET', 'HEAD', 'OPTIONS'], +})); +app.use(api.routes()); +app.use(api.allowedMethods()); + +module.exports = app; + +if (require.main === module) { + app.listen(port, (err) => { + if (err) { throw err; } + // eslint-disable-next-line no-console + console.log(`Started on http://localhost:${port}`); + }); +} diff --git a/package.json b/package.json index c895a34..5f862cc 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,14 @@ "main": "index.js", "scripts": { "build": "node scripts/write-config.js && webpack", - "start": "node scripts/write-config.js && webpack-dev-server", + "debug:api": "supervisor local-server.js", + "debug:app": "node scripts/write-config.js && webpack-dev-server", + "start": "npm run debug:api & npm run debug:app", "lint": "eslint .", "test": "npm run lint", - "deploy": "npm run build && node node_modules/s3-website/s3-website.js deploy" + "deploy:api": "serverless deploy", + "deploy:app": "npm run build && node node_modules/s3-website/s3-website.js deploy", + "deploy": "npm run deploy:api && npm run deploy:app" }, "author": "Kevin Tonon ", "license": "MIT", @@ -27,17 +31,31 @@ "webpack-dev-server": "^1.16.2" }, "devDependencies": { + "@microservice/koa-logging": "^1.0.0", + "@microservice/logger": "^1.0.0", + "aws-sdk": "^2.7.20", "babel-core": "^6.13.2", "babel-loader": "^6.2.4", "babel-preset-es2015": "^6.13.2", + "bluebird": "^3.4.7", "eslint": "^3.3.0", "eslint-config-airbnb": "^10.0.0", "eslint-plugin-import": "^1.13.0", "eslint-plugin-jsx-a11y": "^2.1.0", "eslint-plugin-react": "^6.0.0", "json-loader": "^0.5.4", + "koa": "^1.2.4", + "koa-bodyparser": "^2.3.0", + "koa-cors": "0.0.16", + "koa-router": "^5.4.0", "node-sass": "^4.1.1", + "randomstring": "^1.1.5", + "rc": "^1.1.6", "s3-website": "^2.1.0", - "sass-loader": "^4.1.1" + "sass-loader": "^4.1.1", + "serverless": "^1.4.0", + "serverless-http": "^1.1.0", + "serverless-plugin-include-dependencies": "^1.1.0", + "supervisor": "^0.12.0" } } diff --git a/scripts/write-config.js b/scripts/write-config.js index 7d31fe6..4bd37ba 100644 --- a/scripts/write-config.js +++ b/scripts/write-config.js @@ -3,7 +3,8 @@ const path = require('path'); const rc = require('rc'); const conf = rc('wordSnake', { - apiEndpoint: 'http://localhost:7631', + apiEndpoint: 'http://localhost:6413', + dictionaryApiEndpoint: 'http://localhost:7631', language: 'en', }); diff --git a/serverless.yml b/serverless.yml new file mode 100644 index 0000000..63d6170 --- /dev/null +++ b/serverless.yml @@ -0,0 +1,44 @@ +service: wordsnake + +frameworkVersion: "=1.4.0" + +plugins: + - serverless-plugin-include-dependencies + +provider: + name: aws + runtime: nodejs4.3 + stage: dev + region: us-east-1 + iamRoleStatements: + - Effect: "Allow" + Action: + - "dynamodb:GetItem" + - "dynamodb:PutItem" + - "dynamodb:UpdateItem" + Resource: + - "*" + +package: + individually: true + include: + - api/** + exclude: + - app/** + - elm-stuff/** + - node_modules/** + - public/** + - scripts/** + +functions: + api: + handler: api.handler + events: + - http: + integration: lambda-proxy + path: / + method: ANY + - http: + integration: lambda-proxy + path: /{any+} + method: ANY