feat: gateway CRUD routes — conversations, projects, missions, tasks (P1-005/006) (#72)

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #72.
This commit is contained in:
2026-03-13 02:41:03 +00:00
committed by jason.woltje
parent 38897fe423
commit c54b69f7ce
17 changed files with 417 additions and 3 deletions

View File

@@ -14,6 +14,7 @@
"dependencies": {
"@mariozechner/pi-coding-agent": "~0.57.1",
"@mosaic/auth": "workspace:^",
"@mosaic/brain": "workspace:^",
"@mosaic/db": "workspace:^",
"@nestjs/common": "^11.0.0",
"@nestjs/core": "^11.0.0",

View File

@@ -2,11 +2,26 @@ import { Module } from '@nestjs/common';
import { HealthController } from './health/health.controller.js';
import { DatabaseModule } from './database/database.module.js';
import { AuthModule } from './auth/auth.module.js';
import { BrainModule } from './brain/brain.module.js';
import { AgentModule } from './agent/agent.module.js';
import { ChatModule } from './chat/chat.module.js';
import { ConversationsModule } from './conversations/conversations.module.js';
import { ProjectsModule } from './projects/projects.module.js';
import { MissionsModule } from './missions/missions.module.js';
import { TasksModule } from './tasks/tasks.module.js';
@Module({
imports: [DatabaseModule, AuthModule, AgentModule, ChatModule],
imports: [
DatabaseModule,
AuthModule,
BrainModule,
AgentModule,
ChatModule,
ConversationsModule,
ProjectsModule,
MissionsModule,
TasksModule,
],
controllers: [HealthController],
})
export class AppModule {}

View File

@@ -0,0 +1,19 @@
import { Global, Module } from '@nestjs/common';
import { createBrain, type Brain } from '@mosaic/brain';
import type { Db } from '@mosaic/db';
import { DB } from '../database/database.module.js';
export const BRAIN = 'BRAIN';
@Global()
@Module({
providers: [
{
provide: BRAIN,
useFactory: (db: Db): Brain => createBrain(db),
inject: [DB],
},
],
exports: [BRAIN],
})
export class BrainModule {}

View File

@@ -0,0 +1,83 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Inject,
NotFoundException,
Param,
Patch,
Post,
UseGuards,
} from '@nestjs/common';
import type { Brain } from '@mosaic/brain';
import { BRAIN } from '../brain/brain.module.js';
import { AuthGuard } from '../auth/auth.guard.js';
import { CurrentUser } from '../auth/current-user.decorator.js';
import type {
CreateConversationDto,
UpdateConversationDto,
SendMessageDto,
} from './conversations.dto.js';
@Controller('api/conversations')
@UseGuards(AuthGuard)
export class ConversationsController {
constructor(@Inject(BRAIN) private readonly brain: Brain) {}
@Get()
async list(@CurrentUser() user: { id: string }) {
return this.brain.conversations.findAll(user.id);
}
@Get(':id')
async findOne(@Param('id') id: string) {
const conversation = await this.brain.conversations.findById(id);
if (!conversation) throw new NotFoundException('Conversation not found');
return conversation;
}
@Post()
async create(@CurrentUser() user: { id: string }, @Body() dto: CreateConversationDto) {
return this.brain.conversations.create({
userId: user.id,
title: dto.title,
projectId: dto.projectId,
});
}
@Patch(':id')
async update(@Param('id') id: string, @Body() dto: UpdateConversationDto) {
const conversation = await this.brain.conversations.update(id, dto);
if (!conversation) throw new NotFoundException('Conversation not found');
return conversation;
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id') id: string) {
const deleted = await this.brain.conversations.remove(id);
if (!deleted) throw new NotFoundException('Conversation not found');
}
@Get(':id/messages')
async listMessages(@Param('id') id: string) {
const conversation = await this.brain.conversations.findById(id);
if (!conversation) throw new NotFoundException('Conversation not found');
return this.brain.conversations.findMessages(id);
}
@Post(':id/messages')
async addMessage(@Param('id') id: string, @Body() dto: SendMessageDto) {
const conversation = await this.brain.conversations.findById(id);
if (!conversation) throw new NotFoundException('Conversation not found');
return this.brain.conversations.addMessage({
conversationId: id,
role: dto.role,
content: dto.content,
metadata: dto.metadata,
});
}
}

View File

@@ -0,0 +1,15 @@
export interface CreateConversationDto {
title?: string;
projectId?: string;
}
export interface UpdateConversationDto {
title?: string;
projectId?: string | null;
}
export interface SendMessageDto {
role: 'user' | 'assistant' | 'system';
content: string;
metadata?: Record<string, unknown>;
}

View File

@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { ConversationsController } from './conversations.controller.js';
@Module({
controllers: [ConversationsController],
})
export class ConversationsModule {}

View File

@@ -0,0 +1,60 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Inject,
NotFoundException,
Param,
Patch,
Post,
UseGuards,
} from '@nestjs/common';
import type { Brain } from '@mosaic/brain';
import { BRAIN } from '../brain/brain.module.js';
import { AuthGuard } from '../auth/auth.guard.js';
import type { CreateMissionDto, UpdateMissionDto } from './missions.dto.js';
@Controller('api/missions')
@UseGuards(AuthGuard)
export class MissionsController {
constructor(@Inject(BRAIN) private readonly brain: Brain) {}
@Get()
async list() {
return this.brain.missions.findAll();
}
@Get(':id')
async findOne(@Param('id') id: string) {
const mission = await this.brain.missions.findById(id);
if (!mission) throw new NotFoundException('Mission not found');
return mission;
}
@Post()
async create(@Body() dto: CreateMissionDto) {
return this.brain.missions.create({
name: dto.name,
description: dto.description,
projectId: dto.projectId,
status: dto.status,
});
}
@Patch(':id')
async update(@Param('id') id: string, @Body() dto: UpdateMissionDto) {
const mission = await this.brain.missions.update(id, dto);
if (!mission) throw new NotFoundException('Mission not found');
return mission;
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id') id: string) {
const deleted = await this.brain.missions.remove(id);
if (!deleted) throw new NotFoundException('Mission not found');
}
}

