mcpField
D
data-model-changes

data-model-changes

github.com/inkeepデータ・データベース1.5k49 回インストール4 日前に更新
リポジトリ

data-model-changes について

Inkeep Agent Framework のデータモデル変更を扱う開発者向けガイドです。スキーマ、検証、型、データアクセス、マイグレーションに加え、設定データと実行時データの区別や Dolt ブランチの検証方法を説明します。

主な機能

  1. 01Manage と runtime データベースの説明
  2. 02スキーマ、リレーション、検証のパターン
  3. 03エンティティ型とデータアクセスの手順
  4. 04マイグレーション生成とブランチ検証
  5. 05Changeset とマイグレーションの安全規則

活用シーン

  1. 01テーブルやカラムの追加
  2. 02リレーションとデータアクセスの拡張
  3. 03データベーススキーマのマイグレーション
  4. 04ブランチ間の manage マイグレーション検証

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/inkeep/agents --skill data-model-changes

インストール後、Claude Code で/data-model-changesを入力して読み込まれたか確認します。

詳細

データモデル変更ガイド

Inkeep Agent Framework のデータモデル(データベーススキーマ、バリデーション、型、データアクセスレイヤー)を変更するための包括的なガイドです。


データベースアーキテクチャ

このフレームワークでは、2つの独立した PostgreSQL データベースを使用します。

データベース設定ファイルスキーマファイル用途
Manage(Doltgres)drizzle.manage.config.tssrc/db/manage/manage-schema.tsバージョン管理される設定:プロジェクト、エージェント、ツール、トリガー、評価器
Runtime(Postgres)drizzle.run.config.tssrc/db/runtime/runtime-schema.tsトランザクションデータ:会話、メッセージ、タスク、API キー

主な違い:

  • Manage DB:変更頻度の低い設定(エージェント定義、ツール設定)です。Dolt のバージョン管理をサポートします。
  • Runtime DB:高頻度のトランザクションデータ(会話、メッセージ)です。manage テーブルとのデータベース間外部キーはありません。

スキーマパターン

1. スコープパターン(マルチテナンシー)

すべてのテーブルで階層的なスコープを使用します。再利用可能な次のフィールドパターンを使用してください。

// Tenant-level (org-wide resources)
const tenantScoped = {
  tenantId: varchar('tenant_id', { length: 256 }).notNull(),
  id: varchar('id', { length: 256 }).notNull(),
};

// Project-level (project-specific resources)
const projectScoped = {
  ...tenantScoped,
  projectId: varchar('project_id', { length: 256 }).notNull(),
};

// Agent-level (agent-specific resources)
const agentScoped = {
  ...projectScoped,
  agentId: varchar('agent_id', { length: 256 }).notNull(),
};

// Sub-agent level (sub-agent-specific resources)
const subAgentScoped = {
  ...agentScoped,
  subAgentId: varchar('sub_agent_id', { length: 256 }).notNull(),
};

実際のテーブルでの使用例:

// Project-scoped: tools belong to a project
export const tools = pgTable(
  'tools',
  {
    ...projectScoped,  // tenantId, id, projectId
    name: varchar('name', { length: 256 }).notNull(),
    config: jsonb('config').$type<ToolConfig>().notNull(),
    ...timestamps,
  },
  (table) => [
    primaryKey({ columns: [table.tenantId, table.projectId, table.id] }),
  ]
);

// Agent-scoped: triggers belong to an agent
export const triggers = pgTable(
  'triggers',
  {
    ...agentScoped,  // tenantId, id, projectId, agentId
    ...uiProperties,
    enabled: boolean('enabled').notNull().default(true),
    ...timestamps,
  },
  (table) => [
    primaryKey({ columns: [table.tenantId, table.projectId, table.agentId, table.id] }),
  ]
);

// Sub-agent scoped: tool relations belong to a sub-agent
export const subAgentToolRelations = pgTable(
  'sub_agent_tool_relations',
  {
    ...subAgentScoped,  // tenantId, id, projectId, agentId, subAgentId
    toolId: varchar('tool_id', { length: 256 }).notNull(),
    ...timestamps,
  },
  (table) => [
    primaryKey({ columns: [table.tenantId, table.projectId, table.agentId, table.id] }),
  ]
);
2. 共通フィールドパターン
// Standard UI properties
const uiProperties = {
  name: varchar('name', { length: 256 }).notNull(),
  description: text('description'),
};

