feat: Add errands schema, repository, and wire into tRPC context/container
Creates the errands table (with conflictFiles column), errand-repository port interface, DrizzleErrandRepository adapter, and wires the repository into TRPCContext, the DI container, _helpers.ts requireErrandRepository guard, and the test harness. Also fixes pre-existing TS error in controller.test.ts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -50,7 +50,6 @@ function makeController(overrides: {
|
||||
cleanupStrategy,
|
||||
overrides.accountRepository as AccountRepository | undefined,
|
||||
false,
|
||||
overrides.eventBus,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
DrizzleConversationRepository,
|
||||
DrizzleChatSessionRepository,
|
||||
DrizzleReviewCommentRepository,
|
||||
DrizzleErrandRepository,
|
||||
} from './db/index.js';
|
||||
import type { InitiativeRepository } from './db/repositories/initiative-repository.js';
|
||||
import type { PhaseRepository } from './db/repositories/phase-repository.js';
|
||||
@@ -36,6 +37,7 @@ import type { LogChunkRepository } from './db/repositories/log-chunk-repository.
|
||||
import type { ConversationRepository } from './db/repositories/conversation-repository.js';
|
||||
import type { ChatSessionRepository } from './db/repositories/chat-session-repository.js';
|
||||
import type { ReviewCommentRepository } from './db/repositories/review-comment-repository.js';
|
||||
import type { ErrandRepository } from './db/repositories/errand-repository.js';
|
||||
import type { EventBus } from './events/index.js';
|
||||
import { createEventBus } from './events/index.js';
|
||||
import { ProcessManager, ProcessRegistry } from './process/index.js';
|
||||
@@ -77,6 +79,7 @@ export interface Repositories {
|
||||
conversationRepository: ConversationRepository;
|
||||
chatSessionRepository: ChatSessionRepository;
|
||||
reviewCommentRepository: ReviewCommentRepository;
|
||||
errandRepository: ErrandRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,6 +101,7 @@ export function createRepositories(db: DrizzleDatabase): Repositories {
|
||||
conversationRepository: new DrizzleConversationRepository(db),
|
||||
chatSessionRepository: new DrizzleChatSessionRepository(db),
|
||||
reviewCommentRepository: new DrizzleReviewCommentRepository(db),
|
||||
errandRepository: new DrizzleErrandRepository(db),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
104
apps/server/db/repositories/drizzle/errand.ts
Normal file
104
apps/server/db/repositories/drizzle/errand.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Drizzle Errand Repository Adapter
|
||||
*
|
||||
* Implements ErrandRepository interface using Drizzle ORM.
|
||||
*/
|
||||
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { DrizzleDatabase } from '../../index.js';
|
||||
import { errands, agents, type Errand } from '../../schema.js';
|
||||
import type {
|
||||
ErrandRepository,
|
||||
CreateErrandData,
|
||||
UpdateErrandData,
|
||||
ErrandWithAlias,
|
||||
FindAllErrandOptions,
|
||||
} from '../errand-repository.js';
|
||||
|
||||
export class DrizzleErrandRepository implements ErrandRepository {
|
||||
constructor(private db: DrizzleDatabase) {}
|
||||
|
||||
async create(data: CreateErrandData): Promise<Errand> {
|
||||
const now = new Date();
|
||||
const id = nanoid();
|
||||
const [created] = await this.db.insert(errands).values({
|
||||
id,
|
||||
description: data.description,
|
||||
branch: data.branch,
|
||||
baseBranch: data.baseBranch ?? 'main',
|
||||
agentId: data.agentId ?? null,
|
||||
projectId: data.projectId,
|
||||
status: data.status ?? 'active',
|
||||
conflictFiles: data.conflictFiles ?? null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}).returning();
|
||||
return created;
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<ErrandWithAlias | null> {
|
||||
const rows = await this.db
|
||||
.select({
|
||||
id: errands.id,
|
||||
description: errands.description,
|
||||
branch: errands.branch,
|
||||
baseBranch: errands.baseBranch,
|
||||
agentId: errands.agentId,
|
||||
projectId: errands.projectId,
|
||||
status: errands.status,
|
||||
conflictFiles: errands.conflictFiles,
|
||||
createdAt: errands.createdAt,
|
||||
updatedAt: errands.updatedAt,
|
||||
agentAlias: agents.name,
|
||||
})
|
||||
.from(errands)
|
||||
.leftJoin(agents, eq(errands.agentId, agents.id))
|
||||
.where(eq(errands.id, id))
|
||||
.limit(1);
|
||||
if (!rows[0]) return null;
|
||||
return rows[0] as ErrandWithAlias;
|
||||
}
|
||||
|
||||
async findAll(options?: FindAllErrandOptions): Promise<ErrandWithAlias[]> {
|
||||
const conditions = [];
|
||||
if (options?.projectId) conditions.push(eq(errands.projectId, options.projectId));
|
||||
if (options?.status) conditions.push(eq(errands.status, options.status));
|
||||
|
||||
const rows = await this.db
|
||||
.select({
|
||||
id: errands.id,
|
||||
description: errands.description,
|
||||
branch: errands.branch,
|
||||
baseBranch: errands.baseBranch,
|
||||
agentId: errands.agentId,
|
||||
projectId: errands.projectId,
|
||||
status: errands.status,
|
||||
conflictFiles: errands.conflictFiles,
|
||||
createdAt: errands.createdAt,
|
||||
updatedAt: errands.updatedAt,
|
||||
agentAlias: agents.name,
|
||||
})
|
||||
.from(errands)
|
||||
.leftJoin(agents, eq(errands.agentId, agents.id))
|
||||
.where(conditions.length > 0 ? and(...conditions) : undefined);
|
||||
return rows as ErrandWithAlias[];
|
||||
}
|
||||
|
||||
async update(id: string, data: UpdateErrandData): Promise<Errand | null> {
|
||||
await this.db
|
||||
.update(errands)
|
||||
.set({ ...data, updatedAt: new Date() })
|
||||
.where(eq(errands.id, id));
|
||||
const rows = await this.db
|
||||
.select()
|
||||
.from(errands)
|
||||
.where(eq(errands.id, id))
|
||||
.limit(1);
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.db.delete(errands).where(eq(errands.id, id));
|
||||
}
|
||||
}
|
||||
@@ -18,3 +18,4 @@ export { DrizzleLogChunkRepository } from './log-chunk.js';
|
||||
export { DrizzleConversationRepository } from './conversation.js';
|
||||
export { DrizzleChatSessionRepository } from './chat-session.js';
|
||||
export { DrizzleReviewCommentRepository } from './review-comment.js';
|
||||
export { DrizzleErrandRepository } from './errand.js';
|
||||
|
||||
45
apps/server/db/repositories/errand-repository.ts
Normal file
45
apps/server/db/repositories/errand-repository.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Errand Repository Port Interface
|
||||
*
|
||||
* Port for Errand aggregate operations.
|
||||
* Implementations (Drizzle, etc.) are adapters.
|
||||
*/
|
||||
|
||||
import type { Errand, NewErrand, ErrandStatus } from '../schema.js';
|
||||
|
||||
/**
|
||||
* Data for creating a new errand.
|
||||
* Omits system-managed fields (id, createdAt, updatedAt).
|
||||
*/
|
||||
export type CreateErrandData = Omit<NewErrand, 'id' | 'createdAt' | 'updatedAt'>;
|
||||
|
||||
/**
|
||||
* Data for updating an errand.
|
||||
*/
|
||||
export type UpdateErrandData = Partial<Omit<NewErrand, 'id' | 'createdAt'>>;
|
||||
|
||||
/**
|
||||
* Errand with the agent alias joined in.
|
||||
*/
|
||||
export interface ErrandWithAlias extends Errand {
|
||||
agentAlias: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter options for listing errands.
|
||||
*/
|
||||
export interface FindAllErrandOptions {
|
||||
projectId?: string;
|
||||
status?: ErrandStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Errand Repository Port
|
||||
*/
|
||||
export interface ErrandRepository {
|
||||
create(data: CreateErrandData): Promise<Errand>;
|
||||
findById(id: string): Promise<ErrandWithAlias | null>;
|
||||
findAll(options?: FindAllErrandOptions): Promise<ErrandWithAlias[]>;
|
||||
update(id: string, data: UpdateErrandData): Promise<Errand | null>;
|
||||
delete(id: string): Promise<void>;
|
||||
}
|
||||
@@ -82,3 +82,11 @@ export type {
|
||||
ReviewCommentRepository,
|
||||
CreateReviewCommentData,
|
||||
} from './review-comment-repository.js';
|
||||
|
||||
export type {
|
||||
ErrandRepository,
|
||||
CreateErrandData,
|
||||
UpdateErrandData,
|
||||
ErrandWithAlias,
|
||||
FindAllErrandOptions,
|
||||
} from './errand-repository.js';
|
||||
|
||||
@@ -628,3 +628,33 @@ export const reviewComments = sqliteTable('review_comments', {
|
||||
|
||||
export type ReviewComment = InferSelectModel<typeof reviewComments>;
|
||||
export type NewReviewComment = InferInsertModel<typeof reviewComments>;
|
||||
|
||||
// ============================================================================
|
||||
// ERRANDS
|
||||
// ============================================================================
|
||||
|
||||
export const ERRAND_STATUS_VALUES = ['active', 'pending_review', 'conflict', 'merged', 'abandoned'] as const;
|
||||
export type ErrandStatus = (typeof ERRAND_STATUS_VALUES)[number];
|
||||
|
||||
export const errands = sqliteTable('errands', {
|
||||
id: text('id').primaryKey(),
|
||||
description: text('description').notNull(),
|
||||
branch: text('branch').notNull(),
|
||||
baseBranch: text('base_branch').notNull().default('main'),
|
||||
agentId: text('agent_id').references(() => agents.id, { onDelete: 'set null' }),
|
||||
projectId: text('project_id')
|
||||
.notNull()
|
||||
.references(() => projects.id, { onDelete: 'cascade' }),
|
||||
status: text('status', { enum: ERRAND_STATUS_VALUES })
|
||||
.notNull()
|
||||
.default('active'),
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
||||
conflictFiles: text('conflict_files'), // JSON-encoded string[] | null; set on merge conflict
|
||||
}, (table) => [
|
||||
index('errands_project_id_idx').on(table.projectId),
|
||||
index('errands_status_idx').on(table.status),
|
||||
]);
|
||||
|
||||
export type Errand = InferSelectModel<typeof errands>;
|
||||
export type NewErrand = InferInsertModel<typeof errands>;
|
||||
|
||||
17
apps/server/drizzle/0034_salty_next_avengers.sql
Normal file
17
apps/server/drizzle/0034_salty_next_avengers.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
CREATE TABLE `errands` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`description` text NOT NULL,
|
||||
`branch` text NOT NULL,
|
||||
`base_branch` text DEFAULT 'main' NOT NULL,
|
||||
`agent_id` text,
|
||||
`project_id` text NOT NULL,
|
||||
`status` text DEFAULT 'active' NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
`conflict_files` text,
|
||||
FOREIGN KEY (`agent_id`) REFERENCES `agents`(`id`) ON UPDATE no action ON DELETE set null,
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `errands_project_id_idx` ON `errands` (`project_id`);--> statement-breakpoint
|
||||
CREATE INDEX `errands_status_idx` ON `errands` (`status`);
|
||||
1988
apps/server/drizzle/meta/0034_snapshot.json
Normal file
1988
apps/server/drizzle/meta/0034_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -239,6 +239,13 @@
|
||||
"when": 1772409600000,
|
||||
"tag": "0033_drop_approval_columns",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 34,
|
||||
"version": "6",
|
||||
"when": 1772808163349,
|
||||
"tag": "0034_salty_next_avengers",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import type { MessageRepository } from '../db/repositories/message-repository.js
|
||||
import type { AgentRepository } from '../db/repositories/agent-repository.js';
|
||||
import type { InitiativeRepository } from '../db/repositories/initiative-repository.js';
|
||||
import type { PhaseRepository } from '../db/repositories/phase-repository.js';
|
||||
import type { ErrandRepository } from '../db/repositories/errand-repository.js';
|
||||
import type { Initiative, Phase, Task } from '../db/schema.js';
|
||||
import { createTestDatabase } from '../db/repositories/drizzle/test-helpers.js';
|
||||
import { createRepositories } from '../container.js';
|
||||
@@ -204,6 +205,8 @@ export interface TestHarness {
|
||||
initiativeRepository: InitiativeRepository;
|
||||
/** Phase repository */
|
||||
phaseRepository: PhaseRepository;
|
||||
/** Errand repository */
|
||||
errandRepository: ErrandRepository;
|
||||
|
||||
// tRPC Caller
|
||||
/** tRPC caller for direct procedure calls */
|
||||
@@ -409,7 +412,7 @@ export function createTestHarness(): TestHarness {
|
||||
|
||||
// Create repositories
|
||||
const repos = createRepositories(db);
|
||||
const { taskRepository, messageRepository, agentRepository, initiativeRepository, phaseRepository } = repos;
|
||||
const { taskRepository, messageRepository, agentRepository, initiativeRepository, phaseRepository, errandRepository } = repos;
|
||||
|
||||
// Create real managers wired to mocks
|
||||
const dispatchManager = new DefaultDispatchManager(
|
||||
@@ -447,6 +450,7 @@ export function createTestHarness(): TestHarness {
|
||||
coordinationManager,
|
||||
initiativeRepository,
|
||||
phaseRepository,
|
||||
errandRepository,
|
||||
});
|
||||
|
||||
// Create tRPC caller
|
||||
@@ -470,6 +474,7 @@ export function createTestHarness(): TestHarness {
|
||||
agentRepository,
|
||||
initiativeRepository,
|
||||
phaseRepository,
|
||||
errandRepository,
|
||||
|
||||
// tRPC Caller
|
||||
caller,
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { LogChunkRepository } from '../db/repositories/log-chunk-repository
|
||||
import type { ConversationRepository } from '../db/repositories/conversation-repository.js';
|
||||
import type { ChatSessionRepository } from '../db/repositories/chat-session-repository.js';
|
||||
import type { ReviewCommentRepository } from '../db/repositories/review-comment-repository.js';
|
||||
import type { ErrandRepository } from '../db/repositories/errand-repository.js';
|
||||
import type { AccountCredentialManager } from '../agent/credentials/types.js';
|
||||
import type { DispatchManager, PhaseDispatchManager } from '../dispatch/types.js';
|
||||
import type { CoordinationManager } from '../coordination/types.js';
|
||||
@@ -80,6 +81,8 @@ export interface TRPCContext {
|
||||
chatSessionRepository?: ChatSessionRepository;
|
||||
/** Review comment repository for inline review comments on phase diffs */
|
||||
reviewCommentRepository?: ReviewCommentRepository;
|
||||
/** Errand repository for errand CRUD operations */
|
||||
errandRepository?: ErrandRepository;
|
||||
/** Project sync manager for remote fetch/sync operations */
|
||||
projectSyncManager?: ProjectSyncManager;
|
||||
/** Absolute path to the workspace root (.cwrc directory) */
|
||||
@@ -113,6 +116,7 @@ export interface CreateContextOptions {
|
||||
conversationRepository?: ConversationRepository;
|
||||
chatSessionRepository?: ChatSessionRepository;
|
||||
reviewCommentRepository?: ReviewCommentRepository;
|
||||
errandRepository?: ErrandRepository;
|
||||
projectSyncManager?: ProjectSyncManager;
|
||||
workspaceRoot?: string;
|
||||
}
|
||||
@@ -148,6 +152,7 @@ export function createContext(options: CreateContextOptions): TRPCContext {
|
||||
conversationRepository: options.conversationRepository,
|
||||
chatSessionRepository: options.chatSessionRepository,
|
||||
reviewCommentRepository: options.reviewCommentRepository,
|
||||
errandRepository: options.errandRepository,
|
||||
projectSyncManager: options.projectSyncManager,
|
||||
workspaceRoot: options.workspaceRoot,
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { LogChunkRepository } from '../../db/repositories/log-chunk-reposit
|
||||
import type { ConversationRepository } from '../../db/repositories/conversation-repository.js';
|
||||
import type { ChatSessionRepository } from '../../db/repositories/chat-session-repository.js';
|
||||
import type { ReviewCommentRepository } from '../../db/repositories/review-comment-repository.js';
|
||||
import type { ErrandRepository } from '../../db/repositories/errand-repository.js';
|
||||
import type { DispatchManager, PhaseDispatchManager } from '../../dispatch/types.js';
|
||||
import type { CoordinationManager } from '../../coordination/types.js';
|
||||
import type { BranchManager } from '../../git/branch-manager.js';
|
||||
@@ -225,3 +226,13 @@ export function requireProjectSyncManager(ctx: TRPCContext): ProjectSyncManager
|
||||
}
|
||||
return ctx.projectSyncManager;
|
||||
}
|
||||
|
||||
export function requireErrandRepository(ctx: TRPCContext): ErrandRepository {
|
||||
if (!ctx.errandRepository) {
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Errand repository not available',
|
||||
});
|
||||
}
|
||||
return ctx.errandRepository;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user