Files
spoon/packages/backend/convex/schema.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

532 lines
16 KiB
TypeScript

import { authTables } from '@convex-dev/auth/server';
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';
const applicationTables = {
/*
* Below is the users table definition from authTables
* You can add additional fields here. You can also remove
* the users table here & create a 'profiles' table if you
* prefer to keep auth data separate from application data.
*/
users: defineTable({
name: v.optional(v.string()),
image: v.optional(v.string()),
email: v.optional(v.string()),
emailVerificationTime: v.optional(v.number()),
phone: v.optional(v.string()),
phoneVerificationTime: v.optional(v.number()),
isAnonymous: v.optional(v.boolean()),
/* Fields below here are custom & not defined in authTables */
isAdmin: v.optional(v.boolean()),
role: v.optional(v.union(v.literal('owner'), v.literal('member'))),
lastSeenAt: v.optional(v.number()),
themePreference: v.optional(
v.union(v.literal('light'), v.literal('dark'), v.literal('system')),
),
})
.index('email', ['email'])
.index('phone', ['phone'])
/* Indexes below here are custom & not defined in authTables */
.index('name', ['name']),
gitConnections: defineTable({
userId: v.id('users'),
provider: v.union(
v.literal('github'),
v.literal('gitea'),
v.literal('gitlab'),
v.literal('other'),
),
providerAccountId: v.optional(v.string()),
displayName: v.string(),
username: v.optional(v.string()),
avatarUrl: v.optional(v.string()),
installationId: v.optional(v.string()),
scopes: v.optional(v.array(v.string())),
status: v.union(
v.literal('active'),
v.literal('needs_reauth'),
v.literal('revoked'),
),
connectedAt: v.number(),
updatedAt: v.number(),
})
.index('by_user', ['userId'])
.index('by_user_provider', ['userId', 'provider'])
.index('by_status', ['status']),
spoons: defineTable({
ownerId: v.id('users'),
name: v.string(),
description: v.optional(v.string()),
provider: v.union(
v.literal('github'),
v.literal('gitea'),
v.literal('gitlab'),
v.literal('other'),
),
upstreamOwner: v.string(),
upstreamRepo: v.string(),
upstreamDefaultBranch: v.string(),
upstreamUrl: v.string(),
forkOwner: v.optional(v.string()),
forkRepo: v.optional(v.string()),
forkDefaultBranch: v.optional(v.string()),
forkUrl: v.optional(v.string()),
visibility: v.union(
v.literal('public'),
v.literal('private'),
v.literal('internal'),
v.literal('unknown'),
),
maintenanceMode: v.union(
v.literal('watch'),
v.literal('auto_pr'),
v.literal('paused'),
),
syncCadence: v.union(
v.literal('daily'),
v.literal('weekly'),
v.literal('manual'),
),
productionRefStrategy: v.union(
v.literal('default_branch'),
v.literal('latest_release'),
v.literal('tag_pattern'),
),
tagPattern: v.optional(v.string()),
status: v.union(
v.literal('draft'),
v.literal('active'),
v.literal('needs_connection'),
v.literal('paused'),
v.literal('archived'),
),
lastCheckedAt: v.optional(v.number()),
lastUpstreamCommit: v.optional(v.string()),
lastForkCommit: v.optional(v.string()),
connectionId: v.optional(v.id('gitConnections')),
githubInstallationId: v.optional(v.string()),
githubRepositoryId: v.optional(v.number()),
upstreamRepositoryId: v.optional(v.number()),
syncStatus: v.optional(
v.union(
v.literal('unknown'),
v.literal('up_to_date'),
v.literal('behind'),
v.literal('ahead'),
v.literal('diverged'),
v.literal('checking'),
v.literal('conflict'),
v.literal('error'),
),
),
upstreamAheadBy: v.optional(v.number()),
forkAheadBy: v.optional(v.number()),
lastMergeBaseCommit: v.optional(v.string()),
lastSyncRunId: v.optional(v.id('syncRuns')),
lastAiReviewId: v.optional(v.id('aiReviews')),
lastGithubRefreshAt: v.optional(v.number()),
lastSuccessfulRefreshAt: v.optional(v.number()),
lastError: v.optional(v.string()),
createdAt: v.number(),
updatedAt: v.number(),
})
.index('by_owner', ['ownerId'])
.index('by_owner_status', ['ownerId', 'status'])
.index('by_owner_provider', ['ownerId', 'provider'])
.index('by_upstream', ['provider', 'upstreamOwner', 'upstreamRepo']),
spoonRepositoryStates: defineTable({
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: v.union(
v.literal('up_to_date'),
v.literal('behind'),
v.literal('ahead'),
v.literal('diverged'),
v.literal('unknown'),
),
openForkPullRequestCount: v.number(),
openUpstreamPullRequestCount: v.number(),
lastCommitAt: v.optional(v.number()),
rawCompareUrl: v.optional(v.string()),
refreshedAt: v.number(),
createdAt: v.number(),
updatedAt: v.number(),
})
.index('by_spoon', ['spoonId'])
.index('by_owner', ['ownerId'])
.index('by_status', ['ownerId', 'status']),
spoonCommits: defineTable({
spoonId: v.id('spoons'),
ownerId: v.id('users'),
sha: v.string(),
side: v.union(v.literal('upstream'), v.literal('fork')),
message: v.string(),
authorName: v.optional(v.string()),
authorEmail: v.optional(v.string()),
authorLogin: v.optional(v.string()),
committedAt: v.optional(v.number()),
htmlUrl: v.optional(v.string()),
filesChanged: v.optional(v.number()),
additions: v.optional(v.number()),
deletions: v.optional(v.number()),
createdAt: v.number(),
updatedAt: v.number(),
})
.index('by_spoon_side', ['spoonId', 'side'])
.index('by_owner', ['ownerId'])
.index('by_sha', ['spoonId', 'sha'])
.index('by_committed', ['spoonId', 'committedAt']),
spoonPullRequests: defineTable({
spoonId: v.id('spoons'),
ownerId: v.id('users'),
githubId: v.number(),
number: v.number(),
repoFullName: v.string(),
scope: v.union(
v.literal('fork'),
v.literal('upstream'),
v.literal('from_fork_to_upstream'),
),
title: v.string(),
state: v.union(v.literal('open'), v.literal('closed'), v.literal('merged')),
draft: v.boolean(),
authorLogin: v.optional(v.string()),
baseRef: v.string(),
headRef: v.string(),
headRepoFullName: v.optional(v.string()),
htmlUrl: v.string(),
createdAtGithub: v.optional(v.number()),
updatedAtGithub: v.optional(v.number()),
mergedAtGithub: v.optional(v.number()),
createdAt: v.number(),
updatedAt: v.number(),
})
.index('by_spoon', ['spoonId'])
.index('by_spoon_scope', ['spoonId', 'scope'])
.index('by_owner', ['ownerId'])
.index('by_github_id', ['githubId'])
.index('by_state', ['spoonId', 'state']),
syncRuns: defineTable({
spoonId: v.id('spoons'),
ownerId: v.id('users'),
kind: v.union(
v.literal('scheduled_check'),
v.literal('manual_check'),
v.literal('upstream_update'),
v.literal('merge_attempt'),
v.literal('ai_review'),
),
status: v.union(
v.literal('queued'),
v.literal('running'),
v.literal('clean'),
v.literal('conflict'),
v.literal('needs_review'),
v.literal('failed'),
v.literal('merged'),
),
upstreamFrom: v.optional(v.string()),
upstreamTo: v.optional(v.string()),
summary: v.optional(v.string()),
aiAssessment: v.optional(v.string()),
mergeRequestUrl: v.optional(v.string()),
error: v.optional(v.string()),
createdAt: v.number(),
updatedAt: v.number(),
})
.index('by_owner', ['ownerId'])
.index('by_spoon', ['spoonId'])
.index('by_owner_status', ['ownerId', 'status'])
.index('by_created', ['createdAt']),
aiReviews: defineTable({
spoonId: v.id('spoons'),
ownerId: v.id('users'),
syncRunId: v.optional(v.id('syncRuns')),
model: v.string(),
status: v.union(
v.literal('queued'),
v.literal('running'),
v.literal('completed'),
v.literal('failed'),
),
reviewType: v.union(
v.literal('upstream_update'),
v.literal('manual_prompt'),
v.literal('merge_safety'),
),
inputSummary: v.string(),
outputSummary: v.optional(v.string()),
risk: v.union(
v.literal('unknown'),
v.literal('low'),
v.literal('medium'),
v.literal('high'),
),
compatible: v.boolean(),
requiresHumanReview: v.boolean(),
recommendedAction: v.union(
v.literal('sync'),
v.literal('open_review_pr'),
v.literal('manual_review'),
v.literal('do_not_sync'),
v.literal('unknown'),
),
potentialConflicts: v.optional(v.array(v.string())),
importantFiles: v.optional(v.array(v.string())),
reasoningSummary: v.optional(v.string()),
error: v.optional(v.string()),
createdAt: v.number(),
updatedAt: v.number(),
completedAt: v.optional(v.number()),
})
.index('by_spoon', ['spoonId'])
.index('by_owner', ['ownerId'])
.index('by_status', ['ownerId', 'status'])
.index('by_sync_run', ['syncRunId'])
.index('by_created', ['createdAt']),
spoonSettings: defineTable({
spoonId: v.id('spoons'),
ownerId: v.id('users'),
autoRefreshEnabled: v.boolean(),
autoReviewEnabled: v.boolean(),
autoSyncEnabled: v.boolean(),
requireAiLowRiskForSync: v.boolean(),
requireCleanCompareForSync: v.boolean(),
ignoredFilePatterns: v.optional(v.array(v.string())),
importantFilePatterns: v.optional(v.array(v.string())),
createdAt: v.number(),
updatedAt: v.number(),
})
.index('by_spoon', ['spoonId'])
.index('by_owner', ['ownerId']),
spoonRemotes: defineTable({
spoonId: v.id('spoons'),
ownerId: v.id('users'),
label: v.string(),
url: v.string(),
remoteName: v.optional(v.string()),
createdAt: v.number(),
updatedAt: v.number(),
})
.index('by_spoon', ['spoonId'])
.index('by_owner', ['ownerId']),
userAiSettings: defineTable({
userId: v.id('users'),
provider: v.literal('openai'),
encryptedApiKey: v.optional(v.string()),
apiKeyPreview: v.optional(v.string()),
model: v.string(),
reasoningEffort: v.union(
v.literal('none'),
v.literal('minimal'),
v.literal('low'),
v.literal('medium'),
v.literal('high'),
v.literal('xhigh'),
),
createdAt: v.number(),
updatedAt: v.number(),
})
.index('by_user', ['userId'])
.index('by_user_provider', ['userId', 'provider']),
agentRequests: defineTable({
spoonId: v.id('spoons'),
ownerId: v.id('users'),
agentJobId: v.optional(v.id('agentJobs')),
prompt: v.string(),
requestType: v.optional(
v.union(
v.literal('manual_prompt'),
v.literal('upstream_review'),
v.literal('future_code_change'),
),
),
priority: v.optional(
v.union(v.literal('low'), v.literal('normal'), v.literal('high')),
),
source: v.optional(v.union(v.literal('user'), v.literal('system'))),
status: v.union(
v.literal('draft'),
v.literal('queued'),
v.literal('running'),
v.literal('changes_ready'),
v.literal('merge_request_opened'),
v.literal('failed'),
v.literal('cancelled'),
),
targetBranch: v.optional(v.string()),
selectedSecretIds: v.optional(v.array(v.id('spoonSecrets'))),
baseBranch: v.optional(v.string()),
requestedBranchName: v.optional(v.string()),
mergeRequestUrl: v.optional(v.string()),
summary: v.optional(v.string()),
error: v.optional(v.string()),
createdAt: v.number(),
updatedAt: v.number(),
})
.index('by_owner', ['ownerId'])
.index('by_spoon', ['spoonId'])
.index('by_owner_status', ['ownerId', 'status'])
.index('by_created', ['createdAt']),
spoonSecrets: defineTable({
spoonId: v.id('spoons'),
ownerId: v.id('users'),
name: v.string(),
encryptedValue: v.string(),
valuePreview: v.optional(v.string()),
description: v.optional(v.string()),
createdAt: v.number(),
updatedAt: v.number(),
})
.index('by_spoon', ['spoonId'])
.index('by_owner', ['ownerId'])
.index('by_name', ['spoonId', 'name']),
spoonAgentSettings: defineTable({
spoonId: v.id('spoons'),
ownerId: v.id('users'),
enabled: v.boolean(),
defaultBaseBranch: v.optional(v.string()),
branchPrefix: v.string(),
installCommand: v.optional(v.string()),
checkCommand: v.optional(v.string()),
testCommand: v.optional(v.string()),
agentModel: v.string(),
reasoningEffort: v.union(
v.literal('none'),
v.literal('minimal'),
v.literal('low'),
v.literal('medium'),
v.literal('high'),
v.literal('xhigh'),
),
maxJobDurationMs: v.number(),
maxOutputBytes: v.number(),
createdAt: v.number(),
updatedAt: v.number(),
})
.index('by_spoon', ['spoonId'])
.index('by_owner', ['ownerId']),
agentJobs: defineTable({
spoonId: v.id('spoons'),
ownerId: v.id('users'),
agentRequestId: v.id('agentRequests'),
status: v.union(
v.literal('queued'),
v.literal('claimed'),
v.literal('preparing'),
v.literal('running'),
v.literal('checks_running'),
v.literal('changes_ready'),
v.literal('draft_pr_opened'),
v.literal('failed'),
v.literal('cancelled'),
v.literal('timed_out'),
),
prompt: v.string(),
baseBranch: v.string(),
workBranch: v.string(),
githubInstallationId: v.optional(v.string()),
forkOwner: v.string(),
forkRepo: v.string(),
forkUrl: v.string(),
upstreamOwner: v.string(),
upstreamRepo: v.string(),
selectedSecretIds: v.array(v.id('spoonSecrets')),
model: v.string(),
reasoningEffort: v.union(
v.literal('none'),
v.literal('minimal'),
v.literal('low'),
v.literal('medium'),
v.literal('high'),
v.literal('xhigh'),
),
commitSha: v.optional(v.string()),
pullRequestUrl: v.optional(v.string()),
pullRequestNumber: v.optional(v.number()),
summary: v.optional(v.string()),
error: v.optional(v.string()),
claimedBy: v.optional(v.string()),
claimedAt: v.optional(v.number()),
startedAt: v.optional(v.number()),
completedAt: v.optional(v.number()),
createdAt: v.number(),
updatedAt: v.number(),
})
.index('by_owner', ['ownerId'])
.index('by_spoon', ['spoonId'])
.index('by_request', ['agentRequestId'])
.index('by_status', ['status'])
.index('by_claim', ['status', 'createdAt']),
agentJobEvents: defineTable({
jobId: v.id('agentJobs'),
spoonId: v.id('spoons'),
ownerId: v.id('users'),
level: v.union(
v.literal('debug'),
v.literal('info'),
v.literal('warn'),
v.literal('error'),
),
phase: v.union(
v.literal('queued'),
v.literal('clone'),
v.literal('plan'),
v.literal('edit'),
v.literal('install'),
v.literal('check'),
v.literal('test'),
v.literal('commit'),
v.literal('push'),
v.literal('pr'),
v.literal('cleanup'),
),
message: v.string(),
metadata: v.optional(v.string()),
createdAt: v.number(),
})
.index('by_job', ['jobId'])
.index('by_spoon', ['spoonId'])
.index('by_owner', ['ownerId']),
agentJobArtifacts: defineTable({
jobId: v.id('agentJobs'),
spoonId: v.id('spoons'),
ownerId: v.id('users'),
kind: v.union(
v.literal('plan'),
v.literal('diff'),
v.literal('test_output'),
v.literal('summary'),
v.literal('error'),
v.literal('pr_body'),
),
title: v.string(),
content: v.string(),
contentType: v.union(
v.literal('text/markdown'),
v.literal('text/plain'),
v.literal('application/json'),
v.literal('text/x-diff'),
),
createdAt: v.number(),
})
.index('by_job', ['jobId'])
.index('by_spoon', ['spoonId'])
.index('by_owner', ['ownerId']),
};
export default defineSchema({
...authTables,
...applicationTables,
});