Skip to content

Phase 2-1: グループ管理Mutationの実装 #449

@takaokouji

Description

@takaokouji

目的

グループの作成、参加、検索機能を実装します(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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions