mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
3202 文字
8 分
Nest.jsとAngularベースの入札プラットフォーム―JestテストとCI/CDも含む
2024-12-07
2024-12-08

Nest.js と Angular をベースにした入札プラットフォーム#

プロジェクト概要#

本プロジェクトは Nest.js と Angular をベースにした入札プラットフォームであり、包括的な入札・管理システムの提供を目的としています。

主な機能には、ユーザー登録とログイン、プロジェクトの作成と管理、入札管理、ユーザーロール管理が含まれます。フロントエンドは Angular、バックエンドは Nest.js で構築され、データベースには PostgreSQL を使用し、Swagger で API ドキュメントを提供しています。

プロジェクトは DigitalOcean の Droplet にデプロイされ、フロントエンドは Nginx を使用して配信されます。

前端 (Angular)
↓(API 请求)
Cognito (用户认证)
↓(验证通过后请求转发)
后端 (Nest.js)
↓(数据库查询)
数据库 (PostgreSQL)
↑(数据返回)
后端 (Nest.js)
↑(处理后的响应)
前端 (Angular)

プロジェクト構成#

  • frontend: Angular で構築された、すべてのフロントエンドコードが含まれます。
  • backend: Nest.js で構築された、すべてのバックエンドコードが含まれます。
  • .github: 継続的インテグレーションとデプロイに使用する GitHub Actions の設定ファイルが含まれます。

バックエンド#

バックエンドの構築#

バックエンドは Nest.js で構築されており、モジュール化された拡張性の高いアーキテクチャを提供します。主な機能には、ユーザー認証、プロジェクト管理、入札管理などがあります。データベース操作には TypeORM を使用し、複数のデータベース形式に対応しています。

バックエンドの技術スタック#

  • Nest.js: 効率的で拡張性の高い Node.js サーバーサイドアプリケーションの構築に使用します。
  • TypeORM: データベースとの連携に使用する ORM フレームワークです。
  • Swagger: API ドキュメントの生成に使用し、開発者が API を確認・テストしやすくします。

バックエンドの構築手順#

  1. 依存関係のインストール: backend ディレクトリで npm install を実行し、必要な依存関係をすべてインストールします。
  2. 環境変数の設定: プロジェクトのルートディレクトリに .env ファイルを作成し、データベース接続情報やその他の環境変数を設定します。
  3. 開発サーバーの実行: npm run start:dev を使用して、ホットリロードに対応した開発サーバーを起動します。
  4. 本番ビルド: npm run build を使用して本番環境向けにビルドします。生成されたファイルは dist ディレクトリに配置されます。

データベース#

本プロジェクトでは PostgreSQL をデータベースとして使用し、すべてのデータベース操作を TypeORM 経由で行います。データベース初期化スクリプトは backend/SQL/init-script.sql にあり、データベースの作成と初期化に使用できます。

バックエンドのコード構成は明確で、モジュール化された設計により、機能の拡張と保守が容易になっています。

バックエンドのセキュリティ認証#

バックエンドのセキュリティ認証は AWS Cognito で実装されており、Nest.js のインターセプターとサービスを組み合わせて、ユーザーの認証と認可を保証します。

セキュリティ認証アーキテクチャ#

  • AWS Cognito: ユーザー登録、ログイン、本人確認に使用します。Cognito は、安全なユーザープールと ID プールの管理機能を提供します。
  • Nest.js インターセプター: HTTP リクエストをインターセプトし、リクエストヘッダー内の JWT トークンを検証して、ユーザー ID の正当性を保証します。
  • Service 層: Cognito との連携、および Cognito ユーザーとデータベース内のユーザー情報の関連付けを担当します。