View File

@@ -0,0 +1,14 @@
export interface CreateMissionDto {
name: string;
description?: string;
projectId?: string;
status?: 'planning' | 'active' | 'paused' | 'completed' | 'failed';
}
export interface UpdateMissionDto {
name?: string;
description?: string | null;
projectId?: string | null;
status?: 'planning' | 'active' | 'paused' | 'completed' | 'failed';
metadata?: Record<string, unknown> | null;
}

View File

@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { MissionsController } from './missions.controller.js';
@Module({
controllers: [MissionsController],
})
export class MissionsModule {}

View File

@@ -0,0 +1,61 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Inject,
NotFoundException,
Param,
Patch,
Post,
UseGuards,
} from '@nestjs/common';
import type { Brain } from '@mosaic/brain';
import { BRAIN } from '../brain/brain.module.js';
import { AuthGuard } from '../auth/auth.guard.js';
import { CurrentUser } from '../auth/current-user.decorator.js';
import type { CreateProjectDto, UpdateProjectDto } from './projects.dto.js';
@Controller('api/projects')
@UseGuards(AuthGuard)
export class ProjectsController {
constructor(@Inject(BRAIN) private readonly brain: Brain) {}
@Get()
async list() {
return this.brain.projects.findAll();
}
@Get(':id')
async findOne(@Param('id') id: string) {
const project = await this.brain.projects.findById(id);
if (!project) throw new NotFoundException('Project not found');
return project;
}
@Post()
async create(@CurrentUser() user: { id: string }, @Body() dto: CreateProjectDto) {
return this.brain.projects.create({
name: dto.name,
description: dto.description,
status: dto.status,
ownerId: user.id,
});
}
@Patch(':id')
async update(@Param('id') id: string, @Body() dto: UpdateProjectDto) {
const project = await this.brain.projects.update(id, dto);
if (!project) throw new NotFoundException('Project not found');
return project;
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id') id: string) {
const deleted = await this.brain.projects.remove(id);
if (!deleted) throw new NotFoundException('Project not found');
}
}

View File

@@ -0,0 +1,12 @@
export interface CreateProjectDto {
name: string;
description?: string;
status?: 'active' | 'paused' | 'completed' | 'archived';
}
export interface UpdateProjectDto {
name?: string;
description?: string | null;
status?: 'active' | 'paused' | 'completed' | 'archived';
metadata?: Record<string, unknown> | null;
}

View File

@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { ProjectsController } from './projects.controller.js';
@Module({
controllers: [ProjectsController],
})
export class ProjectsModule {}

View File

