import { ConvexError, v } from 'convex/values'; import type { Id } from './_generated/dataModel'; import { internalMutation, internalQuery, mutation, query, } from './_generated/server'; import { getRequiredUserId } from './model'; export const getInstallUrl = query({ args: {}, handler: () => { const slug = process.env.GITHUB_APP_SLUG; if (!slug) return null; return `https://github.com/apps/${slug}/installations/new`; }, }); export const getConnection = query({ args: {}, handler: async (ctx) => { const userId = await getRequiredUserId(ctx); return await ctx.db .query('gitConnections') .withIndex('by_user_provider', (q) => q.eq('userId', userId).eq('provider', 'github'), ) .first(); }, }); export const connectInstallation = mutation({ args: { installationId: v.string() }, handler: async (ctx, { installationId }) => { const userId = await getRequiredUserId(ctx); const trimmedInstallationId = installationId.trim(); if (!trimmedInstallationId) { throw new ConvexError('GitHub installation ID is required.'); } const now = Date.now(); const existing = await ctx.db .query('gitConnections') .withIndex('by_user_provider', (q) => q.eq('userId', userId).eq('provider', 'github'), ) .first(); const patch = { provider: 'github' as const, displayName: `GitHub installation ${trimmedInstallationId}`, installationId: trimmedInstallationId, scopes: [ 'metadata:read', 'administration:write', 'contents:write', 'pull_requests:write', ], status: 'active' as const, updatedAt: now, }; if (existing) { await ctx.db.patch(existing._id, patch); return existing._id; } return await ctx.db.insert('gitConnections', { userId, ...patch, connectedAt: now, }); }, }); export const getConnectionForUser = internalQuery({ args: { userId: v.id('users') }, handler: async (ctx, { userId }) => { return await ctx.db .query('gitConnections') .withIndex('by_user_provider', (q) => q.eq('userId', userId).eq('provider', 'github'), ) .first(); }, }); export const upsertConnectionForUser = internalMutation({ args: { userId: v.id('users'), providerAccountId: v.optional(v.string()), displayName: v.string(), username: v.optional(v.string()), avatarUrl: v.optional(v.string()), installationId: v.string(), }, handler: async (ctx, args) => { const now = Date.now(); const existing = await ctx.db .query('gitConnections') .withIndex('by_user_provider', (q) => q.eq('userId', args.userId).eq('provider', 'github'), ) .first(); const patch = { providerAccountId: args.providerAccountId, displayName: args.displayName, username: args.username, avatarUrl: args.avatarUrl, installationId: args.installationId, scopes: [ 'metadata:read', 'administration:write', 'contents:write', 'pull_requests:write', 'checks:write', 'statuses:write', 'issues:write', ], status: 'active' as const, updatedAt: now, }; if (existing) { await ctx.db.patch(existing._id, patch); return existing._id; } return await ctx.db.insert('gitConnections', { userId: args.userId, provider: 'github', ...patch, connectedAt: now, }); }, }); export const createForkSpoonRecord = internalMutation({ args: { ownerId: v.id('users'), name: v.string(), description: v.optional(v.string()), upstreamOwner: v.string(), upstreamRepo: v.string(), upstreamDefaultBranch: v.string(), upstreamUrl: v.string(), forkOwner: v.string(), forkRepo: v.string(), forkDefaultBranch: v.string(), forkUrl: v.string(), visibility: v.union( v.literal('public'), v.literal('private'), v.literal('internal'), v.literal('unknown'), ), connectionId: v.optional(v.id('gitConnections')), }, handler: async (ctx, args): Promise> => { const now = Date.now(); const spoonId = await ctx.db.insert('spoons', { ownerId: args.ownerId, name: args.name, description: args.description, provider: 'github', upstreamOwner: args.upstreamOwner, upstreamRepo: args.upstreamRepo, upstreamDefaultBranch: args.upstreamDefaultBranch, upstreamUrl: args.upstreamUrl, forkOwner: args.forkOwner, forkRepo: args.forkRepo, forkDefaultBranch: args.forkDefaultBranch, forkUrl: args.forkUrl, visibility: args.visibility, maintenanceMode: 'watch', syncCadence: 'daily', productionRefStrategy: 'default_branch', status: 'active', syncStatus: 'unknown', connectionId: args.connectionId, createdAt: now, updatedAt: now, }); await ctx.db.insert('spoonSettings', { spoonId, ownerId: args.ownerId, autoRefreshEnabled: true, autoReviewEnabled: true, autoSyncEnabled: false, requireAiLowRiskForSync: true, requireCleanCompareForSync: true, ignoredFilePatterns: [], importantFilePatterns: [], createdAt: now, updatedAt: now, }); await ctx.db.insert('syncRuns', { spoonId, ownerId: args.ownerId, kind: 'manual_check', status: 'clean', summary: `Created GitHub fork ${args.forkOwner}/${args.forkRepo} from ${args.upstreamOwner}/${args.upstreamRepo}.`, createdAt: now, updatedAt: now, }); return spoonId; }, });