import type { AuthEnv } from '@/middleware/auth.js' import { Buffer } from 'node:buffer' import { eq, sql } from 'drizzle-orm' import { Hono } from 'hono' import { userSettings } from '@/db/schema.js' import { db } from '@/lib/db.js' import { authMiddleware } from '@/middleware/auth.js' import { config } from '@/shared/config.js' export const sync = new Hono() sync.use('*', authMiddleware) sync.get('/sync', async (c) => { const userId = c.get('userId') const [row] = await db.select().from(userSettings).where(eq(userSettings.userId, userId)) if (!row) return c.json({ version: 0, data: {} }) return c.json({ version: row.version, data: row.data }) }) interface PutBody { baseVersion: number patch: Record } sync.put('/sync', async (c) => { const raw = await c.req.text() if (Buffer.byteLength(raw, 'utf8') > config.maxPayloadBytes) return c.json({ error: 'payload-too-large' }, 413) let body: PutBody try { body = JSON.parse(raw) } catch { return c.json({ error: 'bad-json' }, 400) } if (typeof body.baseVersion !== 'number' || typeof body.patch !== 'object' || body.patch === null || Array.isArray(body.patch)) return c.json({ error: 'malformed' }, 400) const userId = c.get('userId') return db.transaction(async (tx) => { const [row] = await tx .select() .from(userSettings) .where(eq(userSettings.userId, userId)) .for('update') if (!row) { const merged = applyPatch({}, body.patch) const [inserted] = await tx .insert(userSettings) .values({ userId, version: 1, data: merged }) .returning() return c.json({ version: inserted.version, data: inserted.data }) } if (body.baseVersion !== row.version) return c.json({ version: row.version, data: row.data }, 409) const merged = applyPatch(row.data, body.patch) const [updated] = await tx .update(userSettings) .set({ version: row.version + 1, data: merged, updatedAt: sql`now()` }) .where(eq(userSettings.userId, userId)) .returning() return c.json({ version: updated.version, data: updated.data }) }) }) function applyPatch( base: Record, patch: Record, ): Record { const out: Record = { ...base } for (const [k, v] of Object.entries(patch)) { if (v === null) delete out[k] else out[k] = v } return out }