This commit is contained in:
2022-05-04 11:13:09 +08:00
parent 3d331ee10d
commit 0b76b5935d
68 changed files with 38788 additions and 1 deletions

2
backend/.env Normal file
View File

@@ -0,0 +1,2 @@
PORT = 4000
TZ = Asia/Taipei

3
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules
dist
.DS_STORE

11
backend/.mocharc.js Normal file
View File

@@ -0,0 +1,11 @@
module.exports = {
require: [
'ts-node/register',
],
timeout: 999999,
exit: true,
spec: [
'./test/**/*.test.ts'
],
'preserve-symlinks': true
}

30
backend/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,30 @@
{
"configurations": [
{
"type": "node",
"request": "launch",
"name": "mocha current file",
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
"args": [
"${file}"
],
"internalConsoleOptions": "openOnSessionStart",
"cwd": "${workspaceFolder}"
},
{
"type": "node",
"request": "launch",
"name": "ts-node current file",
"protocol": "inspector",
"args": [
"${relativeFile}"
],
"cwd": "${workspaceRoot}",
"runtimeArgs": [
"-r",
"ts-node/register"
],
"internalConsoleOptions": "openOnSessionStart"
}
]
}

3
backend/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib"
}

30
backend/Dockerfile Normal file
View File

@@ -0,0 +1,30 @@
FROM node
# 使用淘宝 NPM 镜像(国内机器构建推荐启用)
# RUN npm config set registry https://registry.npm.taobao.org/
# npm install
ADD package*.json /src/
WORKDIR /src
RUN npm i
# build
ADD . /src
RUN npm run build
# clean
RUN npm prune --production
# move
RUN rm -rf /app \
&& mv dist /app \
&& mv node_modules /app/ \
&& rm -rf /src
# ENV
ENV NODE_ENV production
EXPOSE 3000
WORKDIR /app
CMD node index.js

31
backend/README.md Normal file
View File

@@ -0,0 +1,31 @@
# TSRPC Server
## Usage
### Local dev server
Dev server would restart automatically when code changed.
```
npm run dev
```
### Build
```
npm run build
```
### Generate API document
Generate API document in swagger/openapi and markdown format.
```shell
npm run doc
```
### Run unit Test
Execute `npm run dev` first, then execute:
```
npm run test
```
---

3699
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
backend/package.json Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "TSRPC_Badminton-Scoreboard-backend",
"version": "0.1.0",
"main": "index.js",
"private": true,
"scripts": {
"dev": "tsrpc-cli dev",
"build": "tsrpc-cli build",
"doc": "tsrpc-cli doc",
"test": "mocha test/**/*.test.ts",
"proto": "tsrpc-cli proto",
"sync": "tsrpc-cli sync",
"api": "tsrpc-cli api"
},
"devDependencies": {
"@types/mocha": "^8.2.3",
"@types/node": "^15.14.9",
"mocha": "^9.2.2",
"onchange": "^7.1.0",
"ts-node": "^10.7.0",
"tsrpc-cli": "^2.4.3",
"typescript": "^4.6.4"
},
"dependencies": {
"dayjs": "^1.11.1",
"dotenv": "^16.0.0",
"tsrpc": "^3.3.0"
}
}

View File

@@ -0,0 +1,26 @@
import { ApiCall } from "tsrpc";
import { server } from "..";
import { ReqSend, ResSend } from "../shared/protocols/PtlSend";
// This is a demo code file
// Feel free to delete it
export async function ApiSend(call: ApiCall<ReqSend, ResSend>): Promise<void> {
// Error
if (call.req.content.length === 0) {
call.error("Content is empty");
return;
}
// Success
let time: Date = new Date();
call.succ({
time: time
});
// Broadcast
server.broadcastMsg("Chat", {
content: "Chat: " + call.req.content,
time: time
});
}

View File

@@ -0,0 +1,31 @@
import dayjs from "dayjs";
import { ApiCall } from "tsrpc";
import { server } from "../..";
import { Reqstart, Resstart } from "../../shared/protocols/lobby/Ptlstart";
export async function Apistart(call: ApiCall<Reqstart, Resstart>): Promise<void> {
// Error
// if (call.req.content.length === 0) {
// call.error("Content is empty");
// return;
// }
// Success
let time: Date = new Date();
let dayjstime: string = dayjs().format("YYYY-MM-DD HH:mm:ss");
call.logger.log(`${dayjstime} Apistart`);
call.succ({
code: 1,
data: {
time: dayjstime
}
});
// Broadcast
server.broadcastMsg("lobby/B_start", {
time: time,
data: {
content: `${dayjstime}: B_start`
}
});
}

32
backend/src/index.ts Normal file
View File

@@ -0,0 +1,32 @@
import dayjs from "dayjs";
import * as path from "path";
import { WsServer } from "tsrpc";
import { serviceProto, ServiceType } from "./shared/protocols/serviceProto";
require("dotenv").config();
require("dayjs/locale/zh-tw");
dayjs.locale("zh-tw");
// Create the Server
export const server: WsServer<ServiceType> = new WsServer(serviceProto, {
port: +process.env.PORT! || 3000,
// Remove this to use binary mode (remove from the client too)
json: true
});
// Initialize before server start
async function init(): Promise<void> {
await server.autoImplementApi(path.resolve(__dirname, "api"));
// TODO
// let time: Date = new Date();
let time: string = dayjs().format("YYYY-MM-DD HH:mm:ss");
console.log(`${time} Start Server`);
// Prepare something... (e.g. connect the db)
}
// Entry function
async function main(): Promise<void> {
await init();
await server.start();
}
main();

View File

@@ -0,0 +1,7 @@
// This is a demo code file
// Feel free to delete it
export interface MsgChat {
content: string;
time: Date;
}

View File

@@ -0,0 +1,11 @@
// This is a demo code file
// Feel free to delete it
export interface ReqSend {
content: string;
}
export interface ResSend {
time: Date;
}

View File

@@ -0,0 +1,15 @@
export interface BaseRequest {
}
export interface BaseResponse {
}
export interface BaseConf {
}
export interface BaseMessage {
}

View File

@@ -0,0 +1,9 @@
// This is a demo code file
// Feel free to delete it
export interface MsgB_start {
data: {
content: string
};
time: Date;
}

View File

@@ -0,0 +1,11 @@
// This is a demo code file
// Feel free to delete it
export interface Reqstart { }
export interface Resstart {
code: number; // 与 TSRPC 的错误处理重复
data: {
time: string;
};
}

View File

@@ -0,0 +1,152 @@
import { ServiceProto } from 'tsrpc-proto';
import { MsgB_start } from './lobby/MsgB_start';
import { Reqstart, Resstart } from './lobby/Ptlstart';
import { MsgChat } from './MsgChat';
import { ReqSend, ResSend } from './PtlSend';
export interface ServiceType {
api: {
"lobby/start": {
req: Reqstart,
res: Resstart
},
"Send": {
req: ReqSend,
res: ResSend
}
},
msg: {
"lobby/B_start": MsgB_start,
"Chat": MsgChat
}
}
export const serviceProto: ServiceProto<ServiceType> = {
"version": 1,
"services": [
{
"id": 5,
"name": "lobby/B_start",
"type": "msg"
},
{
"id": 1,
"name": "lobby/start",
"type": "api"
},
{
"id": 2,
"name": "Chat",
"type": "msg"
},
{
"id": 4,
"name": "Send",
"type": "api"
}
],
"types": {
"lobby/MsgB_start/MsgB_start": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "data",
"type": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "content",
"type": {
"type": "String"
}
}
]
}
},
{
"id": 1,
"name": "time",
"type": {
"type": "Date"
}
}
]
},
"lobby/Ptlstart/Reqstart": {
"type": "Interface"
},
"lobby/Ptlstart/Resstart": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "code",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "data",
"type": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "time",
"type": {
"type": "String"
}
}
]
}
}
]
},
"MsgChat/MsgChat": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "content",
"type": {
"type": "String"
}
},
{
"id": 1,
"name": "time",
"type": {
"type": "Date"
}
}
]
},
"PtlSend/ReqSend": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "content",
"type": {
"type": "String"
}
}
]
},
"PtlSend/ResSend": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "time",
"type": {
"type": "Date"
}
}
]
}
}
};

