Scrumツールキット#3 - Node, TypeScript, WebsocketとTypeORMを使ったサーバー開発
前回の記事では、アプリケーションのクライアント側のセットアップをカバーしました。今日はサーバー側について詳しく見ていきます。🚀
アプリケーションAPIは、ファイル提供用のExpressフレームワークと、通信用のWebsocketで書かれています。サーバーのエントリーファイルは以下の通りです。
// ...
dotenv.config();
const port = process.env.PORT;
const app: Express = express();
const server = http.createServer(app);
app.use(express.static(path.join(__dirname, 'public')));
app.get('(/*)?', async (req, res, next) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
AppDataSource.initialize().then(async () => {
console.info('データベース接続成功');
}).catch((error) => {
console.error(error);
});
const io = new Server<IncomingEvents, OutgoingEvents, {}, User>(server, {
transports: ['websocket', 'polling'],
});
io.on('connection', (socket: Socket<IncomingEvents, OutgoingEvents, {}, User>) => {
registerUsersHandlers(io, socket);
registerCardsHandlers(io, socket);
registerBoardsHandlers(io, socket);
});
server.listen(port, () => {
// eslint-disable-next-line no-console
console.log(`サーバーが起動しています http://localhost:${port}`);
});
全画面モードにする エクシット全画面モード
クライアント側で行ったように、サーバーでも同様にイベントを登録しています。これは同じイベントセットを使用しているためです。
export type IncomingUsersEvents = {
Join: (data: {boardId: string, nickname: string; avatar: number;}) => void;
SetSelectedPlanningCard: (data: {selectedPlanningCard: number}) => void;
ToggleReady: () => void;
ChangeUserData: (data: {nickname: string, avatar: number}) => void;
}
export type OutgoingUsersEvents = {
Joined: (data: {
localUser: RawUser,
users: RawUser[],
cards: RawCard[],
board: {id: string, stage: number, maxVotes: number, timerTo: number, mode: string},
}) => void;
UserState: (data: {user: RawUser}) => void;
UsersState: (data: {users: RawUser[]}) => void;
}
export type IncomingCardsEvents = {
CreateCard: (data: {content: string, column: number}) => void;
UpdateCard: (data: {cardId: string, content: string}) => void;
DeleteCard: (data: {cardId: string}) => void;
GetCards: () => void;
GroupCards: (data: {cardId: string, stackedOn: string}) => void;
UngroupCards: (data: {cardId: string}) => void;
UpvoteCard: (data: {cardId: string}) => void;
DownvoteCard: (data: {cardId: string}) => void;
}
export type OutgoingCardsEvents = {
CardState: (data: {card: RawCard}) => void;
DeleteCard: (data: {cardId: string}) => void;
CardsState: (data: {cards: RawCard[]}) => void;
}
export type IncomingBoardsEvents = {
SetTimer: (data: {duration: number}) => void;
SetBoardMode: (data: { mode: string }) => void;
SetMaxVotes: (data: {maxVotes: number}) => void;
SetStage: (data: {stage: number}) => void;
}
export type OutgoingBoardsEvents = {
BoardConfig: (data: {board: {
stage: number,
timerTo: number,
maxVotes: number,
mode: string,
}}) => void;
}
export type IncomingEvents = IncomingUsersEvents & IncomingCardsEvents & IncomingBoardsEvents;
export type OutgoingEvents = OutgoingUsersEvents & OutgoingCardsEvents & OutgoingBoardsEvents;
全画面モードにする エクシット全画面モード
そして、ハンドラーは以下のようにこれらのイベントを使用しています。
// ...
const registerCardsHandlers = (
io: Server<IncomingEvents, OutgoingEvents, {}, User>,
socket: Socket<IncomingEvents, OutgoingEvents, {}, User>,
) => {
socket.on('CreateCard', async ({ content, column }) => {
try {
if (Joi.string().min(1).max(512).validate(content).error) {
console.error(`CreateCard: 不正なコンテンツ: ${content}`);
return;
}
if (Joi.number().allow(0, 1, 2).validate(column).error) {
console.error(`CreateCard: 不正なカラム: ${column}`);
return;
}
const card = await Cards.create({
content,
column,
board: {
id: socket.data.boardId,
},
user: {
id: socket.data.userId,
},
stackedOn: '',
votes: [],
}).save();
io.to(socket.data.boardId || '')
.emit('CardState', { card: getRawCard(card) });
} catch (error) {
console.error(error);
}
});
// ...
export default registerCardsHandlers;
全画面モードにする エクシット全画面モード
バックエンドサーバーとの通信にはTypeORMを使用しています。初めはPostgresに接続していましたが、私の目的には過剰だったため、調達、開発、維持が速いSQLiteに切り替えました。もしPostgresに戻したい場合は、dataSourceの設定で数行変更するだけです。
import { DataSource } from 'typeorm';
import dotenv from 'dotenv';
import Boards from './Boards';
import Cards from './Cards';
import Users from './Users';
import Votes from './Votes';
dotenv.config();
const AppDataSource = new DataSource({
type: 'sqlite',
database: './db.sqlite',
synchronize: true,
logging: true,
entities: [Boards, Cards, Users, Votes],
subscribers: [],
migrations: [],
});
export default AppDataSource;
全画面モードにする エクシット全画面モード
モデルは、いくつかの静的メソッドを備えたシンプルなエンティティクラスで、TypeORMのBaseEntityを拡張しています。これらのメソッドを使用してSQLクエリを作成・実行できます。
export enum BoardMode {
RETRO= 'retro',
PLANNING_HIDDEN = 'planning_hidden',
PLANNING_REVEALED = 'planning_revealed',
}
@Entity()
export default class Boards extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@OneToMany(() => Cards, (card) => card.board)
cards: Cards[];
@OneToMany(() => Users, (user) => user.board)
users: Users[];
@Column({
type: 'integer',
name: 'stage',
})
stage: number;
@Column({
type: 'integer',
name: 'max_votes',
})
maxVotes: number;
@Column({
type: 'varchar',
name: 'mode',
})
mode: string;
@Column({
name: 'timer_to',
})
timerTo: Date;
@CreateDateColumn({
name: 'created_at',
})
createdAt: Date;
@UpdateDateColumn({
name: 'updated_at',
})
updatedAt: Date;
}
全画面モードにする エクシット全画面モード
TypeORMは小規模なAPIサーバーに使用するには素晴らしい選択でした。Sequelizeと比較して非常にクリアな文法を持ち、TypeScriptとも非常にうまく動作します。
こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/meatboy/writing-scrum-toolkit-3-server-with-node-typescript-websocket-and-typeorm-47ka