diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9ef41f62d..9100c5d24 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,6 +40,16 @@ For detailed development guidelines including linting and code style requirement - [DEVELOPMENT.md (English)](docs/en/DEVELOPMENT.md) - [DEVELOPMENT.md (日本語)](docs/ja/DEVELOPMENT.md) +## Coding Standards + +When contributing code, please follow the coding standards for each package: + +- **CDK Infrastructure**: [packages/cdk/CODING_RULES.md](packages/cdk/CODING_RULES.md) +- **Backend API (Lambda)**: [packages/cdk/lambda/CODING_RULES.md](packages/cdk/lambda/CODING_RULES.md) +- **Frontend (React)**: [packages/web/CODING_RULES.md](packages/web/CODING_RULES.md) + +These coding standards ensure consistency, maintainability, and quality across the codebase. Please review the relevant coding rules before making changes to ensure your contributions align with the project's standards. + GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). diff --git a/packages/cdk/CODING_RULES.md b/packages/cdk/CODING_RULES.md new file mode 100644 index 000000000..c98dd19e1 --- /dev/null +++ b/packages/cdk/CODING_RULES.md @@ -0,0 +1,509 @@ +# コーディング規約 + +## CDK (AWS Cloud Development Kit) + +### 1. ファイル・ディレクトリ構成 + +**概要**: 機能別にディレクトリを分けて、コードの可読性と保守性を向上させる + +#### 基本構成 + +``` +packages/cdk/ +├── bin/ # CDKアプリのエントリーポイント +├── lib/ # CDK Stackとメインロジック +│ ├── construct/ # 再利用可能なConstruct +│ └── utils/ # ヘルパー関数・ユーティリティ +├── lambda/ # TypeScript/JavaScript Lambda関数 +├── lambda-python/ # Python Lambda関数 +├── custom-resources/ # カスタムリソース +├── assets/ # 静的アセット +├── parameter.ts # 設定管理(メイン) +└── cdk.json # CDK設定ファイル +``` + +#### ファイル配置ルール + +- **Stack**: `lib/` 直下に配置(例: `lib/genu-stack.ts`) +- **Construct**: `lib/construct/` 配下に配置(例: `lib/construct/auth.ts`) +- **Lambda関数**: 言語別にディレクトリ分け + - TypeScript/JavaScript: `lambda/`(基本はこちらを使用) + - Python: `lambda-python/`(Pythonでしか実装できない機能のみ) +- **カスタムリソース**: `custom-resources/` 配下に機能別フォルダ作成 +- **ユーティリティ**: `lib/utils/` 配下に配置 + +#### Lambda関数の言語選択基準 + +```typescript +// ✅ 良い例 - 基本はTypeScript/JavaScriptを使用 +// lambda/createChat.ts +export const handler = async (event: APIGatewayProxyEvent) => { + // 一般的なAPI処理 +}; + +// ✅ 良い例 - Pythonでしか実装できない機能 +// lambda-python/generic-agent-core-runtime/app.py +# AgentCore固有のPythonライブラリが必要な処理 +import agent_core_specific_library + +// ❌ 悪い例 - TypeScriptで実装可能なのにPythonを使用 +// lambda-python/simple-api/handler.py +# 単純なAPI処理をPythonで実装 +``` + +#### Lambda関数のファイル分割単位 + +```typescript +// ✅ 良い例 - Web API単位で分割 +// lambda/createChat.ts - チャット作成API +// lambda/deleteChat.ts - チャット削除API +// lambda/listChats.ts - チャット一覧取得API + +// ❌ 悪い例 - 機能単位で1つのファイルにまとめる +// lambda/chatHandler.ts - チャット関連の全API処理 +export const createChatHandler = async () => {}; +export const deleteChatHandler = async () => {}; +export const listChatsHandler = async () => {}; + +// ❌ 悪い例 - 過度に細かく分割 +// lambda/validateChatInput.ts - バリデーションのみ +// lambda/saveChatToDb.ts - DB保存のみ +``` + +#### Lambda関数の定義(Construct内) + +**概要**: `NodejsFunction`を使用してConstruct内で定義し、必要最小限の設定のみを行う + +##### 使用するConstruct + +- **TypeScript/JavaScript**: `NodejsFunction` (aws-cdk-lib/aws-lambda-nodejs) - 必須 +- **Python**: 以下のいずれかを使用 + - `PythonFunction` (@aws-cdk/aws-lambda-python-alpha) - 単純なPython関数 + - `DockerImageFunction` (aws-cdk-lib/aws-lambda) - 複雑な依存関係がある場合 +- **禁止**: `Function` (aws-cdk-lib/aws-lambda) - TypeScriptの場合 + +##### 基本設定 + +```typescript +const functionName = new NodejsFunction(this, 'LogicalId', { + runtime: LAMBDA_RUNTIME_NODEJS, // 必須: 定数を使用 + entry: './lambda/functionName.ts', // 必須: ファイルパス + timeout: Duration.minutes(15), // 必須: 15分固定 + environment: { + // 必要な環境変数のみ + // 設定ルール参照 + }, + bundling: { + // 外部モジュールがある場合のみ + nodeModules: ['module-name'], + }, +}); +``` + +##### 環境変数設定ルール + +```typescript +environment: { + // ✅ 必須設定 + TABLE_NAME: props.table.tableName, // DynamoDB使用時 + MODEL_REGION: props.modelRegion, // Bedrock使用時 + + // ✅ 条件付き設定(三項演算子使用) + ...(props.knowledgeBaseId + ? { KNOWLEDGE_BASE_ID: props.knowledgeBaseId } + : {}), + + // ❌ 不要な設定 + AWS_REGION: 'us-east-1', // 自動設定される + NODE_ENV: 'production', // 不要 +} +``` + +##### バンドル設定ルール + +```typescript +bundling: { + // ✅ 必要な外部モジュールのみ指定 + nodeModules: [ + '@aws-sdk/client-bedrock-runtime', // Bedrock使用時 + '@aws-sdk/client-dynamodb', // DynamoDB使用時 + 'cheerio', // HTML解析時 + ], + + // ❌ 不要な設定 + nodeModules: ['aws-sdk'], // v2は使用禁止 + minify: true, // デフォルトで有効 +} +``` + +##### 権限付与パターン + +```typescript +// ✅ 必要最小限の権限のみ +props.table.grantReadData(functionName); // 読み取りのみ +props.table.grantWriteData(functionName); // 書き込みのみ +props.table.grantReadWriteData(functionName); // 読み書き両方 + +// ✅ Bedrock権限(カスタムポリシー) +functionName.addToRolePolicy( + new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['bedrock:InvokeModel'], + resources: [`arn:aws:bedrock:${props.modelRegion}::foundation-model/*`], + }) +); + +// ❌ 過度な権限 +functionName.addToRolePolicy( + new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['*'], + resources: ['*'], + }) +); +``` + +##### 命名規則 + +```typescript +// ✅ 良い例 +const createChatFunction = new NodejsFunction(this, 'CreateChat', {}); +const listChatsFunction = new NodejsFunction(this, 'ListChats', {}); + +// ❌ 悪い例 +const function1 = new NodejsFunction(this, 'Function1', {}); +const chatFunc = new NodejsFunction(this, 'chatFunc', {}); +``` + +### 2. 命名規則 + +**概要**: PascalCaseを使用し、役割が明確に分かる名前を付ける + +#### Construct名 + +```typescript +// ✅ 良い例 +export class ChatConstruct extends Construct {} +export class RagKendraConstruct extends Construct {} + +// ❌ 悪い例 +export class chat extends Construct {} +export class ChatComponent extends Construct {} +``` + +#### Stack名 + +```typescript +// ✅ 良い例 +export class GenUStack extends Stack {} +export class WebSocketApiStack extends Stack {} + +// ❌ 悪い例 +export class stack extends Stack {} +export class MyStack extends Stack {} +``` + +#### リソース名 + +```typescript +// ✅ 良い例 - 自動付番を利用 +const chatTable = new Table(this, 'ChatTable', { + // resourceNameは指定しない +}); +const ragBucket = new Bucket(this, 'RagBucket', { + // bucketNameは指定しない +}); + +// ❌ 悪い例 - 明示的な名前指定 +const table1 = new Table(this, 'Table1', { + tableName: 'my-chat-table', // 名前の衝突リスクあり +}); +const bucket = new Bucket(this, 'bucket', { + bucketName: 'my-rag-bucket', // グローバルで一意である必要がある +}); +``` + +### 3. リソース名管理 + +**概要**: CDKの自動付番機能を活用し、明示的なリソース名指定は避ける + +#### 自動付番の利用 + +```typescript +// ✅ 良い例 - CDKが自動でユニークな名前を生成 +const lambda = new Function(this, 'ProcessorFunction', { + // functionNameは指定しない +}); + +const queue = new Queue(this, 'MessageQueue', { + // queueNameは指定しない +}); + +// ❌ 悪い例 - 明示的な名前指定 +const lambda = new Function(this, 'ProcessorFunction', { + functionName: 'my-processor', // 環境間での衝突リスク +}); +``` + +#### 例外的な名前指定 + +```typescript +// ✅ 良い例 - 外部参照が必要な場合のみ明示的に指定 +const apiGateway = new RestApi(this, 'Api', { + restApiName: `${props.systemName}-api`, // システム名をプレフィックスに使用 +}); +``` + +### 4. Construct設計原則 + +**概要**: 単一責任の原則に従い、必要なプロパティのみを公開する。不必要な分割は避け、汎用性が高い場合のみConstruct化する + +#### 適切なConstruct分割 + +```typescript +// ✅ 良い例 - 可読性向上のための分割 +export class AuthenticationConstruct extends Construct { + // 認証関連のリソースをまとめて可読性を向上 + // 複雑な認証ロジックを分離 +} + +export class DatabaseConstruct extends Construct { + // データベース関連のリソースをまとめて可読性を向上 +} + +// ❌ 悪い例 - 分割する意味がない +export class ChatMessageTableConstruct extends Construct { + // 単純なテーブル1つだけで分割の必要性がない +} +``` + +#### Stack分割の判断基準 + +```typescript +// ✅ 良い例 - 明確な責任分離がある場合 +export class NetworkStack extends Stack {} // ネットワーク基盤 +export class ApplicationStack extends Stack {} // アプリケーション + +// ❌ 悪い例 - 分ける必要がない場合 +export class S3Stack extends Stack {} // S3だけ +export class DynamoDBStack extends Stack {} // DynamoDB だけ +export class LambdaStack extends Stack {} // Lambda だけ +``` + +#### 単一責任の原則 + +```typescript +// ✅ 良い例 - 認証のみを担当 +export class AuthConstruct extends Construct { + public readonly userPool: UserPool; + public readonly userPoolClient: UserPoolClient; +} + +// ❌ 悪い例 - 複数の責任を持つ +export class AppConstruct extends Construct { + // 認証、API、データベースを全て含む +} +``` + +#### プロパティの公開 + +```typescript +// ✅ 良い例 +export class ApiConstruct extends Construct { + public readonly api: RestApi; + public readonly apiUrl: string; + + constructor(scope: Construct, id: string, props: ApiConstructProps) { + super(scope, id); + // 実装 + } +} +``` + +### 5. 設定管理 + +**概要**: `packages/cdk/parameter.ts`で設定を一元管理し、cdk.jsonにも同様の設定を追加する。メインはparameter.tsを使用する + +#### parameter.tsの活用(メイン) + +```typescript +// ✅ 良い例 - parameter.tsから設定を取得 +import { getParameter } from './parameter'; + +const enableRag = getParameter('enableRag'); +const modelRegion = getParameter('modelRegion'); + +// ❌ 悪い例 - ハードコード +const enableRag = true; +const modelRegion = 'us-east-1'; +``` + +#### cdk.jsonとの併用 + +```json +// cdk.jsonにも同様の設定を記載(parameter.tsと整合性を保つ) +{ + "context": { + "enableRag": true, + "ragType": "kendra", + "modelRegion": "us-east-1" + } +} +``` + +#### 設定値の型安全性 + +```typescript +// ✅ 良い例 - 型定義された設定 +interface AppConfig { + enableRag: boolean; + modelRegion: string; + ragType: 'kendra' | 'knowledgeBase'; +} + +// ❌ 悪い例 - any型や型なし +const config: any = getParameter(); +``` + +### 6. エラーハンドリング + +**概要**: 必須パラメータの検証と適切な例外処理を行う + +#### 必須パラメータの検証 + +```typescript +// ✅ 良い例 +constructor(scope: Construct, id: string, props: MyConstructProps) { + super(scope, id); + + if (!props.bucketName) { + throw new Error('bucketName is required'); + } +} +``` + +#### 条件付きリソース作成 + +```typescript +// ✅ 良い例 +if (enableRag) { + new RagConstruct(this, 'Rag', { + // props + }); +} +``` + +### 7. セキュリティ + +**概要**: 最小権限の原則に従い、機密情報は適切に管理する + +#### IAMポリシーの最小権限 + +```typescript +// ✅ 良い例 +lambdaFunction.addToRolePolicy( + new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['s3:GetObject'], + resources: [`${bucket.bucketArn}/*`], + }) +); + +// ❌ 悪い例 +lambdaFunction.addToRolePolicy( + new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['s3:*'], + resources: ['*'], + }) +); +``` + +#### 機密情報の管理 + +```typescript +// ✅ 良い例 +const secret = new Secret(this, 'ApiSecret', { + generateSecretString: { + secretStringTemplate: JSON.stringify({ username: 'admin' }), + generateStringKey: 'password', + }, +}); + +// ❌ 悪い例 +const password = 'hardcoded-password'; +``` + +### 8. パフォーマンス + +**概要**: 不要なリソース作成を避け、効率的なデプロイを実現する + +#### 不要なリソース作成の回避 + +```typescript +// ✅ 良い例 - 条件付きで作成 +const ragConstruct = enableRag ? new RagConstruct(this, 'Rag') : undefined; + +// ❌ 悪い例 - 常に作成してから削除 +const ragConstruct = new RagConstruct(this, 'Rag'); +if (!enableRag) { + ragConstruct.node.tryRemoveChild('Rag'); +} +``` + +### 9. カスタムリソース + +**概要**: 既存のCDK Constructを最優先で使用し、カスタムリソースは最終手段として使用する + +#### カスタムリソース使用基準 + +```typescript +// ✅ 良い例 - 既存のCDK Constructを使用 +const bucket = new Bucket(this, 'MyBucket', { + versioned: true, + encryption: BucketEncryption.S3_MANAGED, +}); + +// ✅ 良い例 - CDKが対応していないリソースの場合のみカスタムリソース +const customResource = new CustomResource(this, 'UnsupportedResource', { + serviceToken: provider.serviceToken, + properties: { + // CDKで対応していないAWSリソースの設定 + }, +}); + +// ❌ 悪い例 - CDKで対応可能なのにカスタムリソースを使用 +const customBucket = new CustomResource(this, 'CustomBucket', { + // S3バケットはCDKで対応済み +}); +``` + +#### カスタムリソースが必要な場合 + +1. **CDKが対応していないAWSリソース** + - 新しいAWSサービスでCDKサポートが追いついていない場合 +2. **CDKが対応していないパラメータ設定** + - 既存リソースの一部パラメータがCDKで未対応の場合 +3. **複雑な初期化処理** + - リソース作成後に特別な設定が必要な場合 + +#### 注意事項 + +- **メンテナンス性の低下**: CDKのアップデートで自動的に恩恵を受けられない +- **可読性の低下**: 生のAWS APIを直接操作するため理解が困難 +- **最終手段**: 他に選択肢がない場合のみ使用する +- **将来の移行**: CDKが対応した際は既存Constructに移行する + +**概要**: リソース間の依存関係を明示的に定義し、正しいデプロイ順序を保証する + +#### 明示的な依存関係 + +```typescript +// ✅ 良い例 +const database = new DatabaseConstruct(this, 'Database'); +const api = new ApiConstruct(this, 'Api', { + table: database.table, +}); + +api.node.addDependency(database); +``` diff --git a/packages/cdk/lambda/CODING_RULES.md b/packages/cdk/lambda/CODING_RULES.md new file mode 100644 index 000000000..fcd5b4e98 --- /dev/null +++ b/packages/cdk/lambda/CODING_RULES.md @@ -0,0 +1,545 @@ +# コーディング規約 - バックエンドAPI + +## Lambda関数 (TypeScript) + +### 1. ファイル構成 + +**概要**: API単位でファイルを分割し、共通処理とデータアクセス層を適切に分離する + +``` +packages/cdk/lambda/ +├── *.ts # API単位でファイル作成(ファイル名はAPIの役割を表現) +│ # 例: createChat.ts, deleteChat.ts, listMessages.ts +├── repository.ts # データアクセス層(メイン) +├── repositoryVideoJob.ts # 特定機能用データアクセス層 +├── useCaseBuilder/ # 機能別ディレクトリ +│ ├── *.ts # 機能内のAPI +│ └── useCaseBuilderRepository.ts # 機能専用データアクセス層 +└── utils/ # 共通ユーティリティ + ├── bedrockApi.ts # Bedrock API呼び出し + ├── bedrockKbApi.ts # Bedrock Knowledge Base API + ├── bedrockAgentApi.ts # Bedrock Agent API + ├── bedrockClient.ts # Bedrockクライアント初期化 + ├── sagemakerApi.ts # SageMaker API呼び出し + ├── models.ts # モデル定義・設定 + ├── auth.ts # 認証処理 + └── api.ts # API共通処理 +``` + +#### ファイル分割ルール + +- **API単位**: 1つのエンドポイントに対して1つのファイル(ファイル名でAPIの役割を表現) +- **大きな機能単位**: メイン機能と独立して開発可能な場合はフォルダ分離 + - 独立性の基準: 独自に開発を進められ、独立して起動可能なレベル + - 例: useCaseBuilder/ - Use Case Builder機能一式 + - utils等の共通機能は引き続き共有する +- **データアクセス層**: repository.tsで一元管理、機能別に必要な場合のみ分離 +- **共通処理**: utils/配下に外部サービス別に分割 + +#### フォルダ分離の判断基準 + +```typescript +// ✅ 良い例 - 独立性が高い機能はフォルダ分離 +useCaseBuilder/ +├── createUseCase.ts +├── listUseCases.ts +└── useCaseBuilderRepository.ts + +// ✅ 良い例 - 関連するAPIでも独立性が低い場合は同一階層 +createChat.ts +deleteChat.ts +listChats.ts + +// ❌ 悪い例 - 過度な分離 +chat/ +├── create.ts # 単純なCRUD操作のみ +├── delete.ts +└── list.ts +``` + +### 2. Lambda関数の基本構造 + +**概要**: APIGatewayProxyEventを受け取り、適切なレスポンスを返す標準的な構造を使用する + +#### ハンドラー関数 + +```typescript +// ✅ 良い例 - 標準的なハンドラー構造 +import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; + +export const handler = async ( + event: APIGatewayProxyEvent +): Promise => { + try { + // ユーザー認証情報の取得 + const userId: string = + event.requestContext.authorizer!.claims['cognito:username']; + + // ビジネスロジック実行 + const result = await businessLogic(userId, event.body); + + // 成功レスポンス + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify(result), + }; + } catch (error) { + console.log(error); + return { + statusCode: 500, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ message: 'Internal Server Error' }), + }; + } +}; + +// ❌ 悪い例 - 型定義なし、エラーハンドリングなし +export const handler = async (event: any) => { + const result = await someFunction(); + return result; +}; +``` + +### 3. 環境変数の使用 + +**概要**: 環境変数は型安全に取得し、必須項目は起動時にチェックする + +```typescript +// ✅ 良い例 - 型安全な環境変数取得 +const TABLE_NAME: string = process.env.TABLE_NAME!; +const MODEL_REGION: string = process.env.MODEL_REGION!; + +// ✅ 良い例 - オプショナルな環境変数 +const KNOWLEDGE_BASE_ID: string | undefined = process.env.KNOWLEDGE_BASE_ID; + +// ❌ 悪い例 - 型指定なし、nullチェックなし +const tableName = process.env.TABLE_NAME; +``` + +### 4. データアクセス層 + +**概要**: repository.tsでデータアクセスを一元管理し、ビジネスロジックと分離する。DynamoDBの効率的なクエリパターンを使用する + +#### DynamoDB操作のベストプラクティス + +```typescript +// ✅ 良い例 - QueryCommandを使用(効率的) +export const getChatsByUserId = async (userId: string): Promise => { + const result = await dynamoDbDocument.send( + new QueryCommand({ + TableName: TABLE_NAME, + KeyConditionExpression: 'id = :userId', + ExpressionAttributeValues: { + ':userId': `user#${userId}`, + }, + }) + ); + return result.Items as Chat[]; +}; + +// ✅ 良い例 - GSI(Global Secondary Index)を使用 +export const getChatsByUseCase = async (usecase: string): Promise => { + const result = await dynamoDbDocument.send( + new QueryCommand({ + TableName: TABLE_NAME, + IndexName: 'UseCaseIndex', // GSIを使用 + KeyConditionExpression: 'usecase = :usecase', + ExpressionAttributeValues: { + ':usecase': usecase, + }, + }) + ); + return result.Items as Chat[]; +}; + +// ✅ 良い例 - BatchGetCommandで複数アイテムを効率的に取得 +export const getMultipleChats = async (chatIds: string[]): Promise => { + const result = await dynamoDbDocument.send( + new BatchGetCommand({ + RequestItems: { + [TABLE_NAME]: { + Keys: chatIds.map((id) => ({ id })), + }, + }, + }) + ); + return (result.Responses?.[TABLE_NAME] as Chat[]) || []; +}; + +// ❌ 悪い例 - ScanCommandを使用(全件走査) +export const getAllChats = async (): Promise => { + const result = await dynamoDbDocument.send( + new ScanCommand({ + TableName: TABLE_NAME, // 全件走査は非効率 + }) + ); + return result.Items as Chat[]; +}; + +// ❌ 悪い例 - N+1クエリ問題 +export const getChatsWithMessages = async (userId: string) => { + const chats = await getChatsByUserId(userId); + + // 各チャットに対して個別にクエリ実行(N+1問題) + for (const chat of chats) { + chat.messages = await getMessagesByChatId(chat.chatId); + } + + return chats; +}; +``` + +#### データアクセスのルール + +- **禁止**: ScanCommand(全件走査)の使用 +- **必須**: QueryCommandまたはGetItemCommandを使用 +- **推奨**: GSI(Global Secondary Index)の活用 +- **N+1クエリ**: 原則禁止(ただし、データ量が少ない場合は許可) +- **バッチ処理**: BatchGetCommand、BatchWriteCommandを活用 +- **分離**: repository.tsでデータアクセスロジックを集約 + +### 5. 外部API呼び出し + +**概要**: utils配下にAPI別のファイルを作成し、エラーハンドリングを適切に行う + +#### Bedrock API呼び出し + +```typescript +// ✅ 良い例 - utils/bedrockApi.tsで外部API呼び出しを集約 +import { + InvokeModelCommand, + ConverseCommand, +} from '@aws-sdk/client-bedrock-runtime'; +import { initBedrockRuntimeClient } from './bedrockClient'; + +export const invokeModel = async (params: InvokeParams): Promise => { + try { + const client = initBedrockRuntimeClient(); + const command = new InvokeModelCommand(params); + const response = await client.send(command); + return response; + } catch (error) { + if (error instanceof ThrottlingException) { + // 適切なエラーハンドリング + throw new Error('Rate limit exceeded'); + } + throw error; + } +}; + +// ❌ 悪い例 - Lambda関数内で直接API呼び出し +export const handler = async (event: APIGatewayProxyEvent) => { + const client = new BedrockRuntimeClient({}); + // 直接API呼び出し +}; +``` + +### 6. エラーハンドリング + +**概要**: try-catchで例外を捕捉し、適切なHTTPステータスコードとエラーメッセージを返す + +```typescript +// ✅ 良い例 - 基本的なエラーハンドリング +export const handler = async ( + event: APIGatewayProxyEvent +): Promise => { + try { + const userId: string = + event.requestContext.authorizer!.claims['cognito:username']; + const result = await businessLogic(userId); + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify(result), + }; + } catch (error) { + console.log(error); + return { + statusCode: 500, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ message: 'Internal Server Error' }), + }; + } +}; + +// ✅ 良い例 - エラー種別による分岐 +try { + const result = await businessLogic(); + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify(result), + }; +} catch (error) { + console.log(error); + + // エラー種別による処理分岐 + if (error instanceof ValidationError) { + return { + statusCode: 400, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ message: 'Invalid input' }), + }; + } + + return { + statusCode: 500, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ message: 'Internal Server Error' }), + }; +} + +// ❌ 悪い例 - エラーハンドリングなし +export const handler = async (event: APIGatewayProxyEvent) => { + const result = await businessLogic(); // エラー時に例外が伝播 + return { statusCode: 200, body: JSON.stringify(result) }; +}; + +// ❌ 悪い例 - 不適切なステータスコード +catch (error) { + return { statusCode: 200, body: JSON.stringify({ error: 'Failed' }) }; +} +``` + +#### レスポンス構造の統一 + +```typescript +// 成功レスポンス +{ + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify(data), +} + +// エラーレスポンス +{ + statusCode: 500, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ message: 'Error message' }), +} +``` + +#### HTTPステータスコード + +**概要**: 一般的なHTTPステータスコードを使用する + +```typescript +// ✅ 良い例 - 一般的なステータスコード +200: // 成功 +400: // バリデーションエラー +401: // 認証エラー +403: // 認可エラー +404: // リソースが見つからない +500: // サーバー内部エラー + +// ❌ 悪い例 - 独自ステータスコード +299: // 独自の成功コード +450: // 独自のエラーコード +``` + +### 7. 型定義 + +**概要**: バックエンドとフロントエンドで共有する型定義は`packages/types/src`内に定義し、APIスキーマは`protocol.d.ts`で管理する + +#### 型定義の配置 + +- **共有型定義**: `packages/types/src/` - バックエンドとフロントエンド間で共有する全ての型 +- **APIスキーマ**: `packages/types/src/protocol.d.ts` - API Request/Response型の専用ファイル +- **エンティティ型**: `packages/types/src/chat.d.ts`、`message.d.ts`等 - ドメインオブジェクトの型定義 +- **ファイル拡張子**: `.d.ts`を使用(型定義専用ファイル) + +#### APIスキーマ定義(protocol.d.ts) + +- **Request型**: APIエンドポイントへの入力パラメータを定義 +- **Response型**: APIエンドポイントからの戻り値を定義 +- **命名規則**: `{機能名}Request`、`{機能名}Response`の形式 +- **用途**: フロントエンドとバックエンドで型安全性を保証 + +#### 共通型の使用 + +- **基本エンティティ**: Chat、Message、SystemContext等の既存型を使用 +- **AWS SDK型**: DynamoDB、Bedrock等のAWS SDK提供型を活用 +- **独自型**: 既存型で表現できない場合のみ最小限で定義 + +#### 型定義のルール + +- **共有型**: `packages/types/src`内で定義し、`generative-ai-use-cases`パッケージ経由でimport +- **APIスキーマ**: `protocol.d.ts`でRequest/Response型を定義 +- **独自型**: 必要最小限に留め、既存型との重複を避ける +- **禁止**: any型の使用、型なしでの実装 + +### 8. ログ出力 + +**概要**: console.logを使用し、適切なログレベルで出力する + +```typescript +// ✅ 良い例 - 適切なログ出力 +console.log('Processing request for user:', userId); +console.error('Error occurred:', error); + +// ✅ 良い例 - 構造化ログ +console.log( + JSON.stringify({ + level: 'info', + message: 'Chat created', + userId, + chatId, + timestamp: new Date().toISOString(), + }) +); + +// ❌ 悪い例 - 機密情報のログ出力 +console.log('User credentials:', event.headers.authorization); +``` + +### 9. 非同期処理 + +**概要**: async/awaitを使用し、Promise.allで並列処理を活用する + +```typescript +// ✅ 良い例 - 並列処理の活用 +const [userData, chatHistory] = await Promise.all([ + getUserData(userId), + getChatHistory(chatId), +]); + +// ✅ 良い例 - 適切なエラーハンドリング付き非同期処理 +try { + const result = await asyncOperation(); + return result; +} catch (error) { + console.error('Async operation failed:', error); + throw error; +} + +// ❌ 悪い例 - 逐次処理 +const userData = await getUserData(userId); +const chatHistory = await getChatHistory(chatId); +``` + +### 10. 認可処理 + +**概要**: userIdを取得してデータアクセスを制御し、ユーザーが自分のデータのみにアクセスできるようにする + +```typescript +// ✅ 良い例 - 適切な認可処理 +export const handler = async ( + event: APIGatewayProxyEvent +): Promise => { + try { + // Cognitoからユーザー情報を取得 + const userId: string = + event.requestContext.authorizer!.claims['cognito:username']; + + // userIdを使ってデータアクセスを制御 + const userChats = await getChatsByUserId(userId); + const userMessages = await getMessagesByUserIdAndChatId(userId, chatId); + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ chats: userChats }), + }; + } catch (error) { + console.log(error); + return { + statusCode: 500, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ message: 'Internal Server Error' }), + }; + } +}; + +// ✅ 良い例 - repository層での認可制御 +export const getChatsByUserId = async (userId: string): Promise => { + const result = await dynamoDbDocument.send( + new QueryCommand({ + TableName: TABLE_NAME, + KeyConditionExpression: 'id = :userId', + ExpressionAttributeValues: { + ':userId': `user#${userId}`, // userIdでフィルタリング + }, + }) + ); + return result.Items as Chat[]; +}; + +// ❌ 悪い例 - 認可制御なし +export const handler = async (event: APIGatewayProxyEvent) => { + // userIdを取得せずに全データを返す + const allChats = await getAllChats(); // 他ユーザーのデータも含む + return { + statusCode: 200, + body: JSON.stringify({ chats: allChats }), + }; +}; + +// ❌ 悪い例 - クライアントから送信されたuserIdを信頼 +export const handler = async (event: APIGatewayProxyEvent) => { + const requestBody = JSON.parse(event.body || '{}'); + const userId = requestBody.userId; // クライアントから送信された値を使用(危険) + + const userChats = await getChatsByUserId(userId); + // 他ユーザーのデータにアクセス可能 +}; +``` + +#### 認可処理のベストプラクティス + +- **必須**: Cognitoの認証情報からuserIdを取得 +- **データアクセス**: 全てのクエリでuserIdによるフィルタリングを実装 +- **禁止**: クライアントから送信されたuserIdの使用 +- **原則**: ユーザーは自分のデータのみにアクセス可能 + +**概要**: 不要な処理を避け、効率的な実装を心がける + +```typescript +// ✅ 良い例 - 早期リターン +if (!userId) { + return errorResponse(400, 'User ID is required'); +} + +// ✅ 良い例 - 必要な場合のみ重い処理を実行 +if (shouldProcessLargeData) { + const result = await heavyProcessing(); +} + +// ❌ 悪い例 - 不要な処理 +const allData = await getAllData(); // 大量データ取得 +const filteredData = allData.filter((item) => item.userId === userId); +``` diff --git a/packages/web/CODING_RULES.md b/packages/web/CODING_RULES.md new file mode 100644 index 000000000..1feb5e339 --- /dev/null +++ b/packages/web/CODING_RULES.md @@ -0,0 +1,418 @@ +# コーディング規約 - フロントエンド + +## React + TypeScript (packages/web) + +### 1. ファイル構成 + +**概要**: 機能別にディレクトリを分け、コンポーネント、フック、ページを適切に分離する + +``` +packages/web/ +├── public/ # 静的ファイル(CloudFrontから直接配信) +│ ├── locales/ # 国際化リソースファイル(JSON) +│ └── images/ # 画像・アイコンファイル +├── src/ +│ ├── components/ # 再利用可能なUIコンポーネント +│ │ # - 汎用コンポーネント: 直下に配置 +│ │ # - 機能固有コンポーネント: サブフォルダに配置 +│ ├── hooks/ # カスタムフック(ビジネスロジック) +│ │ # - 汎用フック: 直下に配置 +│ │ # - 機能固有フック: サブフォルダに配置 +│ ├── pages/ # ページコンポーネント(ルーティング対象) +│ ├── utils/ # ユーティリティ関数・ヘルパー +│ ├── prompts/ # AI用プロンプトテンプレート +│ │ └── diagrams/ # 図表生成用プロンプト +│ ├── i18n/ # 国際化設定・ユーティリティ +│ │ └── utils/ # 国際化ヘルパー関数 +│ ├── assets/ # 静的アセット(SVG、画像等) +│ └── @types/ # フロントエンド固有型定義 +├── tests/ # テストファイル +└── 設定ファイル群 # vite.config.ts, tailwind.config.ts等 +``` + +#### ディレクトリの役割 + +- **public/**: CloudFrontから直接配信される静的ファイル +- **components/**: 汎用コンポーネントは直下、機能固有コンポーネントはサブフォルダに配置 +- **hooks/**: 汎用フックは直下、機能固有フックはサブフォルダに配置 +- **pages/**: ルーティング対象のページコンポーネント +- **utils/**: 汎用的なユーティリティ関数 +- **prompts/**: AI用プロンプトテンプレート、機能別分類 +- **i18n/**: 国際化設定とヘルパー関数 +- **assets/**: バンドルされる静的アセット(SVG等) +- **@types/**: フロントエンド固有の型定義ファイル + +### 2. コンポーネント設計 + +**概要**: 関数コンポーネントを使用し、Props型を明確に定義する。適切な分割単位でコンポーネントを作成する + +#### コンポーネント分割単位 + +```typescript +// ✅ 良い例 - 再利用可能なコンポーネント(components/直下) +const Button: React.FC = ({ children, onClick, disabled }) => { + return ( + + ); +}; + +// ✅ 良い例 - 可読性向上のための分割(機能固有、サブフォルダ内) +// components/ChatPage/MessageInput.tsx +const MessageInput: React.FC = ({ onSend }) => { + // チャット専用の入力コンポーネント + // 他機能では再利用しないが、ChatPageの可読性向上のため分離 +}; + +// ❌ 悪い例 - 無理な再利用を意識した過度な分割 +const ButtonText: React.FC = ({ children }) => {children}; +const ButtonWrapper: React.FC = ({ children }) =>
{children}
; +``` + +#### 分割ルール + +- **再利用可能**: 複数箇所で使用するコンポーネントは`components/`直下に配置 +- **可読性向上**: 単一機能でのみ使用するが、処理を切り出したいコンポーネントはサブフォルダに配置 +- **過度な分割禁止**: 無理に再利用を意識した細かすぎる分割は避ける +- **配置ルール**: 再利用できないコンポーネントは機能別サブフォルダ内に格納 + +#### 命名規則 + +```typescript +// ✅ 良い例 - 大項目+中項目+小項目の階層命名(ソート考慮) +ButtonSend.tsx; // Button系の送信ボタン +ButtonCopy.tsx; // Button系のコピーボタン +ButtonToggle.tsx; // Button系のトグルボタン +InputText.tsx; // Input系のテキスト入力 +InputChatContent.tsx; // Input系のチャット入力 +ModalDialog.tsx; // Modal系のダイアログ +ModalSystemContext.tsx; // Modal系のシステムコンテキスト + +// ❌ 悪い例 - 階層を意識しない命名 +Send.tsx; // 何の送信か不明 +CopyButton.tsx; // ソート時にButtonと離れる +TextInput.tsx; // ソート時にInputと離れる +``` + +#### 基本構造 + +```typescript +// ✅ 良い例 - 標準的なコンポーネント構造 +import React from 'react'; +import { BaseProps } from '../@types/common'; + +type Props = BaseProps & { + title?: string; + disabled?: boolean; + onClick: () => void; + children: React.ReactNode; +}; + +const Button: React.FC = (props) => { + return ( + + ); +}; + +export default Button; + +// ❌ 悪い例 - 型定義なし +const Button = (props: any) => { + return ; +}; +``` + +#### Props設計 + +- **BaseProps**: 共通プロパティ(className等)を継承 +- **Optional vs Required**: 必須項目は`?`なしで定義 +- **children**: React.ReactNodeを使用 +- **イベントハンドラー**: 明確な関数型を定義 + +### 3. カスタムフック + +**概要**: ビジネスロジックをカスタムフックに分離し、コンポーネントから状態管理を切り離す。APIアクセスは必ずAPIフックを経由する + +#### APIアクセスの分離 + +- **APIフック**: `use{機能名}Api.ts`でサーバーとの通信を担当 +- **ビジネスロジックフック**: APIフックを経由してビジネスロジックを実装 +- **結合度の低減**: 直接fetch呼び出しを禁止し、APIフック経由でアクセス + +#### フック分類 + +- **API呼び出し**: `use{機能名}Api.ts` - サーバーとの通信(必須経由ポイント) +- **状態管理**: `use{機能名}.ts` - アプリケーション状態・ビジネスロジック +- **ユーティリティ**: `use{機能名}.ts` - 汎用的なロジック + +#### フックとユーティリティの使い分け + +- **Hooks**: 状態管理を含む共通処理(useState、useEffect等を使用) +- **Utils**: 状態管理しない純粋な関数(`utils/`配下に配置) +- **判断基準**: React Hooksを使用する場合はカスタムフック、使用しない場合はユーティリティ関数 + +#### APIアクセスのルール + +- **必須**: 全てのAPIアクセスはAPIフック(`use*Api.ts`)を経由 +- **目的**: バックエンドAPIとフロントエンドの結合度を弱める +- **禁止**: ビジネスロジックフック内での直接fetch呼び出し + +### 4. 状態管理 + +**概要**: 状態のスコープに応じて適切な管理方法を選択し、ライフサイクルを考慮した設計を行う + +#### 状態管理のスコープ + +- **コンポーネントレベル**: コンポーネント制御用の状態をuseStateで管理(コンポーネントのライフサイクルに依存) +- **Hooksレベル**: フック機能提供用の状態をuseStateで管理(フックのライフサイクルに依存) +- **アプリレベル**: アプリ全体で一貫した状態をZustandで管理(画面更新で初期化) +- **永続レベル**: ブラウザで永続保持する状態をLocalStorageで管理(UI設定等) + +#### Zustand使用パターン + +```typescript +// ✅ 良い例 - Zustandでグローバル状態管理 +import { create } from 'zustand'; +import { produce } from 'immer'; + +type ChatState = { + messages: Message[]; + loading: boolean; + addMessage: (message: Message) => void; + setLoading: (loading: boolean) => void; +}; + +const useChatState = create((set) => ({ + messages: [], + loading: false, + addMessage: (message) => + set( + produce((state) => { + state.messages.push(message); + }) + ), + setLoading: (loading) => set({ loading }), +})); + +// ✅ 良い例 - ローカル状態はuseState +const [inputValue, setInputValue] = useState(''); +const [isOpen, setIsOpen] = useState(false); +``` + +#### 状態管理のルール + +- **コンポーネント状態**: useStateでローカル管理 +- **グローバル状態**: Zustand + Immerで不変性を保持 +- **永続状態**: LocalStorageでUI設定を保存 +- **サーバー状態**: SWRまたはTanStack Queryを使用 +- **フォーム状態**: React Hook Formを推奨 + +### 5. 型定義 + +**概要**: バックエンドと共有する型定義は`packages/types/src`を使用し、APIスキーマは`protocol.d.ts`で管理する + +#### 型定義の使用 + +- **共有型定義**: `packages/types/src/` - バックエンドとフロントエンド間で共有する全ての型 +- **APIスキーマ**: `packages/types/src/protocol.d.ts` - API Request/Response型の専用ファイル +- **エンティティ型**: `packages/types/src/chat.d.ts`、`message.d.ts`等 - ドメインオブジェクトの型定義 +- **フロントエンド固有型**: `@types/` - フロントエンドの処理でのみ使用し、フック・コンポーネント間で共有する型 + +#### フロントエンド固有型の用途 + +- **ナビゲーション**: ルーティングパラメータ、クエリパラメータの型定義 +- **UI状態**: コンポーネント間で共有するUI状態の型定義 +- **フォーム**: フォーム入力値、バリデーション結果の型定義 +- **表示制御**: 表示モード、フィルタ条件等のフロントエンド専用ロジックの型定義 +- **イベント**: カスタムイベント、コールバック関数の型定義 + +#### 型使用パターン + +```typescript +// ✅ 良い例 - 共有型の使用 +import { Chat, Message, SystemContext } from 'generative-ai-use-cases'; +import { + CreateChatResponse, + UpdateFeedbackRequest, +} from 'generative-ai-use-cases'; + +// ✅ 良い例 - フロントエンド固有型 +// @types/navigate.d.ts +export interface ChatPageQueryParams { + chatId?: string; + systemContextId?: string; +} + +// ✅ 良い例 - コンポーネントProps型 +type ButtonProps = BaseProps & { + variant?: 'primary' | 'secondary'; + size?: 'sm' | 'md' | 'lg'; +}; +``` + +#### 型定義のルール + +- **APIスキーマ**: `protocol.d.ts`でRequest/Response型を定義 +- **共有型**: `generative-ai-use-cases`パッケージ経由でimport +- **独自型**: 必要最小限に留め、既存型との重複を避ける +- **禁止**: any型の使用、型なしでの実装 + +### 6. スタイリング + +**概要**: TailwindCSSを使用し、テーマカラーを活用して一貫性のあるデザインを構築する + +#### TailwindCSS使用ルール + +```typescript +// ✅ 良い例 - テーマカラー使用 + + +// ✅ 良い例 - テーマカラーでの条件付きスタイル + + +// ✅ 良い例 - その他のテーマカラー +
+ Content +
+ +// ❌ 悪い例 - 直接色指定 + + +// ❌ 悪い例 - インラインスタイル + +``` + +#### スタイリングのルール + +- **必須**: TailwindCSSクラスを使用 +- **カラー**: テーマカラーを用途別に使い分け + - `aws-smile`: アクション可能な要素(ボタン、リンク等) + - `aws-sky`: 選択状態・強調表現 + - `aws-font-color`: 文字色 + - `aws-squid-ink`: メインテーマ・背景色 +- **禁止**: インラインスタイル、直接的な色指定(blue-500等) +- **推奨**: 透明度調整(/90、/20等)でバリエーション作成 + +### 7. 国際化(i18n) + +**概要**: react-i18nextを使用して多言語対応を実装する。英語・日本語は必須対応、他言語はオプショナル + +#### 対応言語 + +- **必須**: 英語(en)、日本語(ja) +- **オプショナル**: その他の言語(韓国語等) +- **デフォルト**: 英語をフォールバック言語として設定 + +```typescript +// ✅ 良い例 - i18n使用 +import { useTranslation } from 'react-i18next'; + +const Component = () => { + const { t } = useTranslation(); + + return ( +
+