// Standard timestamps
const timestamps = {
  createdAt: timestamp('created_at', { mode: 'string' }).notNull().defaultNow(),
  updatedAt: timestamp('updated_at', { mode: 'string' }).notNull().defaultNow(),
};

使用例:

// Table with UI properties (user-facing entity)
export const projects = pgTable(
  'projects',
  {
    ...tenantScoped,
    ...uiProperties,  // name (required), description (optional)
    models: jsonb('models').$type<ProjectModels>(),
    ...timestamps,    // createdAt, updatedAt
  },
  (table) => [primaryKey({ columns: [table.tenantId, table.id] })]
);

// Table without UI properties (internal/join table)
export const subAgentRelations = pgTable(
  'sub_agent_relations',
  {
    ...agentScoped,
    sourceSubAgentId: varchar('source_sub_agent_id', { length: 256 }).notNull(),
    targetSubAgentId: varchar('target_sub_agent_id', { length: 256 }),
    relationType: varchar('relation_type', { length: 256 }),
    ...timestamps,  // Still include timestamps for auditing
  },
  (table) => [
    primaryKey({ columns: [table.tenantId, table.projectId, table.agentId, table.id] }),
  ]
);
3. JSONB 型注釈

型安全性を確保するため、JSONB カラムには必ず .$type<T>() を指定してください。

models: jsonb('models').$type<Models>(),
config: jsonb('config').$type<{ type: 'mcp'; mcp: ToolMcpConfig }>().notNull(),
metadata: jsonb('metadata').$type<ConversationMetadata>(),

新しいテーブルの追加

ステップ 1:スキーマでテーブルを定義する

場所:packages/agents-core/src/db/manage/manage-schema.ts(設定)または runtime-schema.ts(ランタイム)

export const myNewTable = pgTable(
  'my_new_table',
  {
    ...projectScoped,  // Choose appropriate scope
    ...uiProperties,   // If it has name/description
    
    // Custom fields
    status: varchar('status', { length: 50 }).notNull().default('active'),
    config: jsonb('config').$type<MyConfigType>(),
    
    // Reference fields (optional)
    parentId: varchar('parent_id', { length: 256 }),
    
    ...timestamps,
  },
  (table) => [
    // Primary key - ALWAYS include all scope fields
    primaryKey({ columns: [table.tenantId, table.projectId, table.id] }),
    
    // Foreign keys (only within same database!)
    foreignKey({
      columns: [table.tenantId, table.projectId],
      foreignColumns: [projects.tenantId, projects.id],
      name: 'my_new_table_project_fk',
    }).onDelete('cascade'),
    
    // Optional: indexes for frequent queries
    index('my_new_table_status_idx').on(table.status),
  ]
);
ステップ 2:リレーションを追加する(必要な場合)
export const myNewTableRelations = relations(myNewTable, ({ one, many }) => ({
  project: one(projects, {
    fields: [myNewTable.tenantId, myNewTable.projectId],
    references: [projects.tenantId, projects.id],
  }),
  // Add more relations as needed
}));
ステップ 3:Zod バリデーションスキーマを作成する

場所:packages/agents-core/src/validation/schemas.ts

// Create base schemas from Drizzle table
export const MyNewTableSelectSchema = registerFieldSchemas(
  createSelectSchema(myNewTable)
).openapi('MyNewTable');

export const MyNewTableInsertSchema = registerFieldSchemas(
  createInsertSchema(myNewTable)
).openapi('MyNewTableInsert');

export const MyNewTableUpdateSchema = MyNewTableInsertSchema.partial()
  .omit({ tenantId: true, projectId: true, id: true, createdAt: true })
  .openapi('MyNewTableUpdate');

// API schemas (omit internal scope fields)
export const MyNewTableApiSelectSchema = createApiSchema(MyNewTableSelectSchema)
  .openapi('MyNewTableApiSelect');

export const MyNewTableApiInsertSchema = createApiInsertSchema(MyNewTableInsertSchema)
  .openapi('MyNewTableApiInsert');

export const MyNewTableApiUpdateSchema = createApiUpdateSchema(MyNewTableUpdateSchema)
  .openapi('MyNewTableApiUpdate');
ステップ 4:エンティティ型を作成する

場所:packages/agents-core/src/types/entities.ts

export type MyNewTableSelect = z.infer<typeof MyNewTableSelectSchema>;
export type MyNewTableInsert = z.infer<typeof MyNewTableInsertSchema>;
export type MyNewTableUpdate = z.infer<typeof MyNewTableUpdateSchema>;
export type MyNewTableApiSelect = z.infer<typeof MyNewTableApiSelectSchema>;
export type MyNewTableApiInsert = z.infer<typeof MyNewTableApiInsertSchema>;
export type MyNewTableApiUpdate = z.infer<typeof MyNewTableApiUpdateSchema>;
ステップ 5:データアクセス関数を作成する