View File

@@ -0,0 +1,40 @@
import assert from 'assert';
import { TsrpcError, WsClient } from 'tsrpc';
import { serviceProto } from '../../src/shared/protocols/serviceProto';
// 1. EXECUTE `npm run dev` TO START A LOCAL DEV SERVER
// 2. EXECUTE `npm test` TO START UNIT TEST
describe('ApiSend', function () {
let client = new WsClient(serviceProto, {
server: 'ws://127.0.0.1:3000',
json: true,
logger: console
});
before(async function () {
let res = await client.connect();
assert.strictEqual(res.isSucc, true, 'Failed to connect to server, have you executed `npm run dev` already?');
})
it('Success', async function () {
let ret = await client.callApi('Send', {
content: 'Test'
});
assert.ok(ret.isSucc)
});
it('Check content is empty', async function () {
let ret = await client.callApi('Send', {
content: ''
});
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Content is empty')
});
})
after(async function () {
await client.disconnect();
})
})

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"lib": [
"es2018"
],
"module": "commonjs",
"target": "es2018",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node"
},
"include": [
".",
"../src"
]
}

18
backend/tsconfig.json Normal file
View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"lib": [
"es2018"
],
"module": "commonjs",
"target": "es2018",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node"
},
"include": [
"src"
]
}

116
backend/tslint.json Normal file
View File

@@ -0,0 +1,116 @@
{
"defaultSeverity": "warning",
"rules": {
"ban": [
true,
[
"_",
"extend"
],
[
"_",
"isNull"
],
[
"_",
"isDefined"
]
],
"class-name": false,
"comment-format": [
true,
"check-space"
],
"curly": true,
"eofline": false,
"forin": false,
"indent": [
true,
4
],
"interface-name": [
true,
"never-prefix"
],
"jsdoc-format": true,
"label-position": true,
"label-undefined": true,
"max-line-length": [
false,
140
],
"no-arg": true,
"no-bitwise": false,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-construct": true,
"no-debugger": true,
"no-duplicate-key": true,
"no-duplicate-variable": true,
"no-empty": true,
// "no-eval": true,
"no-string-literal": false,
"no-trailing-comma": true,
"no-trailing-whitespace": true,
"no-unused-expression": false,
"no-unused-variable": true,
"no-unreachable": true,
"no-use-before-declare": false,
"one-line": [
true,
"check-open-brace",
"check-catch",
"check-else",
"check-whitespace"
],
"quotemark": [
true,
"double"
],
"radix": true,
"semicolon": [
true,
"always"
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef": [
true,
"call-signature",
"parameter",
"property-declaration",
"variable-declaration"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace"
},
{
"index-signature": "space"
}
],
"use-strict": [
true,
"check-module",
"check-function"
],
"variable-name": false,
"whitespace": [
false,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
]
}
}

39
backend/tsrpc.config.ts Normal file
View File

@@ -0,0 +1,39 @@
import { CodeTemplate, TsrpcConfig } from "tsrpc-cli";
const tsrpcConf: TsrpcConfig = {
// Generate ServiceProto
proto: [
{
ptlDir: "src/shared/protocols", // Protocol dir
output: "src/shared/protocols/serviceProto.ts", // Path for generated ServiceProto
apiDir: "src/api", // API dir
docDir: "docs", // API documents dir
ptlTemplate: CodeTemplate.getExtendedPtl(),
// msgTemplate: CodeTemplate.getExtendedMsg(),
}
],
// Sync shared code
sync: [
{
from: "src/shared",
to: "../frontend/src/shared",
type: "symlink" // Change this to 'copy' if your environment not support symlink
}
],
// Dev server
dev: {
autoProto: true, // Auto regenerate proto
autoSync: true, // Auto sync when file changed
autoApi: true, // Auto create API when ServiceProto updated
watch: "src", // Restart dev server when these files changed
entry: "src/index.ts", // Dev server command: node -r ts-node/register {entry}
},
// Build config
build: {
autoProto: true, // Auto generate proto before build
autoSync: true, // Auto sync before build
autoApi: true, // Auto generate API before build
outDir: "dist", // Clean this dir before build
}
};
export default tsrpcConf;