Skip to content

Commit 5fde901

Browse files
chore(worker): Refactor permission syncing join table to be between Account <> Repo (#600)
1 parent 449c76f commit 5fde901

File tree

10 files changed

+255
-182
lines changed

10 files changed

+255
-182
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
- Fixed issue with an unbounded `Promise.allSettled(...)` when retrieving details from the GitHub API about a large number of repositories (or orgs or users). [#591](https://github.com/sourcebot-dev/sourcebot/pull/591)
2020
- Fixed resource exhaustion (EAGAIN errors) when syncing generic-git-host connections with thousands of repositories. [#593](https://github.com/sourcebot-dev/sourcebot/pull/593)
2121

22-
## Removed
22+
### Removed
2323
- Removed built-in secret manager. [#592](https://github.com/sourcebot-dev/sourcebot/pull/592)
2424

25+
### Changed
26+
- Changed internal representation of how repo permissions are represented in the database. [#600](https://github.com/sourcebot-dev/sourcebot/pull/600)
27+
2528
## [4.8.1] - 2025-10-29
2629

2730
### Fixed

packages/backend/src/ee/userPermissionSyncer.ts renamed to packages/backend/src/ee/accountPermissionSyncer.ts

Lines changed: 102 additions & 105 deletions
Large diffs are not rendered by default.

packages/backend/src/ee/repoPermissionSyncer.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ export class RepoPermissionSyncer {
168168
throw new Error(`No credentials found for repo ${id}`);
169169
}
170170

171-
const userIds = await (async () => {
171+
const accountIds = await (async () => {
172172
if (repo.external_codeHostType === 'github') {
173173
const isGitHubCloud = credentials.hostUrl ? new URL(credentials.hostUrl).hostname === GITHUB_CLOUD_HOSTNAME : false;
174174
const { octokit } = await createOctokitFromToken({
@@ -195,12 +195,9 @@ export class RepoPermissionSyncer {
195195
in: githubUserIds,
196196
}
197197
},
198-
select: {
199-
userId: true,
200-
},
201198
});
202199

203-
return accounts.map(account => account.userId);
200+
return accounts.map(account => account.id);
204201
} else if (repo.external_codeHostType === 'gitlab') {
205202
const api = await createGitLabFromPersonalAccessToken({
206203
token: credentials.token,
@@ -222,12 +219,9 @@ export class RepoPermissionSyncer {
222219
in: gitlabUserIds,
223220
}
224221
},
225-
select: {
226-
userId: true,
227-
},
228222
});
229223

230-
return accounts.map(account => account.userId);
224+
return accounts.map(account => account.id);
231225
}
232226

233227
return [];
@@ -239,14 +233,14 @@ export class RepoPermissionSyncer {
239233
id: repo.id,
240234
},
241235
data: {
242-
permittedUsers: {
236+
permittedAccounts: {
243237
deleteMany: {},
244238
}
245239
}
246240
}),
247-
this.db.userToRepoPermission.createMany({
248-
data: userIds.map(userId => ({
249-
userId,
241+
this.db.accountToRepoPermission.createMany({
242+
data: accountIds.map(accountId => ({
243+
accountId,
250244
repoId: repo.id,
251245
})),
252246
})

packages/backend/src/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { ConnectionManager } from './connectionManager.js';
1111
import { INDEX_CACHE_DIR, REPOS_CACHE_DIR } from './constants.js';
1212
import { GithubAppManager } from "./ee/githubAppManager.js";
1313
import { RepoPermissionSyncer } from './ee/repoPermissionSyncer.js';
14-
import { UserPermissionSyncer } from "./ee/userPermissionSyncer.js";
14+
import { AccountPermissionSyncer } from "./ee/accountPermissionSyncer.js";
1515
import { env } from "./env.js";
1616
import { PromClient } from './promClient.js';
1717
import { RepoIndexManager } from "./repoIndexManager.js";
@@ -52,7 +52,7 @@ if (hasEntitlement('github-app')) {
5252

5353
const connectionManager = new ConnectionManager(prisma, settings, redis, promClient);
5454
const repoPermissionSyncer = new RepoPermissionSyncer(prisma, settings, redis);
55-
const userPermissionSyncer = new UserPermissionSyncer(prisma, settings, redis);
55+
const accountPermissionSyncer = new AccountPermissionSyncer(prisma, settings, redis);
5656
const repoIndexManager = new RepoIndexManager(prisma, settings, redis, promClient);
5757
const configManager = new ConfigManager(prisma, connectionManager, env.CONFIG_PATH);
5858

@@ -65,7 +65,7 @@ if (env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' && !hasEntitlement('per
6565
}
6666
else if (env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' && hasEntitlement('permission-syncing')) {
6767
repoPermissionSyncer.startScheduler();
68-
userPermissionSyncer.startScheduler();
68+
accountPermissionSyncer.startScheduler();
6969
}
7070

7171
logger.info('Worker started.');
@@ -81,7 +81,7 @@ const cleanup = async (signal: string) => {
8181
repoIndexManager.dispose(),
8282
connectionManager.dispose(),
8383
repoPermissionSyncer.dispose(),
84-
userPermissionSyncer.dispose(),
84+
accountPermissionSyncer.dispose(),
8585
promClient.dispose(),
8686
configManager.dispose(),
8787
]),
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
Warnings:
3+
4+
- You are about to drop the column `permissionSyncedAt` on the `User` table. All the data in the column will be lost.
5+
- You are about to drop the `UserPermissionSyncJob` table. If the table is not empty, all the data it contains will be lost.
6+
- You are about to drop the `UserToRepoPermission` table. If the table is not empty, all the data it contains will be lost.
7+
8+
*/
9+
-- CreateEnum
10+
CREATE TYPE "AccountPermissionSyncJobStatus" AS ENUM ('PENDING', 'IN_PROGRESS', 'COMPLETED', 'FAILED');
11+
12+
-- DropForeignKey
13+
ALTER TABLE "UserPermissionSyncJob" DROP CONSTRAINT "UserPermissionSyncJob_userId_fkey";
14+
15+
-- DropForeignKey
16+
ALTER TABLE "UserToRepoPermission" DROP CONSTRAINT "UserToRepoPermission_repoId_fkey";
17+
18+
-- DropForeignKey
19+
ALTER TABLE "UserToRepoPermission" DROP CONSTRAINT "UserToRepoPermission_userId_fkey";
20+
21+
-- AlterTable
22+
ALTER TABLE "Account" ADD COLUMN "permissionSyncedAt" TIMESTAMP(3);
23+
24+
-- AlterTable
25+
ALTER TABLE "User" DROP COLUMN "permissionSyncedAt";
26+
27+
-- DropTable
28+
DROP TABLE "UserPermissionSyncJob";
29+
30+
-- DropTable
31+
DROP TABLE "UserToRepoPermission";
32+
33+
-- DropEnum
34+
DROP TYPE "UserPermissionSyncJobStatus";
35+
36+
-- CreateTable
37+
CREATE TABLE "AccountPermissionSyncJob" (
38+
"id" TEXT NOT NULL,
39+
"status" "AccountPermissionSyncJobStatus" NOT NULL DEFAULT 'PENDING',
40+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
41+
"updatedAt" TIMESTAMP(3) NOT NULL,
42+
"completedAt" TIMESTAMP(3),
43+
"errorMessage" TEXT,
44+
"accountId" TEXT NOT NULL,
45+
46+
CONSTRAINT "AccountPermissionSyncJob_pkey" PRIMARY KEY ("id")
47+
);
48+
49+
-- CreateTable
50+
CREATE TABLE "AccountToRepoPermission" (
51+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
52+
"repoId" INTEGER NOT NULL,
53+
"accountId" TEXT NOT NULL,
54+
55+
CONSTRAINT "AccountToRepoPermission_pkey" PRIMARY KEY ("repoId","accountId")
56+
);
57+
58+
-- AddForeignKey
59+
ALTER TABLE "AccountPermissionSyncJob" ADD CONSTRAINT "AccountPermissionSyncJob_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account"("id") ON DELETE CASCADE ON UPDATE CASCADE;
60+
61+
-- AddForeignKey
62+
ALTER TABLE "AccountToRepoPermission" ADD CONSTRAINT "AccountToRepoPermission_repoId_fkey" FOREIGN KEY ("repoId") REFERENCES "Repo"("id") ON DELETE CASCADE ON UPDATE CASCADE;
63+
64+
-- AddForeignKey
65+
ALTER TABLE "AccountToRepoPermission" ADD CONSTRAINT "AccountToRepoPermission_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account"("id") ON DELETE CASCADE ON UPDATE CASCADE;

packages/db/prisma/schema.prisma

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ model Repo {
5959
connections RepoToConnection[]
6060
imageUrl String?
6161
62-
permittedUsers UserToRepoPermission[]
62+
permittedAccounts AccountToRepoPermission[]
6363
permissionSyncJobs RepoPermissionSyncJob[]
6464
permissionSyncedAt DateTime? /// When the permissions were last synced successfully.
6565
@@ -349,7 +349,6 @@ model User {
349349
accounts Account[]
350350
orgs UserToOrg[]
351351
accountRequest AccountRequest?
352-
accessibleRepos UserToRepoPermission[]
353352
354353
/// List of pending invites that the user has created
355354
invites Invite[]
@@ -361,40 +360,38 @@ model User {
361360
createdAt DateTime @default(now())
362361
updatedAt DateTime @updatedAt
363362
364-
permissionSyncJobs UserPermissionSyncJob[]
365-
permissionSyncedAt DateTime?
366363
}
367364

368-
enum UserPermissionSyncJobStatus {
365+
enum AccountPermissionSyncJobStatus {
369366
PENDING
370367
IN_PROGRESS
371368
COMPLETED
372369
FAILED
373370
}
374371

375-
model UserPermissionSyncJob {
372+
model AccountPermissionSyncJob {
376373
id String @id @default(cuid())
377-
status UserPermissionSyncJobStatus @default(PENDING)
374+
status AccountPermissionSyncJobStatus @default(PENDING)
378375
createdAt DateTime @default(now())
379376
updatedAt DateTime @updatedAt
380377
completedAt DateTime?
381378
382379
errorMessage String?
383380
384-
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
385-
userId String
381+
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
382+
accountId String
386383
}
387384

388-
model UserToRepoPermission {
385+
model AccountToRepoPermission {
389386
createdAt DateTime @default(now())
390387
391388
repo Repo @relation(fields: [repoId], references: [id], onDelete: Cascade)
392389
repoId Int
393390
394-
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
395-
userId String
391+
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
392+
accountId String
396393
397-
@@id([repoId, userId])
394+
@@id([repoId, accountId])
398395
}
399396

400397
// @see : https://authjs.dev/concepts/database-models#account
@@ -411,6 +408,12 @@ model Account {
411408
scope String?
412409
id_token String?
413410
session_state String?
411+
412+
/// List of repos that this account has access to.
413+
accessibleRepos AccountToRepoPermission[]
414+
415+
permissionSyncJobs AccountPermissionSyncJob[]
416+
permissionSyncedAt DateTime?
414417
415418
createdAt DateTime @default(now())
416419
updatedAt DateTime @updatedAt

packages/web/src/__mocks__/prisma.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { SINGLE_TENANT_ORG_DOMAIN, SINGLE_TENANT_ORG_ID, SINGLE_TENANT_ORG_NAME } from '@/lib/constants';
2-
import { ApiKey, Org, PrismaClient, User } from '@prisma/client';
2+
import { Account, ApiKey, Org, PrismaClient, User } from '@prisma/client';
33
import { beforeEach, vi } from 'vitest';
44
import { mockDeep, mockReset } from 'vitest-mock-extended';
55

@@ -35,7 +35,7 @@ export const MOCK_API_KEY: ApiKey = {
3535
createdById: '1',
3636
}
3737

38-
export const MOCK_USER: User = {
38+
export const MOCK_USER_WITH_ACCOUNTS: User & { accounts: Account[] } = {
3939
id: '1',
4040
name: 'Test User',
4141
@@ -44,7 +44,7 @@ export const MOCK_USER: User = {
4444
hashedPassword: null,
4545
emailVerified: null,
4646
image: null,
47-
permissionSyncedAt: null
47+
accounts: [],
4848
}
4949

5050
export const userScopedPrismaClientExtension = vi.fn();

packages/web/src/prisma.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,29 @@ if (env.NODE_ENV !== "production") globalForPrisma.prisma = prisma
2020
* Creates a prisma client extension that scopes queries to striclty information
2121
* a given user should be able to access.
2222
*/
23-
export const userScopedPrismaClientExtension = (userId?: string) => {
23+
export const userScopedPrismaClientExtension = (accountIds?: string[]) => {
2424
return Prisma.defineExtension(
2525
(prisma) => {
2626
return prisma.$extends({
2727
query: {
2828
...(env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' && hasEntitlement('permission-syncing') ? {
2929
repo: {
3030
async $allOperations({ args, query }) {
31-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
32-
const argsWithWhere = args as any;
31+
const argsWithWhere = args as Record<string, unknown> & {
32+
where?: Prisma.RepoWhereInput;
33+
}
34+
3335
argsWithWhere.where = {
3436
...(argsWithWhere.where || {}),
3537
OR: [
3638
// Only include repos that are permitted to the user
37-
...(userId ? [
39+
...(accountIds ? [
3840
{
39-
permittedUsers: {
41+
permittedAccounts: {
4042
some: {
41-
userId,
43+
accountId: {
44+
in: accountIds,
45+
}
4246
}
4347
}
4448
},
@@ -48,7 +52,7 @@ export const userScopedPrismaClientExtension = (userId?: string) => {
4852
isPublic: true,
4953
}
5054
]
51-
}
55+
};
5256

5357
return query(args);
5458
}

0 commit comments

Comments
 (0)