@@ -0,0 +1,79 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Inject,
NotFoundException,
Param,
Patch,
Post,
Query,
UseGuards,
} from '@nestjs/common';
import type { Brain } from '@mosaic/brain';
import { BRAIN } from '../brain/brain.module.js';
import { AuthGuard } from '../auth/auth.guard.js';
import type { CreateTaskDto, UpdateTaskDto } from './tasks.dto.js';
@Controller('api/tasks')
@UseGuards(AuthGuard)
export class TasksController {
constructor(@Inject(BRAIN) private readonly brain: Brain) {}
@Get()
async list(
@Query('projectId') projectId?: string,
@Query('missionId') missionId?: string,
@Query('status') status?: string,
) {
if (projectId) return this.brain.tasks.findByProject(projectId);
if (missionId) return this.brain.tasks.findByMission(missionId);
if (status)
return this.brain.tasks.findByStatus(
status as Parameters<typeof this.brain.tasks.findByStatus>[0],
);
return this.brain.tasks.findAll();
}
@Get(':id')
async findOne(@Param('id') id: string) {
const task = await this.brain.tasks.findById(id);
if (!task) throw new NotFoundException('Task not found');
return task;
}
@Post()
async create(@Body() dto: CreateTaskDto) {
return this.brain.tasks.create({
title: dto.title,
description: dto.description,
status: dto.status,
priority: dto.priority,
projectId: dto.projectId,
missionId: dto.missionId,
assignee: dto.assignee,
tags: dto.tags,
dueDate: dto.dueDate ? new Date(dto.dueDate) : undefined,
});
}
@Patch(':id')
async update(@Param('id') id: string, @Body() dto: UpdateTaskDto) {
const task = await this.brain.tasks.update(id, {
...dto,
dueDate: dto.dueDate ? new Date(dto.dueDate) : dto.dueDate === null ? null : undefined,
});
if (!task) throw new NotFoundException('Task not found');
return task;
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id') id: string) {
const deleted = await this.brain.tasks.remove(id);
if (!deleted) throw new NotFoundException('Task not found');
}
}

View File

@@ -0,0 +1,24 @@
export interface CreateTaskDto {
title: string;
description?: string;
status?: 'not-started' | 'in-progress' | 'blocked' | 'done' | 'cancelled';
priority?: 'critical' | 'high' | 'medium' | 'low';
projectId?: string;
missionId?: string;
assignee?: string;
tags?: string[];
dueDate?: string;
}
export interface UpdateTaskDto {
title?: string;
description?: string | null;
status?: 'not-started' | 'in-progress' | 'blocked' | 'done' | 'cancelled';
priority?: 'critical' | 'high' | 'medium' | 'low';
projectId?: string | null;
missionId?: string | null;
assignee?: string | null;
tags?: string[] | null;
dueDate?: string | null;
metadata?: Record<string, unknown> | null;
}

View File

@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { TasksController } from './tasks.controller.js';
@Module({
controllers: [TasksController],
})
export class TasksModule {}

View File

@@ -17,8 +17,8 @@
| P1-002 | done | Phase 1 | Auth middleware — BetterAuth session validation | #71 | #11 |
| P1-003 | done | Phase 1 | @mosaic/brain — migrate from v0, PG backend | #71 | #12 |
| P1-004 | done | Phase 1 | @mosaic/queue — migrate from v0 | #71 | #13 |
| P1-005 | not-started | Phase 1 | Gateway routes — conversations CRUD + messages | | #14 |
| P1-006 | not-started | Phase 1 | Gateway routes — tasks, projects, missions CRUD | | #15 |
| P1-005 | done | Phase 1 | Gateway routes — conversations CRUD + messages | #72 | #14 |
| P1-006 | done | Phase 1 | Gateway routes — tasks, projects, missions CRUD | #72 | #15 |
| P1-007 | done | Phase 1 | WebSocket server — chat streaming | #61 | #16 |
| P1-008 | done | Phase 1 | Basic agent dispatch — single provider | #61 | #17 |
| P1-009 | not-started | Phase 1 | Verify Phase 1 — gateway functional, API tested | — | #18 |

3
pnpm-lock.yaml generated
View File

@@ -47,6 +47,9 @@ importers:
'@mosaic/auth':
specifier: workspace:^
version: link:../../packages/auth
'@mosaic/brain':
specifier: workspace:^
version: link:../../packages/brain
'@mosaic/db':
specifier: workspace:^
version: link:../../packages/db