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#
  • レスポンスハンドラーの実装
    • 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
  • レスポンスハンドラーの実装
    • 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型を返却
  • Resolverの登録

テスト

  • listGroupsByDomain動作確認
    • Domain指定あり
    • Domain指定なし(グローバルIP自動取得)
  • createGroup動作確認
    • fullIdの正しい生成
  • joinGroup動作確認
  • エラーハンドリング確認

成果物

  • JSリゾルバーファイル
    • js/resolvers/Query.listGroupsByDomain.req.js
    • js/resolvers/Query.listGroupsByDomain.res.js
    • js/resolvers/Mutation.createGroup.req.js
    • js/resolvers/Mutation.createGroup.res.js
    • js/resolvers/Mutation.joinGroup.req.js
    • js/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 }
        }
      }
    ]
  };
}

関連

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com