import { eq, sql, desc, and, ilike, or, inArray } from 'drizzle-orm'; import { db, schema } from '../db/index.js'; import { authenticate } from '../middleware/auth.js'; export default async function workshopRoutes(fastify) { // Browse patches (public) fastify.get('/', async (request) => { const { q, tags, sort = 'recent', page = 1 } = request.query; const limit = 20; const offset = (page - 1) * limit; const conditions = [eq(schema.sharedPatches.isDeleted, false)]; if (q) { conditions.push( or( ilike(schema.sharedPatches.title, `%${q}%`), ilike(schema.sharedPatches.description, `%${q}%`) ) ); } if (tags) { const tagList = tags.split(',').map(t => t.trim()); conditions.push(sql`${schema.sharedPatches.tags} && ARRAY[${sql.join(tagList.map(t => sql`${t}`), sql`, `)}]::text[]`); } const orderBy = sort === 'popular' ? desc(schema.sharedPatches.likesCount) : desc(schema.sharedPatches.createdAt); const patches = await db.select({ id: schema.sharedPatches.id, title: schema.sharedPatches.title, description: schema.sharedPatches.description, tags: schema.sharedPatches.tags, data: schema.sharedPatches.data, likesCount: schema.sharedPatches.likesCount, createdAt: schema.sharedPatches.createdAt, userId: schema.sharedPatches.userId, }).from(schema.sharedPatches) .where(and(...conditions)) .orderBy(orderBy) .limit(limit) .offset(offset); // Get usernames for patches const userIds = [...new Set(patches.filter(p => p.userId).map(p => p.userId))]; let userMap = {}; if (userIds.length > 0) { const users = await db.select({ id: schema.users.id, username: schema.users.username, avatarUrl: schema.users.avatarUrl, }).from(schema.users).where(inArray(schema.users.id, userIds)); userMap = Object.fromEntries(users.map(u => [u.id, u])); } const result = patches.map(p => ({ ...p, author: userMap[p.userId] || null, userId: undefined, })); return { patches: result, page: +page, limit }; }); // Get single patch fastify.get('/:id', async (request, reply) => { const [patch] = await db.select().from(schema.sharedPatches) .where(and( eq(schema.sharedPatches.id, request.params.id), eq(schema.sharedPatches.isDeleted, false) )).limit(1); if (!patch) return reply.code(404).send({ error: 'Not found' }); let author = null; if (patch.userId) { const [user] = await db.select({ username: schema.users.username, avatarUrl: schema.users.avatarUrl, }).from(schema.users).where(eq(schema.users.id, patch.userId)).limit(1); author = user || null; } return { ...patch, author }; }); // Share a patch (requires auth) fastify.post('/', { preHandler: [authenticate] }, async (request) => { const { title, description, tags, data } = request.body; if (!title || !data) return { error: 'title and data required' }; const [patch] = await db.insert(schema.sharedPatches).values({ userId: request.user.id, title, description: description || '', tags: tags || [], data, }).returning(); return patch; }); // Delete own patch fastify.delete('/:id', { preHandler: [authenticate] }, async (request, reply) => { const [patch] = await db.select().from(schema.sharedPatches) .where(eq(schema.sharedPatches.id, request.params.id)).limit(1); if (!patch) return reply.code(404).send({ error: 'Not found' }); // Owner or admin can delete if (patch.userId !== request.user.id && request.user.role !== 'admin') { return reply.code(403).send({ error: 'Forbidden' }); } await db.update(schema.sharedPatches) .set({ isDeleted: true }) .where(eq(schema.sharedPatches.id, request.params.id)); return { ok: true }; }); // Like a patch fastify.post('/:id/like', { preHandler: [authenticate] }, async (request, reply) => { const patchId = request.params.id; // Check if already liked const [existing] = await db.select().from(schema.likes) .where(and( eq(schema.likes.userId, request.user.id), eq(schema.likes.patchId, patchId) )).limit(1); if (existing) return { liked: true, message: 'Already liked' }; await db.insert(schema.likes).values({ userId: request.user.id, patchId, }); await db.update(schema.sharedPatches) .set({ likesCount: sql`${schema.sharedPatches.likesCount} + 1` }) .where(eq(schema.sharedPatches.id, patchId)); return { liked: true }; }); // Unlike a patch fastify.delete('/:id/like', { preHandler: [authenticate] }, async (request) => { const patchId = request.params.id; const result = await db.delete(schema.likes) .where(and( eq(schema.likes.userId, request.user.id), eq(schema.likes.patchId, patchId) )); await db.update(schema.sharedPatches) .set({ likesCount: sql`GREATEST(${schema.sharedPatches.likesCount} - 1, 0)` }) .where(eq(schema.sharedPatches.id, patchId)); return { liked: false }; }); // Report/flag a patch fastify.post('/:id/report', { preHandler: [authenticate] }, async (request) => { await db.update(schema.sharedPatches) .set({ isFlagged: true }) .where(eq(schema.sharedPatches.id, request.params.id)); return { ok: true }; }); }