mirror your GitHub repos to tangled.org automatically
1import { sql } from 'drizzle-orm'
2import { installation, repoMapping, sshKey, userIdentity } from '#server/db/schema'
3import { useDb } from '#server/utils/db'
4import { requireSession } from '#server/utils/server-session'
5
6export interface DashboardRepo {
7 id: number
8 githubRepoId: number
9 githubFullName: string
10 tangledRepoDid: string | null
11 tangledFullName: string | null
12 knot: string | null
13 status: string
14 lastError: string | null
15 disabledAt: string | null
16 lastSyncedRefs: Record<string, string>
17 lastSyncedAt: string | null
18 refCount: number
19}
20
21export interface DashboardPayload {
22 did: string
23 handle: string | null
24 installation: {
25 id: number
26 accountLogin: string
27 accountType: string
28 suspendedAt: string | null
29 } | null
30 hasSshKey: boolean
31 sshKey: {
32 publicKey: string
33 createdAt: string
34 rotatedAt: string | null
35 } | null
36 repos: DashboardRepo[]
37}
38
39export default defineEventHandler(async (event): Promise<DashboardPayload> => {
40 const session = await requireSession(event)
41 const db = useDb()
42
43 const [identityRow, installRows, repoRows, sshKeyRows] = await Promise.all([
44 db.select({ did: userIdentity.did, handle: userIdentity.handle })
45 .from(userIdentity)
46 .where(sql`${userIdentity.did} = ${session.did}`)
47 .limit(1),
48 db.select({
49 id: installation.id,
50 accountLogin: installation.accountLogin,
51 accountType: installation.accountType,
52 suspendedAt: installation.suspendedAt,
53 })
54 .from(installation)
55 .where(sql`${installation.id} = ${session.installationId}`)
56 .limit(1),
57 db.select().from(repoMapping)
58 .where(sql`${repoMapping.installationId} = ${session.installationId}`)
59 .orderBy(sql`${repoMapping.githubFullName}`),
60 db.select({
61 publicKey: sshKey.publicKey,
62 createdAt: sshKey.createdAt,
63 rotatedAt: sshKey.rotatedAt,
64 })
65 .from(sshKey)
66 .where(sql`${sshKey.installationId} = ${session.installationId} AND ${sshKey.did} = ${session.did}`)
67 .limit(1),
68 ])
69
70 const installRow = installRows[0] ?? null
71 const sshKeyRow = sshKeyRows[0] ?? null
72
73 const repos: DashboardRepo[] = repoRows.map(row => {
74 // Schema audit prefers no new column for "last push time": `updatedAt`
75 // moves on any mapping mutation (status, errors, disable toggle), so it's
76 // a noisy proxy for "last sync". When at least one ref has synced,
77 // surface `updatedAt` as a best-effort timestamp; when refs is empty, we
78 // have nothing meaningful to show.
79 // eslint-disable-next-line ts/no-unsafe-type-assertion -- jsonb column is typed `unknown`
80 const refs = (row.lastSyncedRefs ?? {}) as Record<string, string>
81 const refKeys = Object.keys(refs)
82 return {
83 id: row.id,
84 githubRepoId: row.githubRepoId,
85 githubFullName: row.githubFullName,
86 tangledRepoDid: row.tangledRepoDid,
87 tangledFullName: row.tangledFullName,
88 knot: row.knot,
89 status: row.status,
90 lastError: row.lastError,
91 disabledAt: row.disabledAt?.toISOString() ?? null,
92 lastSyncedRefs: refs,
93 lastSyncedAt: refKeys.length > 0 && row.updatedAt ? row.updatedAt.toISOString() : null,
94 refCount: refKeys.length,
95 }
96 })
97
98 const installationPayload: DashboardPayload['installation'] = installRow
99 ? {
100 id: installRow.id,
101 accountLogin: installRow.accountLogin,
102 accountType: installRow.accountType,
103 suspendedAt: installRow.suspendedAt?.toISOString() ?? null,
104 }
105 : null
106
107 const sshKeyPayload: DashboardPayload['sshKey'] = sshKeyRow
108 ? {
109 publicKey: sshKeyRow.publicKey,
110 createdAt: sshKeyRow.createdAt.toISOString(),
111 rotatedAt: sshKeyRow.rotatedAt?.toISOString() ?? null,
112 }
113 : null
114
115 return {
116 did: session.did,
117 handle: identityRow[0]?.handle ?? session.handle ?? null,
118 installation: installationPayload,
119 hasSshKey: sshKeyPayload !== null,
120 sshKey: sshKeyPayload,
121 repos,
122 }
123})