場所:packages/agents-core/src/data-access/manage/myNewTable.ts(または runtime/)

import { and, eq, desc, count } from 'drizzle-orm';
import type { AgentsManageDatabaseClient } from '../../db/manage/manage-client';
import { myNewTable } from '../../db/manage/manage-schema';
import type { MyNewTableInsert, MyNewTableSelect, MyNewTableUpdate } from '../../types/entities';
import type { ProjectScopeConfig, PaginationConfig } from '../../types/utility';

export const getMyNewTableById =
  (db: AgentsManageDatabaseClient) =>
  async (params: {
    scopes: ProjectScopeConfig;
    itemId: string;
  }): Promise<MyNewTableSelect | undefined> => {
    const { scopes, itemId } = params;
    return db.query.myNewTable.findFirst({
      where: and(
        eq(myNewTable.tenantId, scopes.tenantId),
        eq(myNewTable.projectId, scopes.projectId),
        eq(myNewTable.id, itemId)
      ),
    });
  };

export const listMyNewTable =
  (db: AgentsManageDatabaseClient) =>
  async (params: { scopes: ProjectScopeConfig }): Promise<MyNewTableSelect[]> => {
    return db.query.myNewTable.findMany({
      where: and(
        eq(myNewTable.tenantId, params.scopes.tenantId),
        eq(myNewTable.projectId, params.scopes.projectId)
      ),
    });
  };

export const createMyNewTable =
  (db: AgentsManageDatabaseClient) =>
  async (params: MyNewTableInsert): Promise<MyNewTableSelect> => {
    const result = await db.insert(myNewTable).values(params as any).returning();
    return result[0];
  };

export const updateMyNewTable =
  (db: AgentsManageDatabaseClient) =>
  async (params: {
    scopes: ProjectScopeConfig;
    itemId: string;
    data: MyNewTableUpdate;
  }): Promise<MyNewTableSelect> => {
    const result = await db
      .update(myNewTable)
      .set({ ...params.data, updatedAt: new Date().toISOString() } as any)
      .where(
        and(
          eq(myNewTable.tenantId, params.scopes.tenantId),
          eq(myNewTable.projectId, params.scopes.projectId),
          eq(myNewTable.id, params.itemId)
        )
      )
      .returning();
    return result[0];
  };

export const deleteMyNewTable =
  (db: AgentsManageDatabaseClient) =>
  async (params: { scopes: ProjectScopeConfig; itemId: string }): Promise<void> => {
    await db.delete(myNewTable).where(
      and(
        eq(myNewTable.tenantId, params.scopes.tenantId),
        eq(myNewTable.projectId, params.scopes.projectId),
        eq(myNewTable.id, params.itemId)
      )
    );
  };
ステップ 6:データアクセスのインデックスからエクスポートする

場所:packages/agents-core/src/data-access/index.ts

export * from './manage/myNewTable';
ステップ 7:マイグレーションを生成して適用する
# Generate migration SQL
pnpm db:generate

# Review generated SQL in drizzle/manage/ or drizzle/runtime/
# Make minor edits if needed (ONLY to newly generated files)

# Apply migration
pnpm db:migrate
ステップ 8:Dolt ブランチ間で manage のマイグレーションを検証する

変更が manage スキーマに関わる場合、main でマイグレーションが成功しただけで終わらせないでください。実データを含む既存のプロジェクトブランチに、main スキーマブランチからスキーマ変更をマージできることを検証してください。 Doltgres はまだベータ版であり、有効な postgres の動作と doltgres の動作には差異がある場合があります。こうした差異により、プロジェクトブランチへのマージ時にマイグレーションが失敗することがあります。

推奨される検証フロー:

# 1. Ensure your local Dolt DB already has representative test data
#    in main and in one or more non-main branches

# 2. Apply the migration on main
pnpm db:manage:migrate

# 3. Push the schema change from main to all non-main branches
pnpm --filter @inkeep/agents-core db:manage:sync-all-branches

確認する内容:

  • SYNCED は、そのブランチのスキーマが問題なく更新されたことを意味します
  • NOOP は、そのブランチがすでに最新であることを意味します
  • 失敗があった場合、そのマイグレーションにはブランチのマージに関する問題がある可能性が高いため、リリース前に修正する必要があります

