import { v } from 'convex/values'; import type { Doc } from './_generated/dataModel'; import { internalMutation, internalQuery, mutation, query, } from './_generated/server'; import { getOwnedSpoon, getRequiredUserId } from './model'; const defaultSettings = { autoRefreshEnabled: true, autoReviewEnabled: true, autoSyncEnabled: false, requireAiLowRiskForSync: true, requireCleanCompareForSync: true, ignoredFilePatterns: [] as string[], importantFilePatterns: [] as string[], }; export const getForSpoon = query({ args: { spoonId: v.id('spoons') }, handler: async (ctx, { spoonId }) => { const ownerId = await getRequiredUserId(ctx); await getOwnedSpoon(ctx, spoonId, ownerId); return ( (await ctx.db .query('spoonSettings') .withIndex('by_spoon', (q) => q.eq('spoonId', spoonId)) .first()) ?? { spoonId, ownerId, ...defaultSettings } ); }, }); export const ensureForSpoon = internalMutation({ args: { spoonId: v.id('spoons'), ownerId: v.id('users') }, handler: async (ctx, { spoonId, ownerId }) => { const existing = await ctx.db .query('spoonSettings') .withIndex('by_spoon', (q) => q.eq('spoonId', spoonId)) .first(); if (existing) return existing._id; const now = Date.now(); return await ctx.db.insert('spoonSettings', { spoonId, ownerId, ...defaultSettings, createdAt: now, updatedAt: now, }); }, }); export const getInternal = internalQuery({ args: { spoonId: v.id('spoons'), ownerId: v.id('users') }, handler: async (ctx, { spoonId, ownerId }) => { const settings = await ctx.db .query('spoonSettings') .withIndex('by_spoon', (q) => q.eq('spoonId', spoonId)) .first(); if (settings && settings.ownerId !== ownerId) { throw new Error('Spoon settings ownership mismatch.'); } return settings; }, }); export const listRefreshDue = internalQuery({ args: { limit: v.number() }, handler: async (ctx, { limit }) => { const now = Date.now(); const settings = await ctx.db.query('spoonSettings').collect(); const due = []; for (const item of settings) { if (!item.autoRefreshEnabled) continue; const spoon = await ctx.db.get(item.spoonId); if ( !spoon || spoon.status === 'archived' || spoon.provider !== 'github' ) { continue; } if (spoon.syncCadence === 'manual') continue; const cadenceMs = spoon.syncCadence === 'weekly' ? 7 * 24 * 60 * 60 * 1000 : 24 * 60 * 60 * 1000; const last = spoon.lastGithubRefreshAt ?? spoon.lastCheckedAt ?? 0; if (now - last >= cadenceMs) { due.push({ spoonId: item.spoonId, ownerId: item.ownerId }); } if (due.length >= limit) break; } return due; }, }); export const update = mutation({ args: { spoonId: v.id('spoons'), autoRefreshEnabled: v.optional(v.boolean()), autoReviewEnabled: v.optional(v.boolean()), autoSyncEnabled: v.optional(v.boolean()), requireAiLowRiskForSync: v.optional(v.boolean()), requireCleanCompareForSync: v.optional(v.boolean()), ignoredFilePatterns: v.optional(v.array(v.string())), importantFilePatterns: v.optional(v.array(v.string())), }, handler: async (ctx, args) => { const ownerId = await getRequiredUserId(ctx); await getOwnedSpoon(ctx, args.spoonId, ownerId); let settings = await ctx.db .query('spoonSettings') .withIndex('by_spoon', (q) => q.eq('spoonId', args.spoonId)) .first(); if (!settings) { const id = await ctx.db.insert('spoonSettings', { spoonId: args.spoonId, ownerId, ...defaultSettings, createdAt: Date.now(), updatedAt: Date.now(), }); settings = await ctx.db.get(id); } const patch: Partial> = { updatedAt: Date.now() }; if (args.autoRefreshEnabled !== undefined) { patch.autoRefreshEnabled = args.autoRefreshEnabled; } if (args.autoReviewEnabled !== undefined) { patch.autoReviewEnabled = args.autoReviewEnabled; } if (args.autoSyncEnabled !== undefined) { patch.autoSyncEnabled = args.autoSyncEnabled; } if (args.requireAiLowRiskForSync !== undefined) { patch.requireAiLowRiskForSync = args.requireAiLowRiskForSync; } if (args.requireCleanCompareForSync !== undefined) { patch.requireCleanCompareForSync = args.requireCleanCompareForSync; } if (args.ignoredFilePatterns !== undefined) { patch.ignoredFilePatterns = args.ignoredFilePatterns; } if (args.importantFilePatterns !== undefined) { patch.importantFilePatterns = args.importantFilePatterns; } if (!settings) throw new Error('Spoon settings not found.'); await ctx.db.patch(settings._id, patch); return { success: true }; }, });