Compare commits

..

5 Commits

Author SHA1 Message Date
k8w
f70fff7851 hall apis 2022-04-20 19:43:00 +08:00
k8w
e197b2842a new protocols 2022-04-19 16:05:40 +08:00
k8w
238ebb9857 protocols 优化 取消双向调用 2022-04-19 14:38:10 +08:00
k8w
3fd0679492 ptl 2022-04-19 01:23:37 +08:00
k8w
d2f770d3ad init 2021-12-29 22:15:07 +08:00
76 changed files with 1545 additions and 124 deletions

View File

@ -15,4 +15,5 @@ Start local frontend server:
``` ```
cd <example-dir>/frontend cd <example-dir>/frontend
npm install npm install
npm run dev
``` ```

View File

@ -15,10 +15,10 @@
"@types/node": "^15.14.9", "@types/node": "^15.14.9",
"onchange": "^7.1.0", "onchange": "^7.1.0",
"ts-node": "^9.1.1", "ts-node": "^9.1.1",
"tsrpc-cli": "^2.4.0", "tsrpc-cli": "^2.0.8",
"typescript": "^4.5.5" "typescript": "^4.4.3"
}, },
"dependencies": { "dependencies": {
"tsrpc": "^3.2.0" "tsrpc": "^3.0.9"
} }
} }

View File

@ -7,15 +7,15 @@
"build": "webpack --mode=production" "build": "webpack --mode=production"
}, },
"devDependencies": { "devDependencies": {
"copy-webpack-plugin": "^9.1.0", "copy-webpack-plugin": "^9.0.1",
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^5.3.2",
"ts-loader": "^9.2.6", "ts-loader": "^9.2.6",
"typescript": "^4.5.5", "typescript": "^4.4.3",
"webpack": "^5.69.1", "webpack": "^5.57.1",
"webpack-cli": "^4.9.2", "webpack-cli": "^4.9.0",
"webpack-dev-server": "^3.11.3" "webpack-dev-server": "^3.11.2"
}, },
"dependencies": { "dependencies": {
"tsrpc-browser": "^3.2.0" "tsrpc-browser": "^3.0.7"
} }
} }

View File

@ -14,11 +14,11 @@
"devDependencies": { "devDependencies": {
"@types/node": "^15.14.9", "@types/node": "^15.14.9",
"onchange": "^7.1.0", "onchange": "^7.1.0",
"ts-node": "^10.5.0", "ts-node": "^10.2.1",
"tsrpc-cli": "^2.4.0", "tsrpc-cli": "^2.0.8",
"typescript": "^4.5.5" "typescript": "^4.4.3"
}, },
"dependencies": { "dependencies": {
"tsrpc": "^3.2.0" "tsrpc": "^3.0.9"
} }
} }

View File

@ -7,16 +7,16 @@
"build": "webpack --mode=production" "build": "webpack --mode=production"
}, },
"devDependencies": { "devDependencies": {
"copy-webpack-plugin": "^9.1.0", "copy-webpack-plugin": "^9.0.1",
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^5.3.2",
"ts-loader": "^9.2.6", "ts-loader": "^9.2.6",
"typescript": "^4.5.5", "typescript": "^4.4.3",
"webpack": "^5.69.1", "webpack": "^5.57.1",
"webpack-cli": "^4.9.2", "webpack-cli": "^4.9.0",
"webpack-dev-server": "^3.11.3" "webpack-dev-server": "^3.11.2"
}, },
"dependencies": { "dependencies": {
"tsrpc-browser": "^3.2.0" "tsrpc-browser": "^3.0.7"
}, },
"browserslist": [ "browserslist": [
"defaults" "defaults"

View File

@ -15,10 +15,10 @@
"@types/node": "^15.14.9", "@types/node": "^15.14.9",
"onchange": "^7.1.0", "onchange": "^7.1.0",
"ts-node": "^9.1.1", "ts-node": "^9.1.1",
"tsrpc-cli": "^2.4.0", "tsrpc-cli": "^2.2.0",
"typescript": "^4.5.5" "typescript": "^4.5.2"
}, },
"dependencies": { "dependencies": {
"tsrpc": "^3.2.0" "tsrpc": "^3.1.2"
} }
} }

View File

@ -9,7 +9,7 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"tsrpc-browser": "^3.2.0", "tsrpc-browser": "^3.1.2",
"tsrpc-miniapp": "^3.2.0" "tsrpc-miniapp": "^3.1.2"
} }
} }

View File

@ -15,10 +15,10 @@
"@types/node": "^15.14.9", "@types/node": "^15.14.9",
"onchange": "^7.1.0", "onchange": "^7.1.0",
"ts-node": "^9.1.1", "ts-node": "^9.1.1",
"tsrpc-cli": "^2.4.0", "tsrpc-cli": "^2.2.0",
"typescript": "^4.5.5" "typescript": "^4.5.2"
}, },
"dependencies": { "dependencies": {
"tsrpc": "^3.2.0" "tsrpc": "^3.1.2"
} }
} }

View File

@ -4,7 +4,7 @@
"uuid": "cd1e2fff-2ab5-4407-b917-e60b25207723", "uuid": "cd1e2fff-2ab5-4407-b917-e60b25207723",
"version": "3.3.2", "version": "3.3.2",
"dependencies": { "dependencies": {
"tsrpc-browser": "^3.2.0", "tsrpc-browser": "^3.1.2",
"tsrpc-miniapp": "^3.2.0" "tsrpc-miniapp": "^3.1.2"
} }
} }

View File

@ -0,0 +1,6 @@
TSRPC 分布式游戏房间服务 Demo
===
## 特性
1. 可开房间、随机匹配,房间逻辑可以自定义
2. 支持分布式部署和水平扩展

View File

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

View File

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

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"
}
]
}