実装手順#

  1. Cognito ユーザープールの設定: AWS Cognito でユーザープールを作成し、JWT トークンの生成と検証に対応するようアプリケーションクライアントを設定します。

  2. JWT インターセプター: Nest.js でインターセプターを作成し、リクエストヘッダー内の JWT トークンを解析して有効性を検証し、ユーザー情報をリクエストオブジェクトに追加します。

    import { Injectable, NestInterceptor, ExecutionContext, CallHandler, UnauthorizedException } from '@nestjs/common';
    import { Observable } from 'rxjs';
    import { AuthService } from './auth.service';
    @Injectable()
    export class JwtInterceptor implements NestInterceptor {
    constructor(private readonly authService: AuthService) {}
    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization?.split(' ')[1];
    if (!token) {
    throw new UnauthorizedException('Token not found');
    }
    const user = this.authService.validateToken(token);
    if (!user) {
    throw new UnauthorizedException('Invalid token');
    }
    request.user = user;
    return next.handle();
    }
    }
  3. ユーザーサービス: データベースからユーザー情報を取得し、Cognito ユーザーと関連付けるユーザーサービスを作成します。Cognito ID を一意の識別子として使用し、ユーザー情報をデータベースに保存します。

    import { Injectable } from '@nestjs/common';
    import { UsersRepository } from './users.repository';
    @Injectable()
    export class UsersService {
    constructor(private readonly usersRepository: UsersRepository) {}
    async findOrCreateUser(cognitoId: string, email: string) {
    let user = await this.usersRepository.findOneByCognitoId(cognitoId);
    if (!user) {
    user = await this.usersRepository.create({ cognitoId, email });
    }
    return user;
    }
    }
  4. ロールと権限の管理: データベースでユーザーロール(管理者、クライアント、入札者など)を定義し、インターセプター内でロールに基づいて権限を検証します。

    import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
    import { Reflector } from '@nestjs/core';
    @Injectable()
    export class RolesGuard implements CanActivate {
    constructor(private reflector: Reflector) {}
    canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!roles) {
    return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    return roles.includes(user.role);
    }
    }

    権限の検証が必要な API に @Roles('admin') デコレーターを追加し、必要なロールを指定します。

    @Post()
    @Roles('admin')
    createProject(@Body() createProjectDto: CreateProjectDto) {
    return this.projectsService.createProject(createProjectDto);
    }

この方法により、バックエンドはユーザー ID と権限を効果的に管理し、システムの安全性と信頼性を確保できます。

プロジェクト管理の実装#

プロジェクト管理モジュールでは、Controller から Service を呼び出し、Service を介してデータベースと連携する方法を示します。

Controller#

ProjectsController では、HTTP リクエストを処理するルートとメソッドを定義します。

import { Controller, Get, Post, Body, Param, Put, Delete } from '@nestjs/common';
import { ProjectsService } from './projects.service';
import { ProjectsDto } from '../entities/DTO/projects.dto';
@Controller('projects')
export class ProjectsController {
constructor(private readonly projectsService: ProjectsService) {}
@Get()
findAll() {
return this.projectsService.findAll();
}
@Get(':id')
findOne(@Param('id') id: number) {
return this.projectsService.findOne(id);
}
@Post()
create(@Body() projectDto: ProjectsDto) {
return this.projectsService.create(projectDto);
}
@Put(':id')
update(@Param('id') id: number, @Body() projectDto: ProjectsDto) {
return this.projectsService.update(id, projectDto);
}
@Delete(':id')
delete(@Param('id') id: number) {
return this.projectsService.delete(id);
}
}

Service#

ProjectsService は、ビジネスロジックの処理とデータベースとの連携を担当します。

import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { Project } from '../entities/projects.entity';
import { ProjectsDto } from '../entities/DTO/projects.dto';
@Injectable()
export class ProjectsService {
constructor(private dataSource: DataSource) {}
findAll() {
return this.dataSource.getRepository(Project).find();
}
findOne(id: number) {
return this.dataSource.getRepository(Project).findOneBy({ project_id: id });
}
create(project: ProjectsDto) {
return this.dataSource.getRepository(Project).save(project);
}
update(id: number, project: ProjectsDto) {
return this.dataSource.getRepository(Project).update(id, project);
}
delete(id: number) {
return this.dataSource.getRepository(Project).delete(id);
}
}

データベースエンティティ#

Project エンティティは、データベース内のプロジェクト構造を定義します。エンティティは @Entity() デコレーター、列は @Column() デコレーターで定義します。

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Project {
@PrimaryGeneratedColumn()
project_id: number;
@Column()
title: string;
@Column()
description: string;
@Column('decimal')
budget_min: number;
@Column('decimal')
budget_max: number;
@Column('date')
deadline: Date;
@Column({ default: 'open' })
status: string;
}

この方法により、Controller が HTTP リクエストを処理し、Service がビジネスロジックを担当し、データベースエンティティがデータ構造を定義します。この3つが連携することで、完全なプロジェクト管理機能を実現します。

フロントエンド#

フロントエンドは Angular で構築されており、使いやすいインターフェースとインタラクティブな操作体験を提供します。主な機能には、プロジェクトの表示、入札管理、ユーザー登録とログインなどがあります。

フロントエンドの技術スタック#

  • Angular: モダンなシングルページアプリケーションの構築に使用します。
  • RxJS: 非同期データストリームの処理に使用します。
  • Angular CLI: 強力な開発ツールとコマンドラインインターフェースを提供します。

