Skip to content

Commit b5dc898

Browse files
brendan-kellammsukkari
authored andcommitted
Connection management (#183)
1 parent 5eb8656 commit b5dc898

43 files changed

Lines changed: 2218 additions & 475 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/backend/src/connectionManager.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,14 @@ export class ConnectionManager implements IConnectionManager {
9292
}
9393
})();
9494

95+
// Filter out any duplicates by external_id and external_codeHostUrl.
96+
repoData.filter((repo, index, self) => {
97+
return index === self.findIndex(r =>
98+
r.external_id === repo.external_id &&
99+
r.external_codeHostUrl === repo.external_codeHostUrl
100+
);
101+
})
102+
95103
// @note: to handle orphaned Repos we delete all RepoToConnection records for this connection,
96104
// and then recreate them when we upsert the repos. For example, if a repo is no-longer
97105
// captured by the connection's config (e.g., it was deleted, marked archived, etc.), it won't

packages/backend/src/github.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ export type OctokitRepository = {
2424
topics?: string[],
2525
// @note: this is expressed in kilobytes.
2626
size?: number,
27+
owner: {
28+
avatar_url: string,
29+
}
2730
}
2831

2932
export const getGitHubReposFromConfig = async (config: GithubConnectionConfig, orgId: number, db: PrismaClient, signal: AbortSignal) => {
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
Warnings:
3+
4+
- Added the required column `name` to the `Connection` table without a default value. This is not possible if the table is not empty.
5+
6+
*/
7+
-- AlterTable
8+
ALTER TABLE "Connection" ADD COLUMN "name" TEXT NOT NULL;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
Warnings:
3+
4+
- Added the required column `connectionType` to the `Connection` table without a default value. This is not possible if the table is not empty.
5+
6+
*/
7+
-- AlterTable
8+
ALTER TABLE "Connection" ADD COLUMN "connectionType" TEXT NOT NULL;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "Repo" ADD COLUMN "imageUrl" TEXT;

packages/db/prisma/schema.prisma

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,17 @@ enum ConnectionSyncStatus {
2727
}
2828

2929
model Repo {
30-
id Int @id @default(autoincrement())
31-
name String
32-
createdAt DateTime @default(now())
33-
updatedAt DateTime @updatedAt
34-
indexedAt DateTime?
35-
isFork Boolean
36-
isArchived Boolean
37-
metadata Json
38-
cloneUrl String
39-
connections RepoToConnection[]
40-
30+
id Int @id @default(autoincrement())
31+
name String
32+
createdAt DateTime @default(now())
33+
updatedAt DateTime @updatedAt
34+
indexedAt DateTime?
35+
isFork Boolean
36+
isArchived Boolean
37+
metadata Json
38+
cloneUrl String
39+
connections RepoToConnection[]
40+
imageUrl String?
4141
repoIndexingStatus RepoIndexingStatus @default(NEW)
4242
4343
// The id of the repo in the external service
@@ -54,15 +54,18 @@ model Repo {
5454
}
5555

5656
model Connection {
57-
id Int @id @default(autoincrement())
58-
config Json
59-
createdAt DateTime @default(now())
60-
updatedAt DateTime @updatedAt
61-
syncedAt DateTime?
62-
repos RepoToConnection[]
63-
57+
id Int @id @default(autoincrement())
58+
name String
59+
config Json
60+
createdAt DateTime @default(now())
61+
updatedAt DateTime @updatedAt
62+
syncedAt DateTime?
63+
repos RepoToConnection[]
6464
syncStatus ConnectionSyncStatus @default(SYNC_NEEDED)
6565
66+
// The type of connection (e.g., github, gitlab, etc.)
67+
connectionType String
68+
6669
// The organization that owns this connection
6770
org Org @relation(fields: [orgId], references: [id], onDelete: Cascade)
6871
orgId Int
@@ -71,10 +74,10 @@ model Connection {
7174
model RepoToConnection {
7275
addedAt DateTime @default(now())
7376
74-
connection Connection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
77+
connection Connection @relation(fields: [connectionId], references: [id], onDelete: Cascade)
7578
connectionId Int
7679
77-
repo Repo @relation(fields: [repoId], references: [id], onDelete: Cascade)
80+
repo Repo @relation(fields: [repoId], references: [id], onDelete: Cascade)
7881
repoId Int
7982
8083
@@id([connectionId, repoId])
@@ -113,12 +116,12 @@ model UserToOrg {
113116
}
114117

115118
model Secret {
116-
orgId Int
117-
key String
118-
encryptedValue String
119-
iv String
119+
orgId Int
120+
key String
121+
encryptedValue String
122+
iv String
120123
121-
createdAt DateTime @default(now())
124+
createdAt DateTime @default(now())
122125
123126
org Org @relation(fields: [orgId], references: [id], onDelete: Cascade)
124127

packages/web/next.config.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ const nextConfig = {
2222
// This is required to support PostHog trailing slash API requests
2323
skipTrailingSlashRedirect: true,
2424

25+
images: {
26+
remotePatterns: [
27+
{
28+
protocol: 'https',
29+
hostname: 'avatars.githubusercontent.com',
30+
}
31+
]
32+
}
33+
2534
// @nocheckin: This was interfering with the the `matcher` regex in middleware.ts,
2635
// causing regular expressions parsing errors when making a request. It's unclear
2736
// why exactly this was happening, but it's likely due to a bad replacement happening

packages/web/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"@hookform/resolvers": "^3.9.0",
4141
"@iconify/react": "^5.1.0",
4242
"@iizukak/codemirror-lang-wgsl": "^0.3.0",
43+
"@radix-ui/react-alert-dialog": "^1.1.5",
4344
"@radix-ui/react-avatar": "^1.1.2",
4445
"@radix-ui/react-dialog": "^1.1.4",
4546
"@radix-ui/react-dropdown-menu": "^2.1.1",
@@ -48,7 +49,8 @@
4849
"@radix-ui/react-navigation-menu": "^1.2.0",
4950
"@radix-ui/react-scroll-area": "^1.1.0",
5051
"@radix-ui/react-separator": "^1.1.0",
51-
"@radix-ui/react-slot": "^1.1.0",
52+
"@radix-ui/react-slot": "^1.1.1",
53+
"@radix-ui/react-tabs": "^1.1.2",
5254
"@radix-ui/react-toast": "^1.2.2",
5355
"@radix-ui/react-toggle": "^1.1.0",
5456
"@radix-ui/react-tooltip": "^1.1.4",
@@ -59,8 +61,8 @@
5961
"@replit/codemirror-vim": "^6.2.1",
6062
"@shopify/lang-jsonc": "^1.0.0",
6163
"@sourcebot/crypto": "^0.1.0",
62-
"@sourcebot/schemas": "^0.1.0",
6364
"@sourcebot/db": "^0.1.0",
65+
"@sourcebot/schemas": "^0.1.0",
6466
"@ssddanbrown/codemirror-lang-twig": "^1.0.0",
6567
"@tanstack/react-query": "^5.53.3",
6668
"@tanstack/react-table": "^8.20.5",

packages/web/src/actions.ts

Lines changed: 137 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import { StatusCodes } from "http-status-codes";
88
import { ErrorCode } from "@/lib/errorCodes";
99
import { isServiceError } from "@/lib/utils";
1010
import { githubSchema } from "@sourcebot/schemas/v3/github.schema";
11+
import { gitlabSchema } from "@sourcebot/schemas/v3/gitlab.schema";
12+
import { ConnectionConfig } from "@sourcebot/schemas/v3/connection.type";
1113
import { encrypt } from "@sourcebot/crypto"
14+
import { getConnection } from "./data/connection";
15+
import { Prisma } from "@sourcebot/db";
1216

1317
const ajv = new Ajv({
1418
validateFormats: false,
@@ -141,25 +145,152 @@ export const switchActiveOrg = async (orgId: number): Promise<{ id: number } | S
141145
}
142146
}
143147

144-
export const createConnection = async (config: string): Promise<{ id: number } | ServiceError> => {
148+
export const createConnection = async (name: string, type: string, connectionConfig: string): Promise<{ id: number } | ServiceError> => {
145149
const orgId = await getCurrentUserOrg();
146150
if (isServiceError(orgId)) {
147151
return orgId;
148152
}
149153

150-
let parsedConfig;
154+
const parsedConfig = parseConnectionConfig(type, connectionConfig);
155+
if (isServiceError(parsedConfig)) {
156+
return parsedConfig;
157+
}
158+
159+
const connection = await prisma.connection.create({
160+
data: {
161+
orgId,
162+
name,
163+
config: parsedConfig as unknown as Prisma.InputJsonValue,
164+
connectionType: type,
165+
}
166+
});
167+
168+
return {
169+
id: connection.id,
170+
}
171+
}
172+
173+
export const updateConnectionDisplayName = async (connectionId: number, name: string): Promise<{ success: boolean } | ServiceError> => {
174+
const orgId = await getCurrentUserOrg();
175+
if (isServiceError(orgId)) {
176+
return orgId;
177+
}
178+
179+
const connection = await getConnection(connectionId, orgId);
180+
if (!connection) {
181+
return notFound();
182+
}
183+
184+
await prisma.connection.update({
185+
where: {
186+
id: connectionId,
187+
orgId,
188+
},
189+
data: {
190+
name,
191+
}
192+
});
193+
194+
return {
195+
success: true,
196+
}
197+
}
198+
199+
export const updateConnectionConfigAndScheduleSync = async (connectionId: number, config: string): Promise<{ success: boolean } | ServiceError> => {
200+
const orgId = await getCurrentUserOrg();
201+
if (isServiceError(orgId)) {
202+
return orgId;
203+
}
204+
205+
const connection = await getConnection(connectionId, orgId);
206+
if (!connection) {
207+
return notFound();
208+
}
209+
210+
const parsedConfig = parseConnectionConfig(connection.connectionType, config);
211+
if (isServiceError(parsedConfig)) {
212+
return parsedConfig;
213+
}
214+
215+
if (connection.syncStatus === "SYNC_NEEDED" ||
216+
connection.syncStatus === "IN_SYNC_QUEUE" ||
217+
connection.syncStatus === "SYNCING") {
218+
return {
219+
statusCode: StatusCodes.BAD_REQUEST,
220+
errorCode: ErrorCode.CONNECTION_SYNC_ALREADY_SCHEDULED,
221+
message: "Connection is already syncing. Please wait for the sync to complete before updating the connection.",
222+
} satisfies ServiceError;
223+
}
224+
225+
await prisma.connection.update({
226+
where: {
227+
id: connectionId,
228+
orgId,
229+
},
230+
data: {
231+
config: parsedConfig as unknown as Prisma.InputJsonValue,
232+
syncStatus: "SYNC_NEEDED",
233+
}
234+
});
235+
236+
return {
237+
success: true,
238+
}
239+
}
240+
241+
export const deleteConnection = async (connectionId: number): Promise<{ success: boolean } | ServiceError> => {
242+
const orgId = await getCurrentUserOrg();
243+
if (isServiceError(orgId)) {
244+
return orgId;
245+
}
246+
247+
const connection = await getConnection(connectionId, orgId);
248+
if (!connection) {
249+
return notFound();
250+
}
251+
252+
await prisma.connection.delete({
253+
where: {
254+
id: connectionId,
255+
orgId,
256+
}
257+
});
258+
259+
return {
260+
success: true,
261+
}
262+
}
263+
264+
const parseConnectionConfig = (connectionType: string, config: string) => {
265+
let parsedConfig: ConnectionConfig;
151266
try {
152267
parsedConfig = JSON.parse(config);
153-
} catch {
268+
} catch (_e) {
154269
return {
155270
statusCode: StatusCodes.BAD_REQUEST,
156271
errorCode: ErrorCode.INVALID_REQUEST_BODY,
157272
message: "config must be a valid JSON object."
158273
} satisfies ServiceError;
159274
}
160275

161-
// @todo: we will need to validate the config against different schemas based on the type of connection.
162-
const isValidConfig = ajv.validate(githubSchema, parsedConfig);
276+
const schema = (() => {
277+
switch (connectionType) {
278+
case "github":
279+
return githubSchema;
280+
case "gitlab":
281+
return gitlabSchema;
282+
}
283+
})();
284+
285+
if (!schema) {
286+
return {
287+
statusCode: StatusCodes.BAD_REQUEST,
288+
errorCode: ErrorCode.INVALID_REQUEST_BODY,
289+
message: "invalid connection type",
290+
} satisfies ServiceError;
291+
}
292+
293+
const isValidConfig = ajv.validate(schema, parsedConfig);
163294
if (!isValidConfig) {
164295
return {
165296
statusCode: StatusCodes.BAD_REQUEST,
@@ -168,14 +299,5 @@ export const createConnection = async (config: string): Promise<{ id: number } |
168299
} satisfies ServiceError;
169300
}
170301

171-
const connection = await prisma.connection.create({
172-
data: {
173-
orgId: orgId,
174-
config: parsedConfig,
175-
}
176-
});
177-
178-
return {
179-
id: connection.id,
180-
}
302+
return parsedConfig;
181303
}

packages/web/src/app/components/navigationMenu.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ export const NavigationMenu = async () => {
6363
</NavigationMenuLink>
6464
</Link>
6565
</NavigationMenuItem>
66+
<NavigationMenuItem>
67+
<Link href="/connections" legacyBehavior passHref>
68+
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
69+
Connections
70+
</NavigationMenuLink>
71+
</Link>
72+
</NavigationMenuItem>
6673
</NavigationMenuList>
6774
</NavigationMenuBase>
6875
</div>

0 commit comments

Comments
 (0)