import { ConvexError, v } from 'convex/values'; import type { Doc, Id } from './_generated/dataModel'; import { internalMutation, internalQuery, query } from './_generated/server'; import { getOwnedSpoon, getRequiredUserId } from './model'; const syncStatus = v.union( v.literal('up_to_date'), v.literal('behind'), v.literal('ahead'), v.literal('diverged'), v.literal('unknown'), ); export const deriveSyncStatus = ({ upstreamAheadBy, forkAheadBy, }: { upstreamAheadBy: number; forkAheadBy: number; }): Doc<'spoonRepositoryStates'>['status'] => { if (upstreamAheadBy === 0 && forkAheadBy === 0) return 'up_to_date'; if (upstreamAheadBy > 0 && forkAheadBy === 0) return 'behind'; if (upstreamAheadBy === 0 && forkAheadBy > 0) return 'ahead'; if (upstreamAheadBy > 0 && forkAheadBy > 0) return 'diverged'; return 'unknown'; }; 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('spoonRepositoryStates') .withIndex('by_spoon', (q) => q.eq('spoonId', spoonId)) .first(); }, }); export const listForOwner = query({ args: {}, handler: async (ctx) => { const ownerId = await getRequiredUserId(ctx); return await ctx.db .query('spoonRepositoryStates') .withIndex('by_owner', (q) => q.eq('ownerId', ownerId)) .collect(); }, }); export const getInternal = internalQuery({ args: { spoonId: v.id('spoons'), ownerId: v.id('users') }, handler: async (ctx, { spoonId, ownerId }) => { const state = await ctx.db .query('spoonRepositoryStates') .withIndex('by_spoon', (q) => q.eq('spoonId', spoonId)) .first(); if (state && state.ownerId !== ownerId) { throw new ConvexError('Repository state ownership mismatch.'); } return state; }, }); export const upsert = internalMutation({ args: { spoonId: v.id('spoons'), ownerId: v.id('users'), upstreamFullName: v.string(), forkFullName: v.string(), upstreamDefaultBranch: v.string(), forkDefaultBranch: v.string(), upstreamHeadSha: v.optional(v.string()), forkHeadSha: v.optional(v.string()), mergeBaseSha: v.optional(v.string()), upstreamAheadBy: v.number(), forkAheadBy: v.number(), status: syncStatus, openForkPullRequestCount: v.number(), openUpstreamPullRequestCount: v.number(), lastCommitAt: v.optional(v.number()), rawCompareUrl: v.optional(v.string()), }, handler: async (ctx, args): Promise> => { const now = Date.now(); const existing = await ctx.db .query('spoonRepositoryStates') .withIndex('by_spoon', (q) => q.eq('spoonId', args.spoonId)) .first(); const patch = { ownerId: args.ownerId, upstreamFullName: args.upstreamFullName, forkFullName: args.forkFullName, upstreamDefaultBranch: args.upstreamDefaultBranch, forkDefaultBranch: args.forkDefaultBranch, upstreamHeadSha: args.upstreamHeadSha, forkHeadSha: args.forkHeadSha, mergeBaseSha: args.mergeBaseSha, upstreamAheadBy: args.upstreamAheadBy, forkAheadBy: args.forkAheadBy, status: args.status, openForkPullRequestCount: args.openForkPullRequestCount, openUpstreamPullRequestCount: args.openUpstreamPullRequestCount, lastCommitAt: args.lastCommitAt, rawCompareUrl: args.rawCompareUrl, refreshedAt: now, updatedAt: now, }; if (existing) { if (existing.ownerId !== args.ownerId) { throw new ConvexError('Repository state ownership mismatch.'); } await ctx.db.patch(existing._id, patch); return existing._id; } return await ctx.db.insert('spoonRepositoryStates', { spoonId: args.spoonId, ...patch, createdAt: now, }); }, });