215 lines
5.6 KiB
TypeScript
215 lines
5.6 KiB
TypeScript
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<Id<'spoons'>> => {
|
|
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;
|
|
},
|
|
});
|