Phase 2-1: グループ管理Mutationの実装
目的
グループの作成、参加、検索機能を実装します(createGroup, joinGroup, listGroupsByDomain)。Domain概念に対応し、グローバルIPベースのスコープ管理を実現します。
タスク
listGroupsByDomain Queryリゾルバー(スキャン機能)
- DynamoDB DataSourceの定義
- リクエストハンドラー (JS) の実装
- Domain自動取得:
ctx.identity.sourceIpまたはctx.args.domain - DynamoDB Query:
PK = DOMAIN#{domain},SK begins_with GROUP#
- Domain自動取得:
- レスポンスハンドラーの実装
- Group型の配列を返却
- Resolverの登録
createGroup Mutationリゾルバー
- リクエストハンドラー (JS) の実装
- Domain自動取得:
ctx.args.domainまたはctx.identity.sourceIp - グループID生成:
util.autoId() - fullId生成:
{group_id}@{domain} - Groupメタデータの作成
PK: DOMAIN#{domain},SK: GROUP#{group_id}#METADATA- 属性:
id,domain,fullId,name,hostId,createdAt
- Domain自動取得:
- レスポンスハンドラーの実装
- Group型を返却
- Resolverの登録
joinGroup Mutationリゾルバー
- リクエストハンドラー (JS) の実装
- Nodeの追加
PK: DOMAIN#{domain},SK: GROUP#{group_id}#NODE#{node_id}
- Node所属情報の作成
PK: NODE#{node_id},SK: METADATA- 属性:
nodeId,groupId,domain
- TransactWriteItemsでアトミックに実行
- Nodeの追加
- レスポンスハンドラーの実装
- Node型を返却
- Resolverの登録
テスト
- listGroupsByDomain動作確認
- Domain指定あり
- Domain指定なし(グローバルIP自動取得)
- createGroup動作確認
- fullIdの正しい生成
- joinGroup動作確認
- エラーハンドリング確認
成果物
- JSリゾルバーファイル
js/resolvers/Query.listGroupsByDomain.req.jsjs/resolvers/Query.listGroupsByDomain.res.jsjs/resolvers/Mutation.createGroup.req.jsjs/resolvers/Mutation.createGroup.res.jsjs/resolvers/Mutation.joinGroup.req.jsjs/resolvers/Mutation.joinGroup.res.js
- CDK Resolver定義コード
リゾルバー実装例
listGroupsByDomain リクエスト
// js/resolvers/Query.listGroupsByDomain.req.js function request(ctx) { // Domain決定: 引数 > ソースIP const sourceIp = ctx.identity.sourceIp[0]; const domain = ctx.args.domain || sourceIp; return { operation: 'Query', query: { expression: 'pk = :pk AND begins_with(sk, :sk_prefix)', expressionValues: { ':pk': { S: `DOMAIN#${domain}` }, ':sk_prefix': { S: 'GROUP#' } } } }; }
listGroupsByDomain レスポンス
// js/resolvers/Query.listGroupsByDomain.res.js function response(ctx) { if (ctx.error) { util.error(ctx.error.message, ctx.error.type); } // DynamoDBアイテムをGroup型に変換 return ctx.result.items .filter(item => item.sk.S.endsWith('#METADATA')) .map(item => ({ id: item.id.S, domain: item.domain.S, fullId: item.fullId.S, name: item.name.S, hostId: item.hostId.S, createdAt: item.createdAt.S })); }
createGroup リクエスト
// js/resolvers/Mutation.createGroup.req.js function request(ctx) { const { name, hostId, domain } = ctx.args; // Domain決定: 引数 > ソースIP const sourceIp = ctx.identity.sourceIp[0]; const actualDomain = domain || sourceIp; // Domain文字列のバリデーション(最大256文字) if (actualDomain.length > 256) { util.error('Domain must be 256 characters or less', 'ValidationError'); } // グループID生成 const groupId = util.autoId(); const fullId = `${groupId}@${actualDomain}`; const now = util.time.nowISO8601(); return { operation: 'PutItem', key: { pk: { S: `DOMAIN#${actualDomain}` }, sk: { S: `GROUP#${groupId}#METADATA` } }, attributeValues: { id: { S: groupId }, domain: { S: actualDomain }, fullId: { S: fullId }, name: { S: name }, hostId: { S: hostId }, createdAt: { S: now }, // GSI用 gsi_pk: { S: `GROUP#${groupId}` }, gsi_sk: { S: `DOMAIN#${actualDomain}` } } }; }
createGroup レスポンス
// js/resolvers/Mutation.createGroup.res.js function response(ctx) { if (ctx.error) { util.error(ctx.error.message, ctx.error.type); } // 入力引数とソースIPからGroup型を構築 const sourceIp = ctx.identity.sourceIp[0]; const domain = ctx.args.domain || sourceIp; const groupId = ctx.result.Attributes.id.S; return { id: groupId, domain: domain, fullId: `${groupId}@${domain}`, name: ctx.args.name, hostId: ctx.args.hostId, createdAt: ctx.result.Attributes.createdAt.S }; }
joinGroup リクエスト(TransactWriteItems)
// js/resolvers/Mutation.joinGroup.req.js function request(ctx) { const { groupId, domain, nodeId } = ctx.args; const now = util.time.nowISO8601(); return { operation: 'TransactWriteItems', transactItems: [ // Node情報の追加 { table: 'MeshV2Table', operation: 'PutItem', key: { pk: { S: `DOMAIN#${domain}` }, sk: { S: `GROUP#${groupId}#NODE#${nodeId}` } }, attributeValues: { nodeId: { S: nodeId }, groupId: { S: groupId }, domain: { S: domain }, name: { S: `Node ${nodeId}` }, data: { L: [] }, timestamp: { S: now } } }, // Node所属情報の作成 { table: 'MeshV2Table', operation: 'PutItem', key: { pk: { S: `NODE#${nodeId}` }, sk: { S: 'METADATA' } }, attributeValues: { nodeId: { S: nodeId }, groupId: { S: groupId }, domain: { S: domain } } } ] }; }
関連
- EPIC Issue: EPIC: Mesh v2 拡張機能の実装 #444
- Phase: 2 (バックエンドロジック)
- 依存: Phase 1-3: AppSync GraphQL APIとスキーマの実装 #448 (Phase 1-3)
- 更新: Domain概念の導入、listGroupsByDomain追加(Phase 0レビューフィードバック)
🤖 Generated with Claude Code
Co-Authored-By: Claude noreply@anthropic.com