Files
spoon/packages/backend/convex/spoonState.ts
T
Gabriel Brown 2dfa97ee4f
Build and Push Next App / quality (push) Failing after 48s
Build and Push Next App / build-next (push) Has been skipped
Add agent workflows & stuff
2026-06-21 21:15:15 -05:00

124 lines
3.9 KiB
TypeScript

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<Id<'spoonRepositoryStates'>> => {
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,
});
},
});