{t('chat.title')}

+

{t('chat.description')}

+
+ ); +}; + +// ❌ 悪い例 - ハードコードされたテキスト +const Component = () => { + return ( +
+

Chat

+

Start a conversation

+
+ ); +}; +``` + +#### 国際化のルール + +- **必須対応**: 全てのユーザー向けテキストを英語・日本語で提供 +- **リソース配置**: `public/locales/{言語コード}/` 配下にJSONファイル +- **禁止**: UI上でのハードコードされたテキスト +- **推奨**: 開発者向けメッセージ(console.log等)は英語のみでも可 + +### 8. エラーハンドリング + +**概要**: ErrorBoundaryとtry-catchを適切に使い分ける + +```typescript +// ✅ 良い例 - API呼び出しのエラーハンドリング +const useChatApi = () => { + const [error, setError] = useState(null); + + const createChat = useCallback(async () => { + try { + setError(null); + const response = await fetch('/api/chat', { method: 'POST' }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.json(); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error'; + setError(errorMessage); + throw err; + } + }, []); + + return { createChat, error }; +}; + +// ✅ 良い例 - ErrorBoundary使用 + + + +``` + +### 9. パフォーマンス最適化 + +**概要**: React.memo、useCallback、useMemoを適切に使用する + +```typescript +// ✅ 良い例 - React.memoでコンポーネント最適化 +const ChatMessage = React.memo(({ message, onEdit }) => { + return
{message.content}
; +}); + +// ✅ 良い例 - useCallbackでコールバック最適化 +const handleSubmit = useCallback((message: string) => { + sendMessage(message); +}, [sendMessage]); + +// ✅ 良い例 - useMemoで計算結果キャッシュ +const filteredMessages = useMemo(() => { + return messages.filter(msg => msg.role === 'user'); +}, [messages]); +```