View File

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

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

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
```
---

View File

@ -0,0 +1,30 @@
{
"name": "backend",
"version": "0.1.0",
"main": "index.js",
"private": true,
"scripts": {
"dev:hall": "tsrpc-cli dev --entry src/hallServer.ts",
"dev:room": "tsrpc-cli dev --entry src/roomServer.ts",
"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",
"@types/uuid": "^8.3.4",
"mocha": "^9.2.2",
"onchange": "^7.1.0",
"ts-node": "^10.7.0",
"tsrpc-cli": "^2.4.3-dev.1",
"typescript": "^4.6.3"
},
"dependencies": {
"tsrpc": "^3.3.0",
"uuid": "^8.3.2"
}
}

View File

@ -0,0 +1,59 @@
import path from "path";
import { HttpServer, WsClient } from "tsrpc";
import { UserUtil } from "../models/UserUtil";
import { MsgUpdateRoomState } from "../shared/protocols/roomServer/admin/MsgUpdateRoomState";
import { serviceProto } from "../shared/protocols/serviceProto_hallServer";
import { ServiceType as ServiceType_Room } from "../shared/protocols/serviceProto_roomServer";
import { UserInfo } from "../shared/types/UserInfo";
export class HallServer {
readonly server = new HttpServer(serviceProto, {
port: 3000,
// Remove this to use binary mode (remove from the client too)
json: true
});
/** 已注册的 RoomServer */
readonly roomServers: {
url: string,
conn: WsClient<ServiceType_Room>,
state?: MsgUpdateRoomState
}[] = [];
constructor() {
// Flows
// 前置鉴别登录态
this.server.flows.preApiCallFlow.push(async call => {
call.currentUser = call.req.sso ? await UserUtil.parseSso(call.req.sso) : undefined;
// 需要登录的接口:前置登录态判定
if (!call.service.conf?.allowGuest) {
if (!call.currentUser) {
call.error('你还未登录', { code: 'NEED_LOGIN' });
return undefined;
}
}
if (call.currentUser) {
call.logger.prefixs.push(`[uid=${call.currentUser.id}]`);
}
return call;
});
}
async init() {
await this.server.autoImplementApi(path.resolve(__dirname, './api'));
}
async start() {
await this.server.start();
}
}
declare module 'tsrpc' {
export interface ApiCall {
/** 只要协议配置的 `allowGuest` 不为 `true`,则必定有值 */
currentUser?: UserInfo;
}
}

View File

@ -0,0 +1,34 @@
import { ApiCall, TsrpcError } from "tsrpc";
import { hallServer } from "../../hallServer";
import { BackConfig } from "../../models/BackConfig";
import { ReqCreateRoom, ResCreateRoom } from "../../shared/protocols/hallServer/PtlCreateRoom";
export async function ApiCreateRoom(call: ApiCall<ReqCreateRoom, ResCreateRoom>) {
// 挑选一个人数最少的 RoomServer
let server = hallServer.roomServers.filter(v => v.state).orderBy(v => v.state!.userNum)[0];
if (!server) {
return call.error('没有可用的 RoomServer', { type: TsrpcError.Type.ServerError });
}
if (!call.req.roomName) {
return call.error('请输入房间名称');
}
// RPC
let op = await server.conn.callApi('admin/CreateRoom', {
adminToken: BackConfig.adminToken,
creator: {
uid: call.currentUser!.id,
nickname: call.currentUser!.nickname
},
roomName: call.req.roomName
})
if (!op.isSucc) {
return call.error(op.err);
}
call.succ({
serverUrl: server.url,
roomId: op.res.roomId
})
}

View File

@ -0,0 +1,22 @@
import { ApiCall } from "tsrpc";
import { hallServer } from "../../hallServer";
import { ReqListRooms, ResListRooms } from "../../shared/protocols/hallServer/PtlListRooms";
export async function ApiListRooms(call: ApiCall<ReqListRooms, ResListRooms>) {
let rooms = hallServer.roomServers.reduce((prev, next) => {
if (next.state) {
prev = prev.concat(next.state.rooms.map(v => ({
name: v.name,
userNum: v.userNum,
serverUrl: next.url,
roomId: v.id,
updateTime: v.updateTime
})))
}
return prev;
}, [] as (ResListRooms['rooms'][0] & { updateTime: number })[])
call.succ({
rooms: rooms.orderByDesc(v => v.updateTime).slice(0, 100)
})
}

View File

@ -0,0 +1,16 @@
import { ApiCall } from "tsrpc";
import uuid from "uuid";
import { UserUtil } from "../../models/UserUtil";
import { ReqLogin, ResLogin } from "../../shared/protocols/hallServer/PtlLogin";
import { UserInfo } from "../../shared/types/UserInfo";
export async function ApiLogin(call: ApiCall<ReqLogin, ResLogin>) {
let uid = uuid.v4();
let user: UserInfo = {
id: uid,
nickname: call.req.nickname
};
let sso = await UserUtil.createSso(user)
call.succ({ sso, user })
}

View File

@ -0,0 +1,7 @@
import { ApiCall } from "tsrpc";
import { ReqStartMatch, ResStartMatch } from "../../shared/protocols/hallServer/PtlStartMatch";
export async function ApiStartMatch(call: ApiCall<ReqStartMatch, ResStartMatch>) {
// TODO
call.error('API Not Implemented');
}

View File

@ -0,0 +1,56 @@
import { ApiCall, TerminalColorLogger, WsClient } from "tsrpc";
import { hallServer } from "../../../hallServer";
import { BackConfig } from "../../../models/BackConfig";
import { ReqRegisterRoomServer, ResRegisterRoomServer } from "../../../shared/protocols/hallServer/admin/PtlRegisterRoomServer";
import { serviceProto } from "../../../shared/protocols/serviceProto_roomServer";
import { HallServer } from "../../HallServer";
let nextRoomIndex = 1;
export async function ApiRegisterRoomServer(call: ApiCall<ReqRegisterRoomServer, ResRegisterRoomServer>) {
// 鉴权
if (call.req.adminToken !== BackConfig.adminToken) {
return call.error('非法操作');
}
// Create
let client = new WsClient(serviceProto, {
server: call.req.serverUrl,
logger: new TerminalColorLogger({
pid: `RoomServer${nextRoomIndex++}`
}),
heartbeat: {
interval: 5000,
timeout: 5000
}
});
// Flows
client.flows.postDisconnectFlow.push(v => {
hallServer.roomServers.remove(v1 => v1.conn === client);
return v;
});
client.listenMsg('admin/UpdateRoomState', msg => {
roomServer.state = msg;
});
// Connect
let op = await client.connect();
if (!op.isSucc) {
return call.error(op.errMsg);
}
// Auth
let op2 = await client.callApi('admin/Auth', { adminToken: call.req.adminToken });
if (!op2.isSucc) {
return call.error(op2.err);
}
// Succ
let roomServer: HallServer['roomServers'][number] = {
url: call.req.serverUrl,
conn: client
}
hallServer.roomServers.push(roomServer);
call.succ({});
}

View File

@ -0,0 +1,44 @@
import path from "path";
import { WsServer } from "tsrpc";
import { Room } from "../models/Room";
import { serviceProto } from "../shared/protocols/serviceProto_roomServer";
import { UserInfo } from "../shared/types/UserInfo";
export class RoomServer {
readonly server = new WsServer(serviceProto, {
port: parseInt(process.env['PORT'] || '3001'),
// Remove this to use binary mode (remove from the client too)
json: true
});
constructor() {
// Flows
// 前置鉴别登录态
this.server.flows.preApiCallFlow.push(async call => {
// 需要登录的接口:前置登录态判定
if (!call.service.conf?.allowGuest) {
if (!call.conn.currentUser) {
call.error('你还未登录', { code: 'NEED_LOGIN' });
return undefined;
}
}
return call;
});
}
async init() {
await this.server.autoImplementApi(path.resolve(__dirname, './api'));
}
async start() {
await this.server.start();
}
}
declare module 'tsrpc' {
export interface BaseConnection {
currentUser?: UserInfo;
currentRoom?: Room;
}
}

View File

@ -0,0 +1,7 @@
import { ApiCall } from "tsrpc";
import { ReqExitRoom, ResExitRoom } from "../../shared/protocols/roomServer/PtlExitRoom";
export async function ApiExitRoom(call: ApiCall<ReqExitRoom, ResExitRoom>) {
// TODO
call.error('API Not Implemented');
}

View File

@ -0,0 +1,7 @@
import { ApiCall } from "tsrpc";
import { ReqJoinRoom, ResJoinRoom } from "../../shared/protocols/roomServer/PtlJoinRoom";
export async function ApiJoinRoom(call: ApiCall<ReqJoinRoom, ResJoinRoom>) {
// TODO
call.error('API Not Implemented');
}

View File

@ -0,0 +1,7 @@
import { ApiCall } from "tsrpc";
import { ReqUpdateRoom, ResUpdateRoom } from "../../shared/protocols/roomServer/PtlUpdateRoom";
export async function ApiUpdateRoom(call: ApiCall<ReqUpdateRoom, ResUpdateRoom>) {
// TODO
call.error('API Not Implemented');
}

View File

@ -0,0 +1,7 @@
import { ApiCall } from "tsrpc";
import { ReqAuth, ResAuth } from "../../../shared/protocols/roomServer/admin/PtlAuth";
export async function ApiAuth(call: ApiCall<ReqAuth, ResAuth>) {
// TODO
call.error('API Not Implemented');
}

View File

@ -0,0 +1,7 @@
import { ApiCall } from "tsrpc";
import { ReqCreateRoom, ResCreateRoom } from "../../../shared/protocols/roomServer/admin/PtlCreateRoom";
export async function ApiCreateRoom(call: ApiCall<ReqCreateRoom, ResCreateRoom>) {
// TODO
call.error('API Not Implemented');
}

View File

@ -0,0 +1,11 @@
import { HallServer } from "./HallServer/HallServer";
export const hallServer = new HallServer();
// Entry function
async function main() {
await hallServer.init();
await hallServer.start();
}
main();

View File

@ -0,0 +1,5 @@
export const BackConfig = {
adminToken: 'AAABBBCCC'
}

View File

@ -0,0 +1,3 @@
export class Room {
}

View File

@ -0,0 +1,21 @@
import { UserInfo } from "../shared/types/UserInfo";
// 登录态 SSO 的编解码
// 这里简单起见,使用未加密的 JSON 字符串
// 你可以根据自己的需要,改为加密字符串,或者服务端 Session Key 等
export class UserUtil {
static async createSso(user: UserInfo): Promise<string> {
return JSON.stringify(user);
}
static async parseSso(sso: string): Promise<UserInfo | undefined> {
try {
return JSON.parse(sso);
}
catch {
return undefined;
}
}
}

View File

@ -0,0 +1,10 @@
import { RoomServer } from "./RoomServer/RoomServer";
export const roomServer = new RoomServer();
// Entry function
async function main() {
await roomServer.init();
await roomServer.start();
}
main();

View File

@ -0,0 +1,20 @@
export interface BaseRequest {
/** 登录态 */
sso?: string
}
export interface BaseResponse {
}
export interface BaseConf {
/**
*
* @defaultValue false
*/
allowGuest?: boolean
}
export interface BaseMessage {
}

View File

@ -0,0 +1,14 @@
import { BaseConf, BaseRequest, BaseResponse } from "./../base";
export interface ReqCreateRoom extends BaseRequest {
roomName: string
}
export interface ResCreateRoom extends BaseResponse {
serverUrl: string,
roomId: string
}
export const conf: BaseConf = {
}

View File

@ -0,0 +1,19 @@
import { uint } from "tsrpc";
import { BaseConf, BaseRequest, BaseResponse } from "./../base";
export interface ReqListRooms extends BaseRequest {
}
export interface ResListRooms extends BaseResponse {
rooms: {
name: string,
userNum: uint,
serverUrl: string,
roomId: string
}[]
}
export const conf: BaseConf = {
}

View File

@ -0,0 +1,15 @@
import { UserInfo } from "../../types/UserInfo";
import { BaseConf } from "../base";
export interface ReqLogin {
nickname: string
}
export interface ResLogin {
sso: string,
user: UserInfo
}
export const conf: BaseConf = {
allowGuest: true
}

View File

@ -0,0 +1,15 @@
import { uint } from "tsrpc-proto";
import { BaseConf, BaseRequest, BaseResponse } from "./../base";
export interface ReqStartMatch extends BaseRequest {
}
export interface ResStartMatch extends BaseResponse {
serverUrl: string,
roomId: uint
}
export const conf: BaseConf = {
}

View File

@ -0,0 +1,12 @@
import { BaseConf } from "../../base";
export interface ReqRegisterRoomServer {
/** RoomServer 的连接地址 */
serverUrl: string,
/** Token 用于鉴权 */
adminToken: string
}
export interface ResRegisterRoomServer {
}

View File

@ -0,0 +1,13 @@
import { BaseConf, BaseRequest, BaseResponse } from "./../base";
export interface ReqExitRoom extends BaseRequest {
}
export interface ResExitRoom extends BaseResponse {
}
export const conf: BaseConf = {
}

View File

@ -0,0 +1,13 @@
import { BaseRequest, BaseResponse, BaseConf } from "./../base";
export interface ReqJoinRoom extends BaseRequest {
roomId: string
}
export interface ResJoinRoom extends BaseResponse {
}
export const conf: BaseConf = {
}

View File

@ -0,0 +1,14 @@
import { BaseConf, BaseRequest, BaseResponse } from "../base";
export interface ReqUpdateRoom extends BaseRequest {
roomId: string,
roomName: string
}
export interface ResUpdateRoom extends BaseResponse {
}
export const conf: BaseConf = {
}

View File

@ -0,0 +1,16 @@
import { uint } from "tsrpc-proto";
export interface MsgUpdateRoomState {
userNum: uint,
rooms: {
id: string,
name: string,
userNum: uint,
/** 为 undefined 代表不在匹配中 */
startMatchTime?: uint,
// 房间信息的最后更新时间
updateTime: uint
}[]
}
// export const conf = {}

View File

@ -0,0 +1,8 @@
export interface ReqAuth {
adminToken: string
}
export interface ResAuth {
}

View File

@ -0,0 +1,13 @@
export interface ReqCreateRoom {
adminToken: string,
creator: {
uid: string,
nickname: string
}
roomName: string
}
export interface ResCreateRoom {
roomId: string
}

View File

@ -0,0 +1,5 @@
export interface MsgChat {
}
// export const conf = {}

View File

@ -0,0 +1,5 @@
export interface MsgUpdateRoomInfo {
}
// export const conf = {}

View File

@ -0,0 +1,319 @@
import { ServiceProto } from 'tsrpc-proto';
import { ReqRegisterRoomServer, ResRegisterRoomServer } from './hallServer/admin/PtlRegisterRoomServer';
import { ReqCreateRoom, ResCreateRoom } from './hallServer/PtlCreateRoom';
import { ReqListRooms, ResListRooms } from './hallServer/PtlListRooms';
import { ReqLogin, ResLogin } from './hallServer/PtlLogin';
import { ReqStartMatch, ResStartMatch } from './hallServer/PtlStartMatch';
export interface ServiceType {
api: {
"admin/RegisterRoomServer": {
req: ReqRegisterRoomServer,
res: ResRegisterRoomServer
},
"CreateRoom": {
req: ReqCreateRoom,
res: ResCreateRoom
},
"ListRooms": {
req: ReqListRooms,
res: ResListRooms
},
"Login": {
req: ReqLogin,
res: ResLogin
},
"StartMatch": {
req: ReqStartMatch,
res: ResStartMatch
}
},
msg: {
}
}
export const serviceProto: ServiceProto<ServiceType> = {
"version": 2,
"services": [
{
"id": 0,
"name": "admin/RegisterRoomServer",
"type": "api"
},
{
"id": 1,
"name": "CreateRoom",
"type": "api",
"conf": {}
},
{
"id": 2,
"name": "ListRooms",
"type": "api",
"conf": {}
},
{
"id": 3,
"name": "Login",
"type": "api",
"conf": {
"allowGuest": true
}
},
{
"id": 4,
"name": "StartMatch",
"type": "api",
"conf": {}
}
],
"types": {
"admin/PtlRegisterRoomServer/ReqRegisterRoomServer": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "serverUrl",
"type": {
"type": "String"
}
},
{
"id": 1,
"name": "adminToken",
"type": {
"type": "String"
}
}
]
},
"admin/PtlRegisterRoomServer/ResRegisterRoomServer": {
"type": "Interface"
},
"PtlCreateRoom/ReqCreateRoom": {
"type": "Interface",
"extends": [
{
"id": 0,
"type": {
"type": "Reference",
"target": "../base/BaseRequest"
}
}
],
"properties": [
{
"id": 0,
"name": "roomName",
"type": {
"type": "String"
}
}
]
},
"../base/BaseRequest": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "sso",
"type": {
"type": "String"
},
"optional": true
}
]
},
"PtlCreateRoom/ResCreateRoom": {
"type": "Interface",
"extends": [
{
"id": 0,
"type": {
"type": "Reference",
"target": "../base/BaseResponse"
}
}
],
"properties": [
{
"id": 0,
"name": "serverUrl",
"type": {
"type": "String"
}
},
{
"id": 1,
"name": "roomId",
"type": {
"type": "String"
}
}
]
},
"../base/BaseResponse": {
"type": "Interface"
},
"PtlListRooms/ReqListRooms": {
"type": "Interface",
"extends": [
{
"id": 0,
"type": {
"type": "Reference",
"target": "../base/BaseRequest"
}
}
]
},
"PtlListRooms/ResListRooms": {
"type": "Interface",
"extends": [
{
"id": 0,
"type": {
"type": "Reference",
"target": "../base/BaseResponse"
}
}
],
"properties": [
{
"id": 0,
"name": "rooms",
"type": {
"type": "Array",
"elementType": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "name",
"type": {
"type": "String"
}
},
{
"id": 4,
"name": "userNum",
"type": {
"type": "Number",
"scalarType": "uint"
}
},
{
"id": 2,
"name": "serverUrl",
"type": {
"type": "String"
}
},
{
"id": 3,
"name": "roomId",
"type": {
"type": "String"
}
}
]
}
}
}
]
},
"PtlLogin/ReqLogin": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "nickname",
"type": {
"type": "String"
}
}
]
},
"PtlLogin/ResLogin": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "sso",
"type": {
"type": "String"
}
},
{
"id": 1,
"name": "user",
"type": {
"type": "Reference",
"target": "../../types/UserInfo/UserInfo"
}
}
]
},
"../../types/UserInfo/UserInfo": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "id",
"type": {
"type": "String"
}
},
{
"id": 1,
"name": "nickname",
"type": {
"type": "String"
}
}
]
},
"PtlStartMatch/ReqStartMatch": {
"type": "Interface",
"extends": [
{
"id": 0,
"type": {
"type": "Reference",
"target": "../base/BaseRequest"
}
}
]
},
"PtlStartMatch/ResStartMatch": {
"type": "Interface",
"extends": [
{
"id": 0,
"type": {
"type": "Reference",
"target": "../base/BaseResponse"
}
}
],
"properties": [
{
"id": 0,
"name": "serverUrl",
"type": {
"type": "String"
}
},
{
"id": 1,
"name": "roomId",
"type": {
"type": "Number",
"scalarType": "uint"
}
}
]
}
}
};

View File

@ -0,0 +1,342 @@
import { ServiceProto } from 'tsrpc-proto';
import { MsgUpdateRoomState } from './roomServer/admin/MsgUpdateRoomState';
import { ReqAuth, ResAuth } from './roomServer/admin/PtlAuth';
import { ReqCreateRoom, ResCreateRoom } from './roomServer/admin/PtlCreateRoom';
import { ReqExitRoom, ResExitRoom } from './roomServer/PtlExitRoom';
import { ReqJoinRoom, ResJoinRoom } from './roomServer/PtlJoinRoom';
import { ReqUpdateRoom, ResUpdateRoom } from './roomServer/PtlUpdateRoom';
import { MsgChat } from './roomServer/roomMsg/MsgChat';
import { MsgUpdateRoomInfo } from './roomServer/roomMsg/MsgUpdateRoomInfo';
export interface ServiceType {
api: {
"admin/Auth": {
req: ReqAuth,
res: ResAuth
},
"admin/CreateRoom": {
req: ReqCreateRoom,
res: ResCreateRoom
},
"ExitRoom": {
req: ReqExitRoom,
res: ResExitRoom
},
"JoinRoom": {
req: ReqJoinRoom,
res: ResJoinRoom
},
"UpdateRoom": {
req: ReqUpdateRoom,
res: ResUpdateRoom
}
},
msg: {
"admin/UpdateRoomState": MsgUpdateRoomState,
"roomMsg/Chat": MsgChat,
"roomMsg/UpdateRoomInfo": MsgUpdateRoomInfo
}
}
export const serviceProto: ServiceProto<ServiceType> = {
"version": 2,
"services": [
{
"id": 0,
"name": "admin/UpdateRoomState",
"type": "msg"
},
{
"id": 1,
"name": "admin/Auth",
"type": "api"
},
{
"id": 2,
"name": "admin/CreateRoom",
"type": "api"
},
{
"id": 3,
"name": "ExitRoom",
"type": "api",
"conf": {}
},
{
"id": 4,
"name": "JoinRoom",
"type": "api",
"conf": {}
},
{
"id": 5,
"name": "UpdateRoom",
"type": "api",
"conf": {}
},
{
"id": 6,
"name": "roomMsg/Chat",
"type": "msg"
},
{
"id": 7,
"name": "roomMsg/UpdateRoomInfo",
"type": "msg"
}
],
"types": {
"admin/MsgUpdateRoomState/MsgUpdateRoomState": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "userNum",
"type": {
"type": "Number",
"scalarType": "uint"
}
},
{
"id": 1,
"name": "rooms",
"type": {
"type": "Array",
"elementType": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "id",
"type": {
"type": "String"
}
},
{
"id": 1,
"name": "name",
"type": {
"type": "String"
}
},
{
"id": 2,
"name": "userNum",
"type": {
"type": "Number",
"scalarType": "uint"
}
},
{
"id": 3,
"name": "startMatchTime",
"type": {
"type": "Number",
"scalarType": "uint"
},
"optional": true
},
{
"id": 4,
"name": "updateTime",
"type": {
"type": "Number",
"scalarType": "uint"
}
}
]
}
}
}
]
},
"admin/PtlAuth/ReqAuth": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "adminToken",
"type": {
"type": "String"
}
}
]
},
"admin/PtlAuth/ResAuth": {
"type": "Interface"
},
"admin/PtlCreateRoom/ReqCreateRoom": {
"type": "Interface",
"properties": [
{
"id": 2,
"name": "adminToken",
"type": {
"type": "String"
}
},
{
"id": 3,
"name": "creator",
"type": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "uid",
"type": {
"type": "String"
}
},
{
"id": 1,
"name": "nickname",
"type": {
"type": "String"
}
}
]
}
},
{
"id": 1,
"name": "roomName",
"type": {
"type": "String"
}
}
]
},
"admin/PtlCreateRoom/ResCreateRoom": {
"type": "Interface",
"properties": [
{
"id": 1,
"name": "roomId",
"type": {
"type": "String"
}
}
]
},
"PtlExitRoom/ReqExitRoom": {
"type": "Interface",
"extends": [
{
"id": 0,
"type": {
"type": "Reference",
"target": "../base/BaseRequest"
}
}
]
},
"../base/BaseRequest": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "sso",
"type": {
"type": "String"
},
"optional": true
}
]
},
"PtlExitRoom/ResExitRoom": {
"type": "Interface",
"extends": [
{
"id": 0,
"type": {
"type": "Reference",
"target": "../base/BaseResponse"
}
}
]
},
"../base/BaseResponse": {
"type": "Interface"
},
"PtlJoinRoom/ReqJoinRoom": {
"type": "Interface",
"extends": [
{
"id": 0,
"type": {
"type": "Reference",
"target": "../base/BaseRequest"
}
}
],
"properties": [
{
"id": 0,
"name": "roomId",
"type": {
"type": "String"
}
}
]
},
"PtlJoinRoom/ResJoinRoom": {
"type": "Interface",
"extends": [
{
"id": 0,
"type": {
"type": "Reference",
"target": "../base/BaseResponse"
}
}
]
},
"PtlUpdateRoom/ReqUpdateRoom": {
"type": "Interface",
"extends": [
{
"id": 0,
"type": {
"type": "Reference",
"target": "../base/BaseRequest"
}
}
],
"properties": [
{
"id": 0,
"name": "roomId",
"type": {
"type": "String"
}
},
{
"id": 1,
"name": "roomName",
"type": {
"type": "String"
}
}
]
},
"PtlUpdateRoom/ResUpdateRoom": {
"type": "Interface",
"extends": [
{
"id": 0,
"type": {
"type": "Reference",
"target": "../base/BaseResponse"
}
}
]
},
"roomMsg/MsgChat/MsgChat": {
"type": "Interface"
},
"roomMsg/MsgUpdateRoomInfo/MsgUpdateRoomInfo": {
"type": "Interface"
}
}
};

View File

@ -0,0 +1,4 @@
export interface UserInfo {
id: string,
nickname: string
}

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,15 @@
{
"compilerOptions": {
"lib": [
"es2018"
],
"module": "commonjs",
"target": "es2018",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node"
}
}

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"
]
}

View File

@ -0,0 +1,47 @@
import { CodeTemplate, TsrpcConfig } from 'tsrpc-cli';
const tsrpcConf: TsrpcConfig = {
// Generate ServiceProto
proto: [
{
ptlDir: 'src/shared/protocols/hallServer', // Protocol dir
output: 'src/shared/protocols/serviceProto_hallServer.ts', // Path for generated ServiceProto
apiDir: 'src/HallServer/api', // API dir
docDir: 'docs/hallServer', // API documents dir
ptlTemplate: CodeTemplate.getExtendedPtl(),
// msgTemplate: CodeTemplate.getExtendedMsg(),
},
{
ptlDir: 'src/shared/protocols/roomServer', // Protocol dir
output: 'src/shared/protocols/serviceProto_roomServer.ts', // Path for generated ServiceProto
apiDir: 'src/RoomServer/api', // API dir
docDir: 'docs/roomServer', // 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;

View File

@ -15,10 +15,10 @@
"@types/node": "^15.14.9", "@types/node": "^15.14.9",
"onchange": "^7.1.0", "onchange": "^7.1.0",
"ts-node": "^9.1.1", "ts-node": "^9.1.1",
"tsrpc-cli": "^2.4.0", "tsrpc-cli": "^2.0.8",
"typescript": "^4.5.5" "typescript": "^4.4.3"
}, },
"dependencies": { "dependencies": {
"tsrpc": "^3.2.0" "tsrpc": "^3.0.9"
} }
} }

View File

@ -6,8 +6,7 @@ import { serviceProto } from "./shared/protocols/serviceProto";
// Create the Server // Create the Server
const server = new HttpServer(serviceProto, { const server = new HttpServer(serviceProto, {
port: 3000, port: 3000,
cors: '*', cors: '*'
json: true
}); });
// Flow: Serve static files // Flow: Serve static files

View File

@ -7,15 +7,15 @@
"build": "webpack --mode=production" "build": "webpack --mode=production"
}, },
"devDependencies": { "devDependencies": {
"copy-webpack-plugin": "^9.1.0", "copy-webpack-plugin": "^9.0.1",
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^5.3.2",
"ts-loader": "^9.2.6", "ts-loader": "^9.2.6",
"typescript": "^4.5.5", "typescript": "^4.4.3",
"webpack": "^5.69.1", "webpack": "^5.57.1",
"webpack-cli": "^4.9.2", "webpack-cli": "^4.9.0",
"webpack-dev-server": "^3.11.3" "webpack-dev-server": "^3.11.2"
}, },
"dependencies": { "dependencies": {
"tsrpc-browser": "^3.2.0" "tsrpc-browser": "^3.0.7"
} }
} }

View File

@ -19,6 +19,6 @@
"typescript": "^4.4.3" "typescript": "^4.4.3"
}, },
"dependencies": { "dependencies": {
"tsrpc": "^3.1.8" "tsrpc": "^3.0.9"
} }
} }

View File

@ -4,8 +4,5 @@ module.exports = {
], ],
timeout: 999999, timeout: 999999,
exit: true, exit: true,
'preserve-symlinks': true, 'preserve-symlinks': true
spec: [
'./test/**/*.test.ts'
]
} }

View File

@ -9,21 +9,20 @@
"api": "tsrpc api", "api": "tsrpc api",
"doc": "tsrpc doc", "doc": "tsrpc doc",
"dev": "tsrpc dev", "dev": "tsrpc dev",
"build": "tsrpc build", "build": "tsrpc build"
"test": "mocha"
}, },
"devDependencies": { "devDependencies": {
"@types/mocha": "^8.2.3", "@types/mocha": "^8.2.3",
"@types/mongodb": "^3.6.20", "@types/mongodb": "^3.6.20",
"@types/node": "^15.14.9", "@types/node": "^15.14.9",
"mocha": "^9.2.1", "mocha": "^9.1.2",
"onchange": "^7.1.0", "onchange": "^7.1.0",
"ts-node": "^10.5.0", "ts-node": "^10.2.1",
"tsrpc-cli": "^2.4.0", "tsrpc-cli": "^2.0.8",
"typescript": "^4.5.5" "typescript": "^4.4.3"
}, },
"dependencies": { "dependencies": {
"mongodb": "^4.4.0", "mongodb": "^3.7.2",
"tsrpc": "^3.2.0" "tsrpc": "^3.0.9"
} }
} }

View File

@ -1,4 +1,3 @@
import { Document, ObjectId } from "mongodb";
import { ApiCall } from "tsrpc"; import { ApiCall } from "tsrpc";
import { Global } from "../models/Global"; import { Global } from "../models/Global";
import { ReqAddPost, ResAddPost } from "../shared/protocols/PtlAddPost"; import { ReqAddPost, ResAddPost } from "../shared/protocols/PtlAddPost";

View File

@ -1,11 +1,11 @@
import { ObjectId } from 'mongodb'; import { ObjectID } from 'mongodb';
import { ApiCall } from "tsrpc"; import { ApiCall } from "tsrpc";
import { Global } from "../models/Global"; import { Global } from "../models/Global";
import { ReqDelPost, ResDelPost } from "../shared/protocols/PtlDelPost"; import { ReqDelPost, ResDelPost } from "../shared/protocols/PtlDelPost";
export async function ApiDelPost(call: ApiCall<ReqDelPost, ResDelPost>) { export async function ApiDelPost(call: ApiCall<ReqDelPost, ResDelPost>) {
let op = await Global.collection('Post').deleteOne({ let op = await Global.collection('Post').deleteOne({
_id: call.req._id _id: new ObjectID(call.req._id)
}) })
call.succ({}); call.succ({});

View File

@ -1,11 +1,11 @@
import { ObjectId } from 'mongodb'; import { ObjectID } from 'mongodb';
import { ApiCall } from "tsrpc"; import { ApiCall } from "tsrpc";
import { Global } from "../models/Global"; import { Global } from "../models/Global";
import { ReqGetPost, ResGetPost } from "../shared/protocols/PtlGetPost"; import { ReqGetPost, ResGetPost } from "../shared/protocols/PtlGetPost";
export async function ApiGetPost(call: ApiCall<ReqGetPost, ResGetPost>) { export async function ApiGetPost(call: ApiCall<ReqGetPost, ResGetPost>) {
let op = await Global.collection('Post').findOne({ let op = await Global.collection('Post').findOne({
_id: call.req._id _id: new ObjectID(call.req._id)
}); });
if (!op) { if (!op) {
@ -16,7 +16,7 @@ export async function ApiGetPost(call: ApiCall<ReqGetPost, ResGetPost>) {
call.succ({ call.succ({
post: { post: {
...op, ...op,
_id: op._id _id: op._id.toHexString()
} }
}) })
} }

View File

@ -1,16 +1,16 @@
import { ObjectId } from 'mongodb'; import { ObjectID } from 'mongodb';
import { ApiCall } from "tsrpc"; import { ApiCall } from "tsrpc";
import { Global } from "../models/Global"; import { Global } from "../models/Global";
import { ReqUpdatePost, ResUpdatePost } from "../shared/protocols/PtlUpdatePost"; import { ReqUpdatePost, ResUpdatePost } from "../shared/protocols/PtlUpdatePost";
export async function ApiUpdatePost(call: ApiCall<ReqUpdatePost, ResUpdatePost>) { export async function ApiUpdatePost(call: ApiCall<ReqUpdatePost, ResUpdatePost>) {
let { _id, ...rest } = call.req.update; let { _id, ...update } = call.req.update;
let op = await Global.collection('Post').updateOne({ let op = await Global.collection('Post').updateOne({
_id: _id _id: new ObjectID(_id)
}, { }, {
$set: { $set: {
...rest, ...update,
update: { update: {
uid: 'xxx', uid: 'xxx',
time: new Date() time: new Date()

View File

@ -1,4 +1,4 @@
export const BackConfig = { export const BackConfig = {
// Please replace by your db // Please replace by your db
mongoDb: 'mongodb+srv://test:test@tsrpc-example.0gzai.mongodb.net/tsrpc-example?retryWrites=true&w=majority', mongoDb: 'mongodb://username:password@xxx.com:27017/test?authSource=admin',
} }

View File

@ -1,7 +1,7 @@
import { Collection, Db, MongoClient, OptionalId } from "mongodb"; import { Collection, Db, MongoClient } from "mongodb";
import { Logger } from "tsrpc"; import { Logger } from "tsrpc";
import { DbPost } from "../shared/db/DbPost";
import { BackConfig } from "./BackConfig"; import { BackConfig } from "./BackConfig";
import { DbPost } from "./dbItems/DbPost";
export class Global { export class Global {
@ -14,7 +14,7 @@ export class Global {
this.db = client.db(); this.db = client.db();
} }
static collection<T extends keyof DbCollectionType>(col: T): Collection<OptionalId<DbCollectionType[T]>> { static collection<T extends keyof DbCollectionType>(col: T): Collection<DbCollectionType[T]> {
return this.db.collection(col); return this.db.collection(col);
} }

View File

@ -0,0 +1,7 @@
import { ObjectID } from "mongodb";
import { Overwrite } from "tsrpc";
import { Post } from "../../shared/protocols/models/Post";
export type DbPost = Overwrite<Post, {
_id: ObjectID
}>

View File

@ -1,7 +1,7 @@
import { DbPost } from "../db/DbPost"; import { Post } from "./models/Post";
export interface ReqAddPost { export interface ReqAddPost {
newPost: Omit<DbPost, '_id' | 'create' | 'update' | 'visitedNum'>; newPost: Omit<Post, '_id' | 'create' | 'update' | 'visitedNum'>;
} }
export interface ResAddPost { export interface ResAddPost {

View File

@ -1,7 +1,5 @@
import { ObjectId } from "bson";
export interface ReqDelPost { export interface ReqDelPost {
_id: ObjectId; _id: string;
} }
export interface ResDelPost { export interface ResDelPost {

View File

@ -1,10 +1,9 @@
import { ObjectId } from "bson"; import { Post } from "./models/Post";
import { DbPost } from "../db/DbPost";
export interface ReqGetPost { export interface ReqGetPost {
_id: ObjectId; _id: string;
} }
export interface ResGetPost { export interface ResGetPost {
post: DbPost; post: Post;
} }

View File

@ -1,7 +1,7 @@
import { DbPost } from "../db/DbPost"; import { Post } from "./models/Post";
export interface ReqUpdatePost { export interface ReqUpdatePost {
update: Pick<DbPost, '_id'> & Partial<Pick<DbPost, 'title' | 'content'>>; update: { _id: string } & Partial<Pick<Post, 'title' | 'content'>>;
} }
export interface ResUpdatePost { export interface ResUpdatePost {

View File

@ -1,7 +1,5 @@
import { ObjectId } from "mongodb"; export interface Post {
_id: string;
export interface DbPost {
_id: ObjectId;
author: string; author: string;
title: string; title: string;
content: string; content: string;

View File

@ -29,7 +29,7 @@ export interface ServiceType {
} }
export const serviceProto: ServiceProto<ServiceType> = { export const serviceProto: ServiceProto<ServiceType> = {
"version": 17, "version": 16,
"services": [ "services": [
{ {
"id": 0, "id": 0,
@ -62,7 +62,7 @@ export const serviceProto: ServiceProto<ServiceType> = {
"type": { "type": {
"target": { "target": {
"type": "Reference", "type": "Reference",
"target": "../db/DbPost/DbPost" "target": "models/Post/Post"
}, },
"keys": [ "keys": [
"_id", "_id",
@ -75,15 +75,14 @@ export const serviceProto: ServiceProto<ServiceType> = {
} }
] ]
}, },
"../db/DbPost/DbPost": { "models/Post/Post": {
"type": "Interface", "type": "Interface",
"properties": [ "properties": [
{ {
"id": 0, "id": 0,
"name": "_id", "name": "_id",
"type": { "type": {
"type": "Reference", "type": "String"
"target": "?mongodb/ObjectId"
} }
}, },
{ {
@ -182,8 +181,7 @@ export const serviceProto: ServiceProto<ServiceType> = {
"id": 0, "id": 0,
"name": "_id", "name": "_id",
"type": { "type": {
"type": "Reference", "type": "String"
"target": "?bson/ObjectId"
} }
} }
] ]
@ -198,8 +196,7 @@ export const serviceProto: ServiceProto<ServiceType> = {
"id": 0, "id": 0,
"name": "_id", "name": "_id",
"type": { "type": {
"type": "Reference", "type": "String"
"target": "?bson/ObjectId"
} }
} }
] ]
@ -212,7 +209,7 @@ export const serviceProto: ServiceProto<ServiceType> = {
"name": "post", "name": "post",
"type": { "type": {
"type": "Reference", "type": "Reference",
"target": "../db/DbPost/DbPost" "target": "models/Post/Post"
} }
} }
] ]
@ -227,26 +224,28 @@ export const serviceProto: ServiceProto<ServiceType> = {
"type": "Intersection", "type": "Intersection",
"members": [ "members": [
{ {
"id": 4, "id": 1,
"type": { "type": {
"target": { "type": "Interface",
"type": "Reference", "properties": [
"target": "../db/DbPost/DbPost" {
}, "id": 0,
"keys": [ "name": "_id",
"_id" "type": {
], "type": "String"
"type": "Pick" }
}
]
} }
}, },
{ {
"id": 5, "id": 3,
"type": { "type": {
"type": "Partial", "type": "Partial",
"target": { "target": {
"target": { "target": {
"type": "Reference", "type": "Reference",
"target": "../db/DbPost/DbPost" "target": "models/Post/Post"
}, },
"keys": [ "keys": [
"title", "title",

View File

@ -7,16 +7,16 @@
"build": "webpack --mode=production" "build": "webpack --mode=production"
}, },
"devDependencies": { "devDependencies": {
"copy-webpack-plugin": "^9.1.0", "copy-webpack-plugin": "^9.0.1",
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^5.3.2",
"ts-loader": "^9.2.6", "ts-loader": "^9.2.6",
"typescript": "^4.5.5", "typescript": "^4.4.3",
"webpack": "^5.69.1", "webpack": "^5.57.1",
"webpack-cli": "^4.9.2", "webpack-cli": "^4.9.0",
"webpack-dev-server": "^3.11.3" "webpack-dev-server": "^3.11.2"
}, },
"dependencies": { "dependencies": {
"tsrpc-browser": "^3.2.0" "tsrpc-browser": "^3.0.7"
}, },
"browserslist": [ "browserslist": [
"defaults" "defaults"

View File

@ -1,9 +0,0 @@
// TSRPC would decode ObjectId as string in frontend.
declare module 'mongodb' {
export type ObjectId = string;
export type ObjectID = string;
}
declare module 'bson' {
export type ObjectId = string;
export type ObjectID = string;
}