84 lines
2.4 KiB
TypeScript
84 lines
2.4 KiB
TypeScript
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<AuthEnv>()
|
|
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<string, unknown>
|
|
}
|
|
|
|
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<string, unknown>,
|
|
patch: Record<string, unknown>,
|
|
): Record<string, unknown> {
|
|
const out: Record<string, unknown> = { ...base }
|
|
for (const [k, v] of Object.entries(patch)) {
|
|
if (v === null)
|
|
delete out[k]
|
|
else
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|