次の問題を検出できます。

  • 制約、インデックス、またはテーブルの再書き込みに関する Dolt 固有のマージ失敗

既存のテーブルへのカラムの追加

ステップ 1:スキーマを変更する

新しいカラムをテーブル定義に追加します。

// In manage-schema.ts or runtime-schema.ts
export const existingTable = pgTable(
  'existing_table',
  {
    // ... existing fields ...
    
    // New column
    newField: varchar('new_field', { length: 256 }),
    newJsonField: jsonb('new_json_field').$type<MyNewType>().default(null),
    
    ...timestamps,
  },
  // ... constraints ...
);
ステップ 2:Zod スキーマを更新する(カスタムバリデーションが必要な場合)

Drizzle のデフォルト設定を超えるカスタムバリデーションがフィールドに必要な場合:

// In schemas.ts, update field schemas if needed
registerFieldSchemas(existingSchema, {
  newField: (schema) => schema.min(1).max(100),
});
ステップ 3:マイグレーションを生成して適用する
pnpm db:generate
# Review the generated migration SQL
pnpm db:migrate

テーブル間のリレーションの追加

多対多リレーション用の結合テーブル
export const entityAEntityBRelations = pgTable(
  'entity_a_entity_b_relations',
  {
    ...projectScoped,  // Appropriate scope
    entityAId: varchar('entity_a_id', { length: 256 }).notNull(),
    entityBId: varchar('entity_b_id', { length: 256 }).notNull(),
    // Optional: relation-specific fields
    config: jsonb('config').$type<RelationConfig>(),
    ...timestamps,
  },
  (table) => [
    primaryKey({ columns: [table.tenantId, table.projectId, table.id] }),
    // Foreign keys to both tables
    foreignKey({
      columns: [table.tenantId, table.projectId, table.entityAId],
      foreignColumns: [entityA.tenantId, entityA.projectId, entityA.id],
      name: 'entity_a_entity_b_relations_a_fk',
    }).onDelete('cascade'),
    foreignKey({
      columns: [table.tenantId, table.projectId, table.entityBId],
      foreignColumns: [entityB.tenantId, entityB.projectId, entityB.id],
      name: 'entity_a_entity_b_relations_b_fk',
    }).onDelete('cascade'),
    // Optional: unique constraint
    unique('entity_a_entity_b_unique').on(table.entityAId, table.entityBId),
  ]
);

外部キーのルール

  1. 削除時は CASCADE:親の削除時に子も自動的に削除します
  2. 削除時は SET NULL:任意の参照に使用します
  3. 同じデータベース内のみ:manage DB と runtime DB の間に外部キーは設定しません
  4. アプリケーションで適用:データベース間の参照はコードで適用します

マイグレーションのルール

⚠️ 重要なルール:

  • drizzle/meta/ 内のファイルを手動で編集しないでください
  • 適用済みの既存のマイグレーション SQL ファイルを編集しないでください
  • マイグレーションファイルを手動で削除しないでください。pnpm db:drop を使用してください
  • ✅ 新しく生成されたマイグレーションは、初回適用前であれば編集できます

変更セットの要件

スキーマの変更には変更セットが必要です:

pnpm bump minor --pkg agents-core "Add myNewTable for storing X"

次の場合は minor バージョンを使用してください:

  • 新しいテーブル
  • 新しい必須カラム
  • 互換性を損なうスキーマ変更

次の場合は patch バージョンを使用してください:

  • デフォルト値を持つ新しい任意カラム
  • 新しいインデックス
  • 互換性を損なわない追加

データモデル変更のチェックリスト

データモデルの変更を完了する前に、次の項目を確認してください:

  • スキーマが正しいファイル(manage または runtime)で定義されている
  • 適切なスコープのパターン(tenant/project/agent/subAgent)が使用されている
  • JSONB フィールドに型注釈が付いている
  • 主キーにすべてのスコープ列が含まれている
  • 外部キーで適切なカスケード動作が設定されている
  • Zod スキーマが作成されている(Select、Insert、Update、Api の各バリアント)
  • エンティティ型がエクスポートされている
  • データアクセス関数が作成されている
  • マイグレーションが生成され、レビューされている
  • manage スキーマを変更した場合:既存のブランチデータに対して pnpm --filter @inkeep/agents-core db:manage:sync-all-branches をテストしている
  • 変更セットが作成されている
  • 新しいデータアクセス関数のテストが作成されている

コメント