Phase 1.1: Monorepo Setup & Design System - Turborepo + pnpm workspaces monorepo structure - Shared packages: @yetanother/types, @yetanother/utils, @yetanother/hooks, @yetanother/ui, @yetanother/db - Radix UI primitives with custom shadcn/ui-style components (Button, Dialog, Toast, Tooltip, Tabs, Label) - Tailwind CSS v4 with design tokens (light theme) - TypeScript strict mode with composite project references Phase 1.2: Authentication & User Management - Fastify server setup with JWT auth (register, login, me endpoints) - @fastify/jwt with cookie support - User and workspace models with Prisma ORM Phase 1.3: Database Schema & Migrations - Full Prisma schema: User, Workspace, Node, NodeLink, Tag, Folder, Reminder, Embedding, Activity, Attachment, WorkspaceMember, SyncCheckpoint - pgvector extension for embeddings (vector(1536)) - Seed script for dev data - Comprehensive indexes (GIN, BRIN, B-tree) Phase 1.4: API Foundation & Middleware - Fastify REST API with versioned routes (/api/v1) - Zod request validation on all endpoints - CORS, Helmet security headers, rate limiting - Structured logging with pino-pretty - Swagger/OpenAPI docs at /docs - WebSocket support (collaboration ready) - CRUD routes: nodes, workspaces, search, auth Tooling: - ESLint 9 flat config with TypeScript parser - Prettier with consistent formatting rules - Turbo v2 task orchestration - GitHub Actions CI workflow - Husky + lint-staged pre-commit hooks
128 lines
4.3 KiB
TypeScript
128 lines
4.3 KiB
TypeScript
import { FastifyInstance } from 'fastify';
|
|
import { z } from 'zod';
|
|
|
|
const createNodeBodySchema = z.object({
|
|
type: z.enum(['task', 'event', 'note', 'project', 'goal']),
|
|
workspaceId: z.string().uuid(),
|
|
title: z.string().min(1).max(500),
|
|
content: z.record(z.unknown()).optional(),
|
|
priority: z.number().int().min(1).max(5).optional(),
|
|
startTime: z.string().datetime().optional(),
|
|
endTime: z.string().datetime().optional(),
|
|
parentId: z.string().uuid().optional(),
|
|
});
|
|
|
|
export async function nodeRoutes(app: FastifyInstance) {
|
|
app.addHook('preHandler', app.authenticate);
|
|
|
|
app.get('/nodes', async (request) => {
|
|
const { id: userId } = request.user as { id: string };
|
|
const prisma = app.prisma;
|
|
const { type, status, workspaceId } = request.query as Record<string, string | undefined>;
|
|
|
|
const nodes = await prisma.node.findMany({
|
|
where: {
|
|
ownerId: userId,
|
|
...(type ? { type: type as never } : {}),
|
|
...(status ? { status: status as never } : {}),
|
|
...(workspaceId ? { workspaceId } : {}),
|
|
},
|
|
orderBy: { updatedAt: 'desc' },
|
|
take: 100,
|
|
});
|
|
|
|
return { nodes };
|
|
});
|
|
|
|
app.post<{ Body: z.infer<typeof createNodeBodySchema> }>('/nodes', async (request, reply) => {
|
|
const body = createNodeBodySchema.parse(request.body);
|
|
const { id: userId } = request.user as { id: string };
|
|
const prisma = app.prisma;
|
|
|
|
const node = await prisma.node.create({
|
|
data: {
|
|
type: body.type,
|
|
workspaceId: body.workspaceId,
|
|
ownerId: userId,
|
|
title: body.title,
|
|
content: (body.content ?? {}) as never,
|
|
priority: body.priority,
|
|
startTime: body.startTime ? new Date(body.startTime) : null,
|
|
endTime: body.endTime ? new Date(body.endTime) : null,
|
|
parentId: body.parentId,
|
|
},
|
|
});
|
|
|
|
return reply.status(201).send({ node });
|
|
});
|
|
|
|
app.get<{ Params: { id: string } }>('/nodes/:id', async (request, reply) => {
|
|
const { id } = request.params;
|
|
const { id: userId } = request.user as { id: string };
|
|
const prisma = app.prisma;
|
|
|
|
const node = await prisma.node.findFirst({
|
|
where: { id, ownerId: userId },
|
|
include: { tags: { include: { tag: true } }, links: true, backlinks: true },
|
|
});
|
|
|
|
if (!node) return reply.status(404).send({ error: 'Node not found' });
|
|
return { node };
|
|
});
|
|
|
|
app.patch<{ Params: { id: string } }>('/nodes/:id', async (request, reply) => {
|
|
const { id } = request.params;
|
|
const { id: userId } = request.user as { id: string };
|
|
const prisma = app.prisma;
|
|
|
|
const existing = await prisma.node.findFirst({ where: { id, ownerId: userId } });
|
|
if (!existing) return reply.status(404).send({ error: 'Node not found' });
|
|
|
|
const body = request.body as Record<string, unknown>;
|
|
const node = await prisma.node.update({
|
|
where: { id },
|
|
data: {
|
|
...(body.title ? { title: body.title as string } : {}),
|
|
...(body.content ? { content: body.content as never } : {}),
|
|
...(body.priority !== undefined ? { priority: body.priority as number } : {}),
|
|
...(body.status ? { status: body.status as never } : {}),
|
|
version: { increment: 1 },
|
|
},
|
|
});
|
|
|
|
return { node };
|
|
});
|
|
|
|
app.delete<{ Params: { id: string } }>('/nodes/:id', async (request, reply) => {
|
|
const { id } = request.params;
|
|
const { id: userId } = request.user as { id: string };
|
|
const prisma = app.prisma;
|
|
|
|
const existing = await prisma.node.findFirst({ where: { id, ownerId: userId } });
|
|
if (!existing) return reply.status(404).send({ error: 'Node not found' });
|
|
|
|
await prisma.node.update({
|
|
where: { id },
|
|
data: { status: 'deleted', archivedAt: new Date() },
|
|
});
|
|
|
|
return reply.status(204).send();
|
|
});
|
|
|
|
app.post<{ Params: { id: string } }>('/nodes/:id/restore', async (request, reply) => {
|
|
const { id } = request.params;
|
|
const { id: userId } = request.user as { id: string };
|
|
const prisma = app.prisma;
|
|
|
|
const existing = await prisma.node.findFirst({ where: { id, ownerId: userId, status: 'deleted' } });
|
|
if (!existing) return reply.status(404).send({ error: 'Node not found' });
|
|
|
|
const node = await prisma.node.update({
|
|
where: { id },
|
|
data: { status: 'active', archivedAt: null },
|
|
});
|
|
|
|
return { node };
|
|
});
|
|
}
|