feat: Add errands persistence layer — repository port, Drizzle adapter, migration, and tests
- Add errand-repository.ts port with ErrandRepository, ErrandWithAlias, ErrandStatus types - Add DrizzleErrandRepository adapter with create, findById (left-joins agents for alias), findAll (optional projectId/status filters, desc by createdAt), update, delete - Wire exports into repositories/index.ts and repositories/drizzle/index.ts - Add migration 0035_faulty_human_fly.sql (CREATE TABLE errands) and drizzle snapshot - Add 13 tests covering CRUD, filtering, ordering, agentAlias join, cascade/set-null FK behaviour - Update docs/database.md to document the errands table and ErrandRepository Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
336
apps/server/db/repositories/drizzle/errand.test.ts
Normal file
336
apps/server/db/repositories/drizzle/errand.test.ts
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
/**
|
||||||
|
* DrizzleErrandRepository Tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import { DrizzleErrandRepository } from './errand.js';
|
||||||
|
import { createTestDatabase } from './test-helpers.js';
|
||||||
|
import type { DrizzleDatabase } from '../../index.js';
|
||||||
|
import { projects, agents, errands } from '../../schema.js';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
|
describe('DrizzleErrandRepository', () => {
|
||||||
|
let db: DrizzleDatabase;
|
||||||
|
let repo: DrizzleErrandRepository;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
db = createTestDatabase();
|
||||||
|
repo = new DrizzleErrandRepository(db);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper: create a project record
|
||||||
|
async function createProject(name = 'Test Project', suffix = '') {
|
||||||
|
const id = nanoid();
|
||||||
|
const now = new Date();
|
||||||
|
const [project] = await db.insert(projects).values({
|
||||||
|
id,
|
||||||
|
name: name + suffix + id,
|
||||||
|
url: `https://github.com/test/${id}`,
|
||||||
|
defaultBranch: 'main',
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
}).returning();
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: create an agent record
|
||||||
|
async function createAgent(name?: string) {
|
||||||
|
const id = nanoid();
|
||||||
|
const now = new Date();
|
||||||
|
const agentName = name ?? `agent-${id}`;
|
||||||
|
const [agent] = await db.insert(agents).values({
|
||||||
|
id,
|
||||||
|
name: agentName,
|
||||||
|
worktreeId: `agent-workdirs/${agentName}`,
|
||||||
|
provider: 'claude',
|
||||||
|
status: 'idle',
|
||||||
|
mode: 'execute',
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
}).returning();
|
||||||
|
return agent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: create an errand
|
||||||
|
async function createErrand(overrides: Partial<{
|
||||||
|
id: string;
|
||||||
|
description: string;
|
||||||
|
branch: string;
|
||||||
|
baseBranch: string;
|
||||||
|
agentId: string | null;
|
||||||
|
projectId: string | null;
|
||||||
|
status: 'active' | 'pending_review' | 'conflict' | 'merged' | 'abandoned';
|
||||||
|
createdAt: Date;
|
||||||
|
}> = {}) {
|
||||||
|
const project = await createProject();
|
||||||
|
const id = overrides.id ?? nanoid();
|
||||||
|
return repo.create({
|
||||||
|
id,
|
||||||
|
description: overrides.description ?? 'Test errand',
|
||||||
|
branch: overrides.branch ?? 'feature/test',
|
||||||
|
baseBranch: overrides.baseBranch ?? 'main',
|
||||||
|
agentId: overrides.agentId !== undefined ? overrides.agentId : null,
|
||||||
|
projectId: overrides.projectId !== undefined ? overrides.projectId : project.id,
|
||||||
|
status: overrides.status ?? 'active',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('create + findById', () => {
|
||||||
|
it('should create errand and find by id with all fields', async () => {
|
||||||
|
const project = await createProject();
|
||||||
|
const id = nanoid();
|
||||||
|
|
||||||
|
await repo.create({
|
||||||
|
id,
|
||||||
|
description: 'Fix the bug',
|
||||||
|
branch: 'fix/bug-123',
|
||||||
|
baseBranch: 'main',
|
||||||
|
agentId: null,
|
||||||
|
projectId: project.id,
|
||||||
|
status: 'active',
|
||||||
|
});
|
||||||
|
|
||||||
|
const found = await repo.findById(id);
|
||||||
|
expect(found).toBeDefined();
|
||||||
|
expect(found!.id).toBe(id);
|
||||||
|
expect(found!.description).toBe('Fix the bug');
|
||||||
|
expect(found!.branch).toBe('fix/bug-123');
|
||||||
|
expect(found!.baseBranch).toBe('main');
|
||||||
|
expect(found!.status).toBe('active');
|
||||||
|
expect(found!.projectId).toBe(project.id);
|
||||||
|
expect(found!.agentId).toBeNull();
|
||||||
|
expect(found!.agentAlias).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findAll', () => {
|
||||||
|
it('should return all errands ordered by createdAt desc', async () => {
|
||||||
|
const project = await createProject();
|
||||||
|
const t1 = new Date('2024-01-01T00:00:00Z');
|
||||||
|
const t2 = new Date('2024-01-02T00:00:00Z');
|
||||||
|
const t3 = new Date('2024-01-03T00:00:00Z');
|
||||||
|
|
||||||
|
const id1 = nanoid();
|
||||||
|
const id2 = nanoid();
|
||||||
|
const id3 = nanoid();
|
||||||
|
|
||||||
|
await db.insert(errands).values([
|
||||||
|
{ id: id1, description: 'Errand 1', branch: 'b1', baseBranch: 'main', agentId: null, projectId: project.id, status: 'active', createdAt: t1, updatedAt: t1 },
|
||||||
|
{ id: id2, description: 'Errand 2', branch: 'b2', baseBranch: 'main', agentId: null, projectId: project.id, status: 'active', createdAt: t2, updatedAt: t2 },
|
||||||
|
{ id: id3, description: 'Errand 3', branch: 'b3', baseBranch: 'main', agentId: null, projectId: project.id, status: 'active', createdAt: t3, updatedAt: t3 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await repo.findAll();
|
||||||
|
expect(result.length).toBeGreaterThanOrEqual(3);
|
||||||
|
// Find our three in the results
|
||||||
|
const ids = result.map((e) => e.id);
|
||||||
|
expect(ids.indexOf(id3)).toBeLessThan(ids.indexOf(id2));
|
||||||
|
expect(ids.indexOf(id2)).toBeLessThan(ids.indexOf(id1));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter by projectId', async () => {
|
||||||
|
const projectA = await createProject('A');
|
||||||
|
const projectB = await createProject('B');
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const idA1 = nanoid();
|
||||||
|
const idA2 = nanoid();
|
||||||
|
const idB1 = nanoid();
|
||||||
|
|
||||||
|
await db.insert(errands).values([
|
||||||
|
{ id: idA1, description: 'A1', branch: 'b-a1', baseBranch: 'main', agentId: null, projectId: projectA.id, status: 'active', createdAt: now, updatedAt: now },
|
||||||
|
{ id: idA2, description: 'A2', branch: 'b-a2', baseBranch: 'main', agentId: null, projectId: projectA.id, status: 'active', createdAt: now, updatedAt: now },
|
||||||
|
{ id: idB1, description: 'B1', branch: 'b-b1', baseBranch: 'main', agentId: null, projectId: projectB.id, status: 'active', createdAt: now, updatedAt: now },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await repo.findAll({ projectId: projectA.id });
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result.map((e) => e.id).sort()).toEqual([idA1, idA2].sort());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter by status', async () => {
|
||||||
|
const project = await createProject();
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const id1 = nanoid();
|
||||||
|
const id2 = nanoid();
|
||||||
|
const id3 = nanoid();
|
||||||
|
|
||||||
|
await db.insert(errands).values([
|
||||||
|
{ id: id1, description: 'E1', branch: 'b1', baseBranch: 'main', agentId: null, projectId: project.id, status: 'active', createdAt: now, updatedAt: now },
|
||||||
|
{ id: id2, description: 'E2', branch: 'b2', baseBranch: 'main', agentId: null, projectId: project.id, status: 'pending_review', createdAt: now, updatedAt: now },
|
||||||
|
{ id: id3, description: 'E3', branch: 'b3', baseBranch: 'main', agentId: null, projectId: project.id, status: 'merged', createdAt: now, updatedAt: now },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await repo.findAll({ status: 'pending_review' });
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].id).toBe(id2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter by both projectId and status', async () => {
|
||||||
|
const projectA = await createProject('PA');
|
||||||
|
const projectB = await createProject('PB');
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const idMatch = nanoid();
|
||||||
|
const idOtherStatus = nanoid();
|
||||||
|
const idOtherProject = nanoid();
|
||||||
|
const idNeither = nanoid();
|
||||||
|
|
||||||
|
await db.insert(errands).values([
|
||||||
|
{ id: idMatch, description: 'Match', branch: 'b1', baseBranch: 'main', agentId: null, projectId: projectA.id, status: 'pending_review', createdAt: now, updatedAt: now },
|
||||||
|
{ id: idOtherStatus, description: 'Wrong status', branch: 'b2', baseBranch: 'main', agentId: null, projectId: projectA.id, status: 'active', createdAt: now, updatedAt: now },
|
||||||
|
{ id: idOtherProject, description: 'Wrong project', branch: 'b3', baseBranch: 'main', agentId: null, projectId: projectB.id, status: 'pending_review', createdAt: now, updatedAt: now },
|
||||||
|
{ id: idNeither, description: 'Neither', branch: 'b4', baseBranch: 'main', agentId: null, projectId: projectB.id, status: 'active', createdAt: now, updatedAt: now },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await repo.findAll({ projectId: projectA.id, status: 'pending_review' });
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].id).toBe(idMatch);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findById', () => {
|
||||||
|
it('should return agentAlias when agentId is set', async () => {
|
||||||
|
const agent = await createAgent('known-agent');
|
||||||
|
const project = await createProject();
|
||||||
|
const id = nanoid();
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
await db.insert(errands).values({
|
||||||
|
id,
|
||||||
|
description: 'With agent',
|
||||||
|
branch: 'feature/x',
|
||||||
|
baseBranch: 'main',
|
||||||
|
agentId: agent.id,
|
||||||
|
projectId: project.id,
|
||||||
|
status: 'active',
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
});
|
||||||
|
|
||||||
|
const found = await repo.findById(id);
|
||||||
|
expect(found).toBeDefined();
|
||||||
|
expect(found!.agentAlias).toBe(agent.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return agentAlias as null when agentId is null', async () => {
|
||||||
|
const project = await createProject();
|
||||||
|
const id = nanoid();
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
await db.insert(errands).values({
|
||||||
|
id,
|
||||||
|
description: 'No agent',
|
||||||
|
branch: 'feature/y',
|
||||||
|
baseBranch: 'main',
|
||||||
|
agentId: null,
|
||||||
|
projectId: project.id,
|
||||||
|
status: 'active',
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
});
|
||||||
|
|
||||||
|
const found = await repo.findById(id);
|
||||||
|
expect(found).toBeDefined();
|
||||||
|
expect(found!.agentAlias).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined for unknown id', async () => {
|
||||||
|
const found = await repo.findById('nonexistent');
|
||||||
|
expect(found).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('update', () => {
|
||||||
|
it('should update status and advance updatedAt', async () => {
|
||||||
|
const project = await createProject();
|
||||||
|
const id = nanoid();
|
||||||
|
const past = new Date('2024-01-01T00:00:00Z');
|
||||||
|
|
||||||
|
await db.insert(errands).values({
|
||||||
|
id,
|
||||||
|
description: 'Errand',
|
||||||
|
branch: 'feature/update',
|
||||||
|
baseBranch: 'main',
|
||||||
|
agentId: null,
|
||||||
|
projectId: project.id,
|
||||||
|
status: 'active',
|
||||||
|
createdAt: past,
|
||||||
|
updatedAt: past,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updated = await repo.update(id, { status: 'pending_review' });
|
||||||
|
expect(updated.status).toBe('pending_review');
|
||||||
|
expect(updated.updatedAt.getTime()).toBeGreaterThan(past.getTime());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw on unknown id', async () => {
|
||||||
|
await expect(
|
||||||
|
repo.update('nonexistent', { status: 'merged' })
|
||||||
|
).rejects.toThrow('Errand not found');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('delete', () => {
|
||||||
|
it('should delete errand and findById returns undefined', async () => {
|
||||||
|
const errand = await createErrand();
|
||||||
|
await repo.delete(errand.id);
|
||||||
|
const found = await repo.findById(errand.id);
|
||||||
|
expect(found).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('cascade and set null', () => {
|
||||||
|
it('should cascade delete errands when project is deleted', async () => {
|
||||||
|
const project = await createProject();
|
||||||
|
const id = nanoid();
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
await db.insert(errands).values({
|
||||||
|
id,
|
||||||
|
description: 'Cascade test',
|
||||||
|
branch: 'feature/cascade',
|
||||||
|
baseBranch: 'main',
|
||||||
|
agentId: null,
|
||||||
|
projectId: project.id,
|
||||||
|
status: 'active',
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete project — should cascade delete errands
|
||||||
|
await db.delete(projects).where(eq(projects.id, project.id));
|
||||||
|
|
||||||
|
const found = await repo.findById(id);
|
||||||
|
expect(found).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set agentId to null when agent is deleted', async () => {
|
||||||
|
const agent = await createAgent();
|
||||||
|
const project = await createProject();
|
||||||
|
const id = nanoid();
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
await db.insert(errands).values({
|
||||||
|
id,
|
||||||
|
description: 'Agent null test',
|
||||||
|
branch: 'feature/agent-null',
|
||||||
|
baseBranch: 'main',
|
||||||
|
agentId: agent.id,
|
||||||
|
projectId: project.id,
|
||||||
|
status: 'active',
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete agent — should set null
|
||||||
|
await db.delete(agents).where(eq(agents.id, agent.id));
|
||||||
|
|
||||||
|
const [errand] = await db.select().from(errands).where(eq(errands.id, id));
|
||||||
|
expect(errand).toBeDefined();
|
||||||
|
expect(errand.agentId).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
89
apps/server/db/repositories/drizzle/errand.ts
Normal file
89
apps/server/db/repositories/drizzle/errand.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* Drizzle Errand Repository Adapter
|
||||||
|
*
|
||||||
|
* Implements ErrandRepository interface using Drizzle ORM.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { eq, desc, and } from 'drizzle-orm';
|
||||||
|
import type { DrizzleDatabase } from '../../index.js';
|
||||||
|
import { errands, agents } from '../../schema.js';
|
||||||
|
import type {
|
||||||
|
ErrandRepository,
|
||||||
|
ErrandWithAlias,
|
||||||
|
ErrandStatus,
|
||||||
|
CreateErrandData,
|
||||||
|
UpdateErrandData,
|
||||||
|
} from '../errand-repository.js';
|
||||||
|
import type { Errand } from '../../schema.js';
|
||||||
|
|
||||||
|
export class DrizzleErrandRepository implements ErrandRepository {
|
||||||
|
constructor(private db: DrizzleDatabase) {}
|
||||||
|
|
||||||
|
async create(data: CreateErrandData): Promise<Errand> {
|
||||||
|
const now = new Date();
|
||||||
|
const [created] = await this.db
|
||||||
|
.insert(errands)
|
||||||
|
.values({ ...data, createdAt: now, updatedAt: now })
|
||||||
|
.returning();
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: string): Promise<ErrandWithAlias | undefined> {
|
||||||
|
const result = 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,
|
||||||
|
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);
|
||||||
|
return result[0] ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(opts?: { projectId?: string; status?: ErrandStatus }): Promise<ErrandWithAlias[]> {
|
||||||
|
const conditions = [];
|
||||||
|
if (opts?.projectId) conditions.push(eq(errands.projectId, opts.projectId));
|
||||||
|
if (opts?.status) conditions.push(eq(errands.status, opts.status));
|
||||||
|
|
||||||
|
return this.db
|
||||||
|
.select({
|
||||||
|
id: errands.id,
|
||||||
|
description: errands.description,
|
||||||
|
branch: errands.branch,
|
||||||
|
baseBranch: errands.baseBranch,
|
||||||
|
agentId: errands.agentId,
|
||||||
|
projectId: errands.projectId,
|
||||||
|
status: errands.status,
|
||||||
|
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)
|
||||||
|
.orderBy(desc(errands.createdAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, data: UpdateErrandData): Promise<Errand> {
|
||||||
|
const [updated] = await this.db
|
||||||
|
.update(errands)
|
||||||
|
.set({ ...data, updatedAt: new Date() })
|
||||||
|
.where(eq(errands.id, id))
|
||||||
|
.returning();
|
||||||
|
if (!updated) throw new Error(`Errand not found: ${id}`);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 { DrizzleConversationRepository } from './conversation.js';
|
||||||
export { DrizzleChatSessionRepository } from './chat-session.js';
|
export { DrizzleChatSessionRepository } from './chat-session.js';
|
||||||
export { DrizzleReviewCommentRepository } from './review-comment.js';
|
export { DrizzleReviewCommentRepository } from './review-comment.js';
|
||||||
|
export { DrizzleErrandRepository } from './errand.js';
|
||||||
|
|||||||
15
apps/server/db/repositories/errand-repository.ts
Normal file
15
apps/server/db/repositories/errand-repository.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import type { Errand, NewErrand } from '../schema.js';
|
||||||
|
|
||||||
|
export type ErrandStatus = 'active' | 'pending_review' | 'conflict' | 'merged' | 'abandoned';
|
||||||
|
export type ErrandWithAlias = Errand & { agentAlias: string | null };
|
||||||
|
|
||||||
|
export type CreateErrandData = Omit<NewErrand, 'createdAt' | 'updatedAt'>;
|
||||||
|
export type UpdateErrandData = Partial<Omit<NewErrand, 'id' | 'createdAt'>>;
|
||||||
|
|
||||||
|
export interface ErrandRepository {
|
||||||
|
create(data: CreateErrandData): Promise<Errand>;
|
||||||
|
findById(id: string): Promise<ErrandWithAlias | undefined>;
|
||||||
|
findAll(opts?: { projectId?: string; status?: ErrandStatus }): Promise<ErrandWithAlias[]>;
|
||||||
|
update(id: string, data: UpdateErrandData): Promise<Errand>;
|
||||||
|
delete(id: string): Promise<void>;
|
||||||
|
}
|
||||||
@@ -82,3 +82,11 @@ export type {
|
|||||||
ReviewCommentRepository,
|
ReviewCommentRepository,
|
||||||
CreateReviewCommentData,
|
CreateReviewCommentData,
|
||||||
} from './review-comment-repository.js';
|
} from './review-comment-repository.js';
|
||||||
|
|
||||||
|
export type {
|
||||||
|
ErrandRepository,
|
||||||
|
ErrandWithAlias,
|
||||||
|
ErrandStatus,
|
||||||
|
CreateErrandData,
|
||||||
|
UpdateErrandData,
|
||||||
|
} from './errand-repository.js';
|
||||||
|
|||||||
13
apps/server/drizzle/0035_faulty_human_fly.sql
Normal file
13
apps/server/drizzle/0035_faulty_human_fly.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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,
|
||||||
|
`status` text DEFAULT 'active' NOT NULL,
|
||||||
|
`created_at` integer NOT NULL,
|
||||||
|
`updated_at` integer NOT NULL,
|
||||||
|
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
|
||||||
|
);
|
||||||
1974
apps/server/drizzle/meta/0035_snapshot.json
Normal file
1974
apps/server/drizzle/meta/0035_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,8 +5,8 @@
|
|||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
- **Schema**: `apps/server/db/schema.ts` — all tables, columns, relations
|
- **Schema**: `apps/server/db/schema.ts` — all tables, columns, relations
|
||||||
- **Ports** (interfaces): `apps/server/db/repositories/*.ts` — 13 repository interfaces
|
- **Ports** (interfaces): `apps/server/db/repositories/*.ts` — 14 repository interfaces
|
||||||
- **Adapters** (implementations): `apps/server/db/repositories/drizzle/*.ts` — 13 Drizzle adapters
|
- **Adapters** (implementations): `apps/server/db/repositories/drizzle/*.ts` — 14 Drizzle adapters
|
||||||
- **Barrel exports**: `apps/server/db/index.ts` re-exports everything
|
- **Barrel exports**: `apps/server/db/index.ts` re-exports everything
|
||||||
|
|
||||||
All adapters use nanoid() for IDs, auto-manage timestamps, and use Drizzle's `.returning()` for atomic reads after writes.
|
All adapters use nanoid() for IDs, auto-manage timestamps, and use Drizzle's `.returning()` for atomic reads after writes.
|
||||||
@@ -196,6 +196,21 @@ Messages within a chat session.
|
|||||||
|
|
||||||
Index: `(chatSessionId)`.
|
Index: `(chatSessionId)`.
|
||||||
|
|
||||||
|
### errands
|
||||||
|
|
||||||
|
Tracks errand work items linked to a project branch, optionally assigned to an agent.
|
||||||
|
|
||||||
|
| Column | Type | Notes |
|
||||||
|
|--------|------|-------|
|
||||||
|
| id | text PK | caller-supplied |
|
||||||
|
| description | text NOT NULL | human-readable description |
|
||||||
|
| branch | text NOT NULL | working branch name |
|
||||||
|
| baseBranch | text NOT NULL | default 'main' |
|
||||||
|
| agentId | text FK → agents (set null) | assigned agent; null if unassigned |
|
||||||
|
| projectId | text FK → projects (cascade) | owning project |
|
||||||
|
| status | text enum | active, pending_review, conflict, merged, abandoned; default 'active' |
|
||||||
|
| createdAt, updatedAt | integer/timestamp | |
|
||||||
|
|
||||||
### review_comments
|
### review_comments
|
||||||
|
|
||||||
Inline review comments on phase diffs, persisted across page reloads.
|
Inline review comments on phase diffs, persisted across page reloads.
|
||||||
@@ -216,7 +231,7 @@ Index: `(phaseId)`.
|
|||||||
|
|
||||||
## Repository Interfaces
|
## Repository Interfaces
|
||||||
|
|
||||||
13 repositories, each with standard CRUD plus domain-specific methods:
|
14 repositories, each with standard CRUD plus domain-specific methods:
|
||||||
|
|
||||||
| Repository | Key Methods |
|
| Repository | Key Methods |
|
||||||
|-----------|-------------|
|
|-----------|-------------|
|
||||||
@@ -233,6 +248,7 @@ Index: `(phaseId)`.
|
|||||||
| ConversationRepository | create, findById, findPendingForAgent, answer |
|
| ConversationRepository | create, findById, findPendingForAgent, answer |
|
||||||
| ChatSessionRepository | createSession, findActiveSession, findActiveSessionByAgentId, updateSession, createMessage, findMessagesBySessionId |
|
| ChatSessionRepository | createSession, findActiveSession, findActiveSessionByAgentId, updateSession, createMessage, findMessagesBySessionId |
|
||||||
| ReviewCommentRepository | create, findByPhaseId, resolve, unresolve, delete |
|
| ReviewCommentRepository | create, findByPhaseId, resolve, unresolve, delete |
|
||||||
|
| ErrandRepository | create, findById, findAll (filter by projectId/status), update, delete |
|
||||||
|
|
||||||
## Migrations
|
## Migrations
|
||||||
|
|
||||||
@@ -244,4 +260,4 @@ Key rules:
|
|||||||
- See [database-migrations.md](database-migrations.md) for full workflow
|
- See [database-migrations.md](database-migrations.md) for full workflow
|
||||||
- Snapshots stale after 0008; migrations 0008+ are hand-written
|
- Snapshots stale after 0008; migrations 0008+ are hand-written
|
||||||
|
|
||||||
Current migrations: 0000 through 0030 (31 total).
|
Current migrations: 0000 through 0035 (36 total).
|
||||||
|
|||||||
Reference in New Issue
Block a user