import { ConvexError, v } from "convex/values"; import { internalQuery, mutation, query } from "./_generated/server"; import { CURRENT_PREFS_SCHEMA_VERSION, MAX_PREFS_BLOB_SIZE } from "./constants"; export const getPreferencesByUserId = internalQuery({ args: { userId: v.string(), variant: v.string() }, handler: async (ctx, args) => { return await ctx.db .query("userPreferences") .withIndex("by_user_variant", (q) => q.eq("userId", args.userId).eq("variant", args.variant), ) .unique(); }, }); export const getPreferences = query({ args: { variant: v.string() }, handler: async (ctx, args) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) return null; const userId = identity.subject; return await ctx.db .query("userPreferences") .withIndex("by_user_variant", (q) => q.eq("userId", userId).eq("variant", args.variant), ) .unique(); }, }); export const setPreferences = mutation({ args: { variant: v.string(), data: v.any(), expectedSyncVersion: v.number(), schemaVersion: v.optional(v.number()), }, handler: async (ctx, args) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new ConvexError("UNAUTHENTICATED"); const userId = identity.subject; const blobSize = JSON.stringify(args.data).length; if (blobSize > MAX_PREFS_BLOB_SIZE) { throw new ConvexError(`BLOB_TOO_LARGE: ${blobSize} > ${MAX_PREFS_BLOB_SIZE}`); } const existing = await ctx.db .query("userPreferences") .withIndex("by_user_variant", (q) => q.eq("userId", userId).eq("variant", args.variant), ) .unique(); if (existing && existing.syncVersion !== args.expectedSyncVersion) { throw new ConvexError("CONFLICT"); } const nextSyncVersion = (existing?.syncVersion ?? 0) + 1; const schemaVersion = args.schemaVersion ?? CURRENT_PREFS_SCHEMA_VERSION; if (existing) { await ctx.db.patch(existing._id, { data: args.data, schemaVersion, updatedAt: Date.now(), syncVersion: nextSyncVersion, }); } else { await ctx.db.insert("userPreferences", { userId, variant: args.variant, data: args.data, schemaVersion, updatedAt: Date.now(), syncVersion: nextSyncVersion, }); } return { syncVersion: nextSyncVersion }; }, });