Skip to content
Closed
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
94 changes: 94 additions & 0 deletions rps-ts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@

# Created by https://www.gitignore.io/api/node
# Edit at https://www.gitignore.io/?templates=node

### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
.env.test

# parcel-bundler cache (https://parceljs.org/)
.cache

# next.js build output
.next

# nuxt.js build output
.nuxt

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# End of https://www.gitignore.io/api/node

!*.sql
**/build
28 changes: 28 additions & 0 deletions rps-ts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# rps-ts

This is a simple demonstration of a typescript backend setup
The app consist in the rps game

In order to run it, setup the database:

```
cd db && docker-compose up -d
```

Install packages:
`yarn`

Run The Server:
`yarn start`

Play a game with an HTTP client:

POST `http://localhost:8080/play`
```
{
"move": "Rock"
}
```

retrieve last game result
GET `http://localhost:8080/play`
11 changes: 11 additions & 0 deletions rps-ts/db/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: "3"
services:
rps-db:
volumes:
- ./schema.sql:/docker-entrypoint-initdb.d/schema.sql
image: postgres:10
ports:
- 9090:5432
environment:
POSTGRES_DB: rps
POSTGRES_PASSWORD: rps
9 changes: 9 additions & 0 deletions rps-ts/db/schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE TYPE move AS ENUM ('Rock', 'Paper', 'Scissor');
CREATE TYPE outcome AS ENUM ('Win', 'Lose', 'Draw');
CREATE TABLE play (
id serial PRIMARY KEY,
user_move move,
computer_move move,
result outcome,
created_at timestamp default now()
);
Empty file added rps-ts/handlers.ts
Empty file.
33 changes: 33 additions & 0 deletions rps-ts/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "rps-ts",
"version": "1.0.0",
"license": "MIT",
"engines": {
"node": "^10"
},
"scripts": {
"clean": "rm -rf ./build && mkdir ./build",
"build": "tsc",
"node-start": "node build",
"start": "nodemon --watch 'src' --exec 'ts-node' ./src/index.ts",
"prettier": "prettier --write \"./src/{**/*,*}.ts\"",
"prettier-check": "prettier --list-different \"./src/{**/*,*}.ts\""
},
"devDependencies": {
"@types/debug": "^4.1.5",
"@types/express": "^4.17.6",
"@types/node": "^14.0.4",
"nodemon": "^2.0.4",
"ts-node": "^8.10.1",
"typescript": "^3.9.3"
},
"dependencies": {
"body-parser": "^1.19.0",
"debug": "^4.1.1",
"express": "^4.17.1",
"fp-ts": "^2.6.1",
"io-ts": "^2.2.3",
"io-ts-types": "^0.5.6",
"pg-promise": "^10.5.6"
}
}
18 changes: 18 additions & 0 deletions rps-ts/src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as express from "express";
import { healthRouter } from "./routers";
import { createGameRouter } from "./routers";
import { connect } from "./repository";
import { createGameRepo } from "./repository";
import { createGameService } from "./services";

const app = express();

const db = connect();
const gameRepo = createGameRepo(db);
const gameService = createGameService(gameRepo);
const gameRouter = createGameRouter(gameService);

app.use("/", healthRouter);
app.use("/", gameRouter);

export default app;
12 changes: 12 additions & 0 deletions rps-ts/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import app from "./app";
import { log } from "./log";

const port =
process.env.SERVICE_PORT !== undefined
? parseInt(process.env.SERVICE_PORT)
: 8080;
const host = process.env.SERVICE_INTERFACE || "0.0.0.0";

app.listen(port, host, () => {
log.info(`listening on ${host}:${port}`);
});
44 changes: 44 additions & 0 deletions rps-ts/src/log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as _debug from "debug";
import * as t from "io-ts";
import { pipe } from "fp-ts/lib/pipeable";
import { fold } from "fp-ts/lib/Either";

const LogLevel = t.keyof(
{
DEBUG: true,
INFO: true,
WARN: true,
ERROR: true,
},
"LogLevel"
);
type LogLevel = t.TypeOf<typeof LogLevel>;

const logLevel = pipe(
LogLevel.decode(process.env.DAVINCI_LOG_LEVEL),
fold(() => "INFO" as LogLevel, t.identity)
);

