feat(spoons): find owned spoons by pushed repo (indexed)

This commit is contained in:
Gabriel Brown
2026-07-11 14:12:30 -04:00
parent f7c3aa6482
commit 68dc7bb4ee
3 changed files with 207 additions and 2 deletions
+2 -1
View File
@@ -135,7 +135,8 @@ const applicationTables = {
.index('by_owner', ['ownerId']) .index('by_owner', ['ownerId'])
.index('by_owner_status', ['ownerId', 'status']) .index('by_owner_status', ['ownerId', 'status'])
.index('by_owner_provider', ['ownerId', 'provider']) .index('by_owner_provider', ['ownerId', 'provider'])
.index('by_upstream', ['provider', 'upstreamOwner', 'upstreamRepo']), .index('by_upstream', ['provider', 'upstreamOwner', 'upstreamRepo'])
.index('by_fork', ['provider', 'forkOwner', 'forkRepo']),
spoonRepositoryStates: defineTable({ spoonRepositoryStates: defineTable({
spoonId: v.id('spoons'), spoonId: v.id('spoons'),
ownerId: v.id('users'), ownerId: v.id('users'),
+40 -1
View File
@@ -1,6 +1,6 @@
import { ConvexError, v } from 'convex/values'; import { ConvexError, v } from 'convex/values';
import type { Doc } from './_generated/dataModel'; import type { Doc, Id } from './_generated/dataModel';
import { import {
internalMutation, internalMutation,
internalQuery, internalQuery,
@@ -221,6 +221,45 @@ export const getOwnedForAction = internalQuery({
}, },
}); });
export const findForRepo = internalQuery({
args: { owner: v.string(), repo: v.string() },
handler: async (
ctx,
{ owner, repo },
): Promise<{ spoonId: Id<'spoons'>; ownerId: Id<'users'> }[]> => {
const [upstreamMatches, forkMatches] = await Promise.all([
ctx.db
.query('spoons')
.withIndex('by_upstream', (q) =>
q
.eq('provider', 'github')
.eq('upstreamOwner', owner)
.eq('upstreamRepo', repo),
)
.collect(),
ctx.db
.query('spoons')
.withIndex('by_fork', (q) =>
q
.eq('provider', 'github')
.eq('forkOwner', owner)
.eq('forkRepo', repo),
)
.collect(),
]);
const seen = new Set<Id<'spoons'>>();
const results: { spoonId: Id<'spoons'>; ownerId: Id<'users'> }[] = [];
for (const spoon of [...upstreamMatches, ...forkMatches]) {
if (spoon.status === 'archived') continue;
if (seen.has(spoon._id)) continue;
seen.add(spoon._id);
results.push({ spoonId: spoon._id, ownerId: spoon.ownerId });
}
return results;
},
});
export const createManual = mutation({ export const createManual = mutation({
args: { args: {
name: v.string(), name: v.string(),
@@ -0,0 +1,165 @@
import { convexTest } from 'convex-test';
import { describe, expect, test } from 'vitest';
import type { Id } from '../../convex/_generated/dataModel.js';
import { internal } from '../../convex/_generated/api.js';
import schema from '../../convex/schema';
const modules = import.meta.glob('../../convex/**/*.*s');
const createUser = async (t: ReturnType<typeof convexTest>, email: string) =>
await t.mutation(async (ctx) => {
return await ctx.db.insert('users', { email, name: email });
});
type SeedSpoon = {
ownerId: Id<'users'>;
upstreamOwner: string;
upstreamRepo: string;
forkOwner?: string;
forkRepo?: string;
status?: 'draft' | 'active' | 'needs_connection' | 'paused' | 'archived';
};
const seedSpoon = async (t: ReturnType<typeof convexTest>, spoon: SeedSpoon) =>
(await t.mutation(async (ctx) => {
const now = Date.now();
return await ctx.db.insert('spoons', {
ownerId: spoon.ownerId,
name: `${spoon.upstreamOwner}/${spoon.upstreamRepo}`,
provider: 'github',
upstreamOwner: spoon.upstreamOwner,
upstreamRepo: spoon.upstreamRepo,
upstreamDefaultBranch: 'main',
upstreamUrl: `https://github.com/${spoon.upstreamOwner}/${spoon.upstreamRepo}`,
forkOwner: spoon.forkOwner,
forkRepo: spoon.forkRepo,
forkUrl: spoon.forkOwner
? `https://github.com/${spoon.forkOwner}/${spoon.forkRepo}`
: undefined,
visibility: 'public',
maintenanceMode: 'watch',
syncCadence: 'daily',
productionRefStrategy: 'default_branch',
status: spoon.status ?? 'active',
createdAt: now,
updatedAt: now,
});
})) as Id<'spoons'>;
describe('spoons.findForRepo', () => {
test('matches on the upstream side', async () => {
const t = convexTest(schema, modules);
const ownerId = (await createUser(t, 'owner@example.com')) as Id<'users'>;
const upstreamSpoon = await seedSpoon(t, {
ownerId,
upstreamOwner: 'up',
upstreamRepo: 'x',
forkOwner: 'me',
forkRepo: 'x-fork',
});
await seedSpoon(t, {
ownerId,
upstreamOwner: 'other',
upstreamRepo: 'y',
forkOwner: 'me',
forkRepo: 'y-fork',
});
const result = await t.query(internal.spoons.findForRepo, {
owner: 'up',
repo: 'x',
});
expect(result).toEqual([{ spoonId: upstreamSpoon, ownerId }]);
});
test('matches on the fork side', async () => {
const t = convexTest(schema, modules);
const ownerId = (await createUser(t, 'owner@example.com')) as Id<'users'>;
await seedSpoon(t, {
ownerId,
upstreamOwner: 'up',
upstreamRepo: 'x',
forkOwner: 'me',
forkRepo: 'x-fork',
});
const forkSpoon = await seedSpoon(t, {
ownerId,
upstreamOwner: 'other',
upstreamRepo: 'y',
forkOwner: 'me',
forkRepo: 'x-fork-2',
});
const result = await t.query(internal.spoons.findForRepo, {
owner: 'me',
repo: 'x-fork-2',
});
expect(result).toEqual([{ spoonId: forkSpoon, ownerId }]);
});
test('returns empty for an unknown repo', async () => {
const t = convexTest(schema, modules);
const ownerId = (await createUser(t, 'owner@example.com')) as Id<'users'>;
await seedSpoon(t, {
ownerId,
upstreamOwner: 'up',
upstreamRepo: 'x',
forkOwner: 'me',
forkRepo: 'x-fork',
});
const result = await t.query(internal.spoons.findForRepo, {
owner: 'nobody',
repo: 'nothing',
});
expect(result).toEqual([]);
});
test('dedups a spoon whose upstream and fork are the same repo', async () => {
const t = convexTest(schema, modules);
const ownerId = (await createUser(t, 'owner@example.com')) as Id<'users'>;
const dupSpoon = await seedSpoon(t, {
ownerId,
upstreamOwner: 'dup',
upstreamRepo: 'z',
forkOwner: 'dup',
forkRepo: 'z',
});
const result = await t.query(internal.spoons.findForRepo, {
owner: 'dup',
repo: 'z',
});
expect(result).toEqual([{ spoonId: dupSpoon, ownerId }]);
});
test('excludes archived spoons', async () => {
const t = convexTest(schema, modules);
const ownerId = (await createUser(t, 'owner@example.com')) as Id<'users'>;
await seedSpoon(t, {
ownerId,
upstreamOwner: 'up',
upstreamRepo: 'x',
forkOwner: 'me',
forkRepo: 'x-fork',
status: 'archived',
});
const upstreamResult = await t.query(internal.spoons.findForRepo, {
owner: 'up',
repo: 'x',
});
const forkResult = await t.query(internal.spoons.findForRepo, {
owner: 'me',
repo: 'x-fork',
});
expect(upstreamResult).toEqual([]);
expect(forkResult).toEqual([]);
});
});