フロントエンドの構築手順#

  1. 依存関係のインストール: frontend ディレクトリで npm install を実行し、必要な依存関係をすべてインストールします。
  2. 開発サーバー: ng serve を使用して開発サーバーを起動します。デフォルトでは http://localhost:4200/ で実行されます。
  3. 本番ビルド: ng build を使用して本番環境向けにビルドします。生成されたファイルは dist ディレクトリに配置されます。

プロジェクト詳細コンポーネント#

フロントエンドアプリケーションは複数のコンポーネントで構成され、それぞれが特定の機能モジュールを担当します。以下にコンポーネントの実装例を示します。

ProjectDetailComponent は、単一プロジェクトの詳細情報を表示するために使用します。

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ProjectsService } from '../../services/projects.service';
import { BidsService } from '../../services/bids.service';
import { AuthService } from '../../services/auth.service';
@Component({
selector: 'app-project-detail',
templateUrl: './project-detail.component.html',
styleUrls: ['./project-detail.component.css']
})
export class ProjectDetailComponent implements OnInit {
project: any = null;
bids: any[] = [];
loading = false;
error = '';
userRole: string = '';
constructor(
private route: ActivatedRoute,
private projectsService: ProjectsService,
private bidsService: BidsService,
private authService: AuthService
) {}
ngOnInit() {
this.userRole = this.authService.getUserRole();
const projectId = this.route.snapshot.paramMap.get('id');
if (projectId) {
this.loadProject(+projectId);
this.loadBids(+projectId);
}
}
loadProject(id: number) {
this.loading = true;
this.projectsService.getProjectById(id).subscribe({
next: (data) => {
this.project = data;
this.loading = false;
},
error: (err) => {
this.error = '加载项目详情失败';
this.loading = false;
console.error('加载项目详情错误:', err);
}
});
}
loadBids(projectId: number) {
this.bidsService.getBidsByProjectId(projectId).subscribe({
next: (data) => {
this.bids = data;
},
error: (err) => {
console.error('加载投标列表错误:', err);
}
});
}
}

テンプレートファイル#

project-detail.component.html は、プロジェクト詳細の表示構造を定義します。

<div class="project-detail">
<div *ngIf="loading" class="loading">
加载中...
</div>
<div *ngIf="error" class="error">
{{ error }}
</div>
<div *ngIf="project && !loading" class="project-info">
<h2>{{ project.title }}</h2>
<div class="project-meta">
<p>预算: ¥{{ project.budget_min }} - ¥{{ project.budget_max }}</p>
<p>截止日期: {{ project.deadline | date }}</p>
<p>状态: {{ project.status }}</p>
</div>
<div class="project-description">
<h3>项目描述</h3>
<p>{{ project.description }}</p>
</div>
<app-bid-form
*ngIf="userRole === 'bidder' && project.status === 'open'"
[projectId]="project.project_id"
(bidSubmitted)="loadBids(project.project_id)">
</app-bid-form>
<div class="bids-section" *ngIf="userRole === 'client' || userRole === 'admin'">
<h3>投标列表</h3>
<div *ngFor="let bid of bids" class="bid-card">
<p>投标人: {{ bid.bidder_id }}</p>
<p>投标金额: ¥{{ bid.amount }}</p>
<p>投标说明: {{ bid.message }}</p>
<p>状态: {{ bid.status }}</p>
</div>
</div>
</div>
</div>

この方法により、フロントエンドアプリケーションは、充実したユーザー操作とデータ表示機能を提供できます。

テスト#

本プロジェクトでは Jest を使用して単体テストと統合テストを実施し、コードの正確性と安定性を確保します。また、ESLint でコード品質を検査し、コーディングスタイルの一貫性を保ちます。

Jest テスト#

Jest は、アサーション、モック、スナップショットテストに対応した強力な JavaScript テストフレームワークです。

Jest の設定#

プロジェクトの package.json で Jest を設定します。

"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage"
},
"jest": {
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": "src",
"testRegex": ".*\\\\.spec\\\\.ts$",
"transform": {
"^.+\\\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}

テスト例#

以下に、簡単なサービステストの例を示します。

import { Test, TestingModule } from '@nestjs/testing';
import { ProjectsService } from './projects.service';
describe('ProjectsService', () => {
let service: ProjectsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ProjectsService],
}).compile();
service = module.get<ProjectsService>(ProjectsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('findOne', () => {
it('应该返回单个项目', async () => {
const result = await service.findOne(1);
expect(result).toEqual(mockProject);
});
});
});