const namespace = (level: LogLevel) => `IS-${level}`;

const enabledLevelsByLogLevel: { [k in LogLevel]: LogLevel[] } = {
DEBUG: ["ERROR", "WARN", "INFO", "DEBUG"],
INFO: ["ERROR", "WARN", "INFO"],
WARN: ["ERROR", "WARN"],
ERROR: ["ERROR"],
};

const enabledNameSpaces = enabledLevelsByLogLevel[logLevel].map(namespace);

_debug.enable(enabledNameSpaces.join(","));

function _log(level: LogLevel) {
return _debug(namespace(level));
}

export const log = {
debug: _log("DEBUG"),
info: _log("INFO"),
warn: _log("WARN"),
error: _log("ERROR"),
};
8 changes: 8 additions & 0 deletions rps-ts/src/models/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
type NeverPlayed = {
type: "NeverPlayed";
};
type DBError = {
type: "DBError";
message: string;
};
export type Error = NeverPlayed | DBError;
8 changes: 8 additions & 0 deletions rps-ts/src/models/game.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Move } from "./move";
import { Result } from "./result";

export interface PlayResponse {
userMove: Move;
computerMove: Move;
result: Result;
}
4 changes: 4 additions & 0 deletions rps-ts/src/models/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./move";
export * from "./result";
export * from "./game";
export * from "./errors";
12 changes: 12 additions & 0 deletions rps-ts/src/models/move.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as t from "io-ts";

export const Move = t.keyof(
{
Rock: null,
Paper: null,
Scissor: null,
},
"Move"
);

export type Move = t.TypeOf<typeof Move>;
1 change: 1 addition & 0 deletions rps-ts/src/models/result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type Result = "Win" | "Lose" | "Draw";
20 changes: 20 additions & 0 deletions rps-ts/src/repository/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as pgPromise from "pg-promise";
import pg = require("pg-promise/typescript/pg-subset");

export const pgp = pgPromise({
error: (e) => console.error("Database Error", e),
});

export const connect = (): pgPromise.IDatabase<{}, pg.IClient> => {
const cn = {
host: "localhost",
port: 9090,
database: "rps",
user: "postgres",
password: "rps",
max: 2,
};

const db = pgp(cn);
return db;
};
22 changes: 22 additions & 0 deletions rps-ts/src/repository/gameRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { IDatabase } from "pg-promise";
import { IClient } from "pg-promise/typescript/pg-subset";
import { PlayResponse } from "../models";
import { TaskEither, tryCatch } from "fp-ts/lib/TaskEither";
import * as sql from "./sql";

export interface GameRepository {
getLast: TaskEither<unknown, PlayResponse>;
insert: (p: PlayResponse) => TaskEither<unknown, void>;
}

export const createGameRepo = (db: IDatabase<{}, IClient>): GameRepository => ({
getLast: tryCatch(
() => db.one<PlayResponse>(sql.getLastGame),
(error) => error //TODO: decode errors
),
insert: (play: PlayResponse) =>
tryCatch(
() => db.none(sql.insertGame, play).then(() => undefined),
(error) => error //TODO: decode errors
),
});
2 changes: 2 additions & 0 deletions rps-ts/src/repository/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./db";
export * from "./gameRepository";
1 change: 1 addition & 0 deletions rps-ts/src/repository/sql/getLastGame.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
select user_move, computer_move, result from play order by created_at desc limit 1
22 changes: 22 additions & 0 deletions rps-ts/src/repository/sql/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { QueryFile, IQueryFileOptions } from "pg-promise";

const path = require("path");

function sql(file: string): QueryFile {
const fullPath: string = path.join(__dirname, file); // generating full path;

const options: IQueryFileOptions = {
minify: true,
};

const query: QueryFile = new QueryFile(fullPath, options);

if (query.error) {
console.error(query.error);
}

return query;
}

export const getLastGame = sql("getLastGame.sql");
export const insertGame = sql("insertGame.sql");
1 change: 1 addition & 0 deletions rps-ts/src/repository/sql/insertGame.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
insert into play(user_move, computer_move, result) values(${userMove}, ${computerMove}, ${result})
Loading