Loading...
Loading...
Guide for making changes to the database schema, validation, types, and data access layer. Use when adding tables, columns, relations, or modifying the data model. Triggers on: add table, add column, modify schema, database change, data model, new entity, schema migration.
npx skill4agent add inkeep/agents data-model-changes| Database | Config File | Schema File | Purpose |
|---|---|---|---|
| Manage (Doltgres) | | | Versioned config: projects, agents, tools, triggers, evaluators |
| Runtime (Postgres) | | | Transactional data: conversations, messages, tasks, API keys |
// 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] }),
]
);// 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] }),
]
);.$type<T>()models: jsonb('models').$type<Models>(),
config: jsonb('config').$type<{ type: 'mcp'; mcp: ToolMcpConfig }>().notNull(),
metadata: jsonb('metadata').$type<ConversationMetadata>(),packages/agents-core/src/db/manage/manage-schema.tsruntime-schema.tsexport 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),
]
);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
}));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');packages/agents-core/src/types/entities.tsexport 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>;packages/agents-core/src/data-access/manage/myNewTable.tsruntime/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)
)
);
};packages/agents-core/src/data-access/index.tsexport * from './manage/myNewTable';# 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// 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 ...
);// In schemas.ts, update field schemas if needed
registerFieldSchemas(existingSchema, {
newField: (schema) => schema.min(1).max(100),
});pnpm db:generate
# Review the generated migration SQL
pnpm db:migrateexport 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),
]
);drizzle/meta/pnpm db:droppnpm bump minor --pkg agents-core "Add myNewTable for storing X"