ESLint によるコード品質検査#

ESLint は、JavaScript コード内のパターンを識別して報告するためのツールであり、開発者がコードの一貫性と品質を維持するのに役立ちます。

ESLint の設定#

プロジェクトのルートディレクトリに .eslintrc.js ファイルを作成します。

module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

ESLint の実行#

package.json にスクリプトを追加します。

"scripts": {
"lint": "eslint . --ext .ts"
}

npm run lint を実行してコード品質を検査します。

Jest と ESLint を使用することで、コードの正確性と一貫性を確保し、開発効率とコード品質を向上できます。

CI/CD#

本プロジェクトでは、GitHub Actions を使用して継続的インテグレーションと継続的デプロイ(CI/CD)を実現し、コードがコミットされるたびに自動でビルド、テスト、デプロイされるようにしています。

GitHub Actions#

GitHub Actions は、ソフトウェア開発ワークフローを自動化するためのツールです。ワークフローファイルを定義することで、リポジトリ内でビルド、テスト、デプロイの各タスクを自動実行できます。

ワークフローの設定#

プロジェクトの .github/workflows/deploy.yml ファイルで CI/CD ワークフローを定義します。

name: CI/CD Pipeline
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '20.18.0'
- name: Install dependencies
run: |
cd backend
npm install
cd ../frontend
npm install
- name: Run tests
run: |
cd backend
npm run test:cov
cd ../frontend
npm run test
- name: Lint code
run: |
cd backend
npm run lint
cd ../frontend
npm run lint
- name: Build project
run: |
cd backend
npm run build
cd ../frontend
npm run build
- name: Create Release Package
run: |
mkdir -p build
cd backend
tar -czvf ../build/backend.tar.gz dist
cd ../frontend
tar -czvf ../build/frontend.tar.gz dist
cd ..
- name: Deploy to DigitalOcean
uses: digitalocean/action-doctl@v2
with:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
run: |
# 部署脚本或命令

デプロイ#

  • DigitalOcean: プロジェクトは DigitalOcean の Droplet にデプロイされ、フロントエンドは Nginx を使用して配信されます。
  • 自動化フロー: コードが main ブランチにコミットされるたびに、GitHub Actions がビルド、テスト、デプロイのフローを自動的に実行します。

この方法により、プロジェクトはコードの変更に迅速に対応し、コミットされたコードが毎回厳格なテストと検証を通過することを保証したうえで、本番環境へ自動的にデプロイできます。

共有

この記事が役に立ったときは、ぜひ他の人に共有してください!

Nest.jsとAngularベースの入札プラットフォーム―JestテストとCI/CDも含む
https://dreaife.tokyo/jp/posts/bidding-platform-nest-angular/
著者
dreaife
公開日
2024-12-07
ライセンス
CC BY-NC-SA 4.0

一部の情報は古い可能性があります

関連した投稿 スマート
1
Angularベースのアニメーション展示サイト+ログイン・ユーザー登録(Cognito)
開拓 Angular 16とAWS CognitoでBangumiのアニメ展示サイトを構築。ログイン・登録、検索、カレンダー表示、GitHub ActionsによるPages配備を扱います。
2
EOAウォレットの署名検証とその関連事項について
開拓 secp256k1の有限体と点演算からECDSAのr/s/v署名と公開鍵復元を導出し、SIWEを例に、サーバーが秘密鍵に触れずメッセージハッシュとkeccak-256でEOAアドレスの管理権を検証する仕組みを解説します。
3
ブラウザ上でEVMトランザクションを手動で完了する
開拓 ブラウザのコンソールで EVM トランザクションを手動で実行します。JSON-RPC でローカルの Anvil テストチェーンに接続し、ウォレットを呼び出してトランザクションを送信し、手動でブロックを生成します。さらに、トランザクションハッシュの検証、署名者アドレスの復元、Merkle 包含証明の検証を通じて、ブロードキャストからブロックへの取り込み、オンチェーンでの承認までの一連の流れを直感的に理解できます。
4
EOA向けEVMウォレットのログイン画面
開拓 React/wagmi製のEVMウォレットログインを例に、接続、サーバー発行nonce、SIWEメッセージ、署名、バックエンドでのアドレス復元を追い、アドレス接続と管理権の証明を分ける理由を解説します。
5
EOAウォレットのオンチェーン基本操作について
開拓 ウォレットの視点からEOA/HDウォレットの鍵派生、EIP-1559トランザクション、SIWE・EIP-191・EIP-712署名を整理し、ethers.jsによる作成・検証・ブロードキャストの実装例を示します。

目次