Skip to content

Commit 10db36b

Browse files
wip
1 parent c0caa5a commit 10db36b

8 files changed

Lines changed: 206 additions & 60 deletions

File tree

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ Copyright (c) 2025 Taqla Inc.
22

33
Portions of this software are licensed as follows:
44

5-
- All content that resides under the "ee/" and "packages/web/src/ee/" directories of this repository, if these directories exist, is licensed under the license defined in "ee/LICENSE".
5+
- All content that resides under the "ee/", "packages/web/src/ee/", and "packages/backend/src/ee/" directories of this repository, if these directories exist, is licensed under the license defined in "ee/LICENSE".
66
- All third party components incorporated into the Sourcebot Software are licensed under the original license provided by the owner of the applicable component.
77
- Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
88

packages/backend/src/connectionManager.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { BackendError, BackendException } from "@sourcebot/error";
99
import { captureEvent } from "./posthog.js";
1010
import { env } from "./env.js";
1111
import * as Sentry from "@sentry/node";
12+
import { syncSearchContexts } from "./ee/syncSearchContexts.js";
13+
import { AppContext } from "./types.js";
1214

1315
interface IConnectionManager {
1416
scheduleConnectionSync: (connection: Connection) => Promise<void>;
@@ -38,6 +40,7 @@ export class ConnectionManager implements IConnectionManager {
3840
private db: PrismaClient,
3941
private settings: Settings,
4042
redis: Redis,
43+
private ctx: AppContext,
4144
) {
4245
this.queue = new Queue<JobPayload>(QUEUE_NAME, {
4346
connection: redis,
@@ -289,7 +292,9 @@ export class ConnectionManager implements IConnectionManager {
289292
notFound.repos.length > 0 ? ConnectionSyncStatus.SYNCED_WITH_WARNINGS : ConnectionSyncStatus.SYNCED,
290293
syncedAt: new Date()
291294
}
292-
})
295+
});
296+
297+
await syncSearchContexts(this.db, this.ctx.config?.contexts);
293298

294299
captureEvent('backend_connection_sync_job_completed', {
295300
connectionId: connectionId,

packages/backend/src/constants.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,12 @@ export const DEFAULT_SETTINGS: Settings = {
1616
repoGarbageCollectionGracePeriodMs: 10 * 1000, // 10 seconds
1717
repoIndexTimeoutMs: 1000 * 60 * 60 * 2, // 2 hours
1818
enablePublicAccess: false,
19-
}
19+
}
20+
21+
// NOTE: changing SOURCEBOT_GUEST_USER_ID may break backwards compatibility since this value is used
22+
// to detect old guest users in the DB. If you change this value ensure it doesn't break upgrade flows
23+
export const SOURCEBOT_GUEST_USER_ID = '1';
24+
export const SOURCEBOT_GUEST_USER_EMAIL = 'guest@sourcebot.dev';
25+
export const SINGLE_TENANT_ORG_ID = 1;
26+
export const SINGLE_TENANT_ORG_DOMAIN = '~';
27+
export const SINGLE_TENANT_ORG_NAME = 'default';
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { SearchContext } from "@sourcebot/schemas/v3/index.type";
2+
import micromatch from "micromatch";
3+
import { createLogger } from "@sourcebot/logger";
4+
import { PrismaClient } from "@sourcebot/db";
5+
import { SINGLE_TENANT_ORG_ID } from "../constants.js";
6+
7+
const logger = createLogger('sync-search-contexts');
8+
9+
export const syncSearchContexts = async (db: PrismaClient, contexts?: { [key: string]: SearchContext }) => {
10+
// @todo: re-enable this.
11+
// if (env.SOURCEBOT_TENANCY_MODE !== 'single') {
12+
// throw new Error("Search contexts are not supported in this tenancy mode. Set SOURCEBOT_TENANCY_MODE=single in your environment variables.");
13+
// }
14+
15+
// if (!hasEntitlement("search-contexts")) {
16+
// if (contexts) {
17+
// const plan = getPlan();
18+
// logger.error(`Search contexts are not supported in your current plan: ${plan}. If you have a valid enterprise license key, pass it via SOURCEBOT_EE_LICENSE_KEY. For support, contact ${SOURCEBOT_SUPPORT_EMAIL}.`);
19+
// }
20+
// return;
21+
// }
22+
23+
if (contexts) {
24+
for (const [key, newContextConfig] of Object.entries(contexts)) {
25+
const allRepos = await db.repo.findMany({
26+
where: {
27+
orgId: SINGLE_TENANT_ORG_ID,
28+
},
29+
select: {
30+
id: true,
31+
name: true,
32+
}
33+
});
34+
35+
let newReposInContext = allRepos.filter(repo => {
36+
return micromatch.isMatch(repo.name, newContextConfig.include);
37+
});
38+
39+
if (newContextConfig.exclude) {
40+
const exclude = newContextConfig.exclude;
41+
newReposInContext = newReposInContext.filter(repo => {
42+
return !micromatch.isMatch(repo.name, exclude);
43+
});
44+
}
45+
46+
const currentReposInContext = (await db.searchContext.findUnique({
47+
where: {
48+
name_orgId: {
49+
name: key,
50+
orgId: SINGLE_TENANT_ORG_ID,
51+
}
52+
},
53+
include: {
54+
repos: true,
55+
}
56+
}))?.repos ?? [];
57+
58+
await db.searchContext.upsert({
59+
where: {
60+
name_orgId: {
61+
name: key,
62+
orgId: SINGLE_TENANT_ORG_ID,
63+
}
64+
},
65+
update: {
66+
repos: {
67+
connect: newReposInContext.map(repo => ({
68+
id: repo.id,
69+
})),
70+
disconnect: currentReposInContext
71+
.filter(repo => !newReposInContext.map(r => r.id).includes(repo.id))
72+
.map(repo => ({
73+
id: repo.id,
74+
})),
75+
},
76+
description: newContextConfig.description,
77+
},
78+
create: {
79+
name: key,
80+
description: newContextConfig.description,
81+
org: {
82+
connect: {
83+
id: SINGLE_TENANT_ORG_ID,
84+
}
85+
},
86+
repos: {
87+
connect: newReposInContext.map(repo => ({
88+
id: repo.id,
89+
})),
90+
}
91+
}
92+
});
93+
}
94+
}
95+
96+
const deletedContexts = await db.searchContext.findMany({
97+
where: {
98+
name: {
99+
notIn: Object.keys(contexts ?? {}),
100+
},
101+
orgId: SINGLE_TENANT_ORG_ID,
102+
}
103+
});
104+
105+
for (const context of deletedContexts) {
106+
logger.info(`Deleting search context with name '${context.name}'. ID: ${context.id}`);
107+
await db.searchContext.delete({
108+
where: {
109+
id: context.id,
110+
}
111+
})
112+
}
113+
}

packages/backend/src/index.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,43 @@ import { main } from "./main.js"
99
import { PrismaClient } from "@sourcebot/db";
1010
import { env } from "./env.js";
1111
import { createLogger } from "@sourcebot/logger";
12+
import { isRemotePath } from './utils.js';
13+
import { readFile } from 'fs/promises';
14+
import stripJsonComments from 'strip-json-comments';
15+
import { SourcebotConfig } from '@sourcebot/schemas/v3/index.type';
16+
import { indexSchema } from '@sourcebot/schemas/v3/index.schema';
17+
import { Ajv } from "ajv";
1218

13-
const logger = createLogger('index');
19+
const logger = createLogger('backend-entrypoint');
20+
const ajv = new Ajv({
21+
validateFormats: false,
22+
});
23+
24+
const loadConfig = async (configPath?: string) => {
25+
if (!configPath) {
26+
return undefined;
27+
}
28+
29+
const configContent = await (async () => {
30+
if (isRemotePath(configPath)) {
31+
const response = await fetch(configPath);
32+
if (!response.ok) {
33+
throw new Error(`Failed to fetch config file ${configPath}: ${response.statusText}`);
34+
}
35+
return response.text();
36+
} else {
37+
return readFile(configPath, { encoding: 'utf-8' });
38+
}
39+
})();
40+
41+
const config = JSON.parse(stripJsonComments(configContent)) as SourcebotConfig;
42+
const isValidConfig = ajv.validate(indexSchema, config);
43+
if (!isValidConfig) {
44+
throw new Error(`Config file '${configPath}' is invalid: ${ajv.errorsText(ajv.errors)}`);
45+
}
46+
47+
return config;
48+
}
1449

1550
// Register handler for normal exit
1651
process.on('exit', (code) => {
@@ -50,10 +85,13 @@ if (!existsSync(indexPath)) {
5085
await mkdir(indexPath, { recursive: true });
5186
}
5287

88+
const config = await loadConfig(env.CONFIG_PATH);
89+
5390
const context: AppContext = {
5491
indexPath,
5592
reposPath,
5693
cachePath: cacheDir,
94+
config,
5795
}
5896

5997
const prisma = new PrismaClient();
@@ -72,3 +110,4 @@ main(prisma, context)
72110
.finally(() => {
73111
logger.info("Shutting down...");
74112
});
113+

packages/backend/src/main.ts

Lines changed: 21 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,15 @@
11
import { PrismaClient } from '@sourcebot/db';
22
import { createLogger } from "@sourcebot/logger";
33
import { AppContext } from "./types.js";
4-
import { DEFAULT_SETTINGS } from './constants.js';
4+
import { DEFAULT_SETTINGS, SINGLE_TENANT_ORG_DOMAIN, SINGLE_TENANT_ORG_ID, SINGLE_TENANT_ORG_NAME } from './constants.js';
55
import { Redis } from 'ioredis';
66
import { ConnectionManager } from './connectionManager.js';
77
import { RepoManager } from './repoManager.js';
88
import { env } from './env.js';
99
import { PromClient } from './promClient.js';
10-
import { isRemotePath } from './utils.js';
11-
import { readFile } from 'fs/promises';
12-
import stripJsonComments from 'strip-json-comments';
13-
import { SourcebotConfig } from '@sourcebot/schemas/v3/index.type';
14-
import { indexSchema } from '@sourcebot/schemas/v3/index.schema';
15-
import { Ajv } from "ajv";
10+
import { syncSearchContexts } from './ee/syncSearchContexts.js';
1611

1712
const logger = createLogger('backend-main');
18-
const ajv = new Ajv({
19-
validateFormats: false,
20-
});
21-
22-
const getSettings = async (configPath?: string) => {
23-
if (!configPath) {
24-
return DEFAULT_SETTINGS;
25-
}
26-
27-
const configContent = await (async () => {
28-
if (isRemotePath(configPath)) {
29-
const response = await fetch(configPath);
30-
if (!response.ok) {
31-
throw new Error(`Failed to fetch config file ${configPath}: ${response.statusText}`);
32-
}
33-
return response.text();
34-
} else {
35-
return readFile(configPath, { encoding: 'utf-8' });
36-
}
37-
})();
38-
39-
const config = JSON.parse(stripJsonComments(configContent)) as SourcebotConfig;
40-
const isValidConfig = ajv.validate(indexSchema, config);
41-
if (!isValidConfig) {
42-
throw new Error(`Config file '${configPath}' is invalid: ${ajv.errorsText(ajv.errors)}`);
43-
}
44-
45-
return {
46-
...DEFAULT_SETTINGS,
47-
...config.settings,
48-
}
49-
}
5013

5114
export const main = async (db: PrismaClient, context: AppContext) => {
5215
const redis = new Redis(env.REDIS_URL, {
@@ -60,11 +23,28 @@ export const main = async (db: PrismaClient, context: AppContext) => {
6023
process.exit(1);
6124
});
6225

63-
const settings = await getSettings(env.CONFIG_PATH);
26+
const settings = {
27+
...DEFAULT_SETTINGS,
28+
...context.config?.settings,
29+
}
30+
31+
await db.org.upsert({
32+
where: {
33+
id: SINGLE_TENANT_ORG_ID,
34+
},
35+
update: {},
36+
create: {
37+
name: SINGLE_TENANT_ORG_NAME,
38+
domain: SINGLE_TENANT_ORG_DOMAIN,
39+
id: SINGLE_TENANT_ORG_ID
40+
}
41+
});
42+
43+
await syncSearchContexts(db, context.config?.contexts);
6444

6545
const promClient = new PromClient();
6646

67-
const connectionManager = new ConnectionManager(db, settings, redis);
47+
const connectionManager = new ConnectionManager(db, settings, redis, context);
6848
connectionManager.registerPollingCallback();
6949

7050
const repoManager = new RepoManager(db, settings, redis, promClient, context);

packages/backend/src/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Settings as SettingsSchema } from "@sourcebot/schemas/v3/index.type";
1+
import { Settings as SettingsSchema, SourcebotConfig } from "@sourcebot/schemas/v3/index.type";
22
import { z } from "zod";
33

44
export type AppContext = {
@@ -13,6 +13,8 @@ export type AppContext = {
1313
indexPath: string;
1414

1515
cachePath: string;
16+
17+
config?: SourcebotConfig;
1618
}
1719

1820
export type Settings = Required<SettingsSchema>;

packages/web/src/initialize.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import { ConnectionSyncStatus, OrgRole, Prisma, RepoIndexingStatus } from '@sourcebot/db';
22
import { env } from './env.mjs';
33
import { prisma } from "@/prisma";
4-
import { SINGLE_TENANT_ORG_ID, SINGLE_TENANT_ORG_DOMAIN, SINGLE_TENANT_ORG_NAME, SOURCEBOT_GUEST_USER_ID } from './lib/constants';
4+
import { SINGLE_TENANT_ORG_ID, SINGLE_TENANT_ORG_DOMAIN, SOURCEBOT_GUEST_USER_ID } from './lib/constants';
55
import { readFile } from 'fs/promises';
66
import { watch } from 'fs';
77
import stripJsonComments from 'strip-json-comments';
88
import { SourcebotConfig } from "@sourcebot/schemas/v3/index.type";
99
import { ConnectionConfig } from '@sourcebot/schemas/v3/connection.type';
1010
import { indexSchema } from '@sourcebot/schemas/v3/index.schema';
1111
import Ajv from 'ajv';
12-
import { syncSearchContexts } from '@/ee/features/searchContexts/syncSearchContexts';
1312
import { hasEntitlement } from '@/features/entitlements/server';
1413
import { createGuestUser, setPublicAccessStatus } from '@/ee/features/publicAccess/publicAccess';
1514
import { isServiceError } from './lib/utils';
@@ -158,7 +157,7 @@ const syncDeclarativeConfig = async (configPath: string) => {
158157
}
159158

160159
await syncConnections(config.connections);
161-
await syncSearchContexts(config.contexts);
160+
// await syncSearchContexts(config.contexts);
162161
}
163162

164163
const pruneOldGuestUser = async () => {
@@ -187,17 +186,17 @@ const pruneOldGuestUser = async () => {
187186
}
188187

189188
const initSingleTenancy = async () => {
190-
await prisma.org.upsert({
191-
where: {
192-
id: SINGLE_TENANT_ORG_ID,
193-
},
194-
update: {},
195-
create: {
196-
name: SINGLE_TENANT_ORG_NAME,
197-
domain: SINGLE_TENANT_ORG_DOMAIN,
198-
id: SINGLE_TENANT_ORG_ID
199-
}
200-
});
189+
// await prisma.org.upsert({
190+
// where: {
191+
// id: SINGLE_TENANT_ORG_ID,
192+
// },
193+
// update: {},
194+
// create: {
195+
// name: SINGLE_TENANT_ORG_NAME,
196+
// domain: SINGLE_TENANT_ORG_DOMAIN,
197+
// id: SINGLE_TENANT_ORG_ID
198+
// }
199+
// });
201200

202201
// This is needed because v4 introduces the GUEST org role as well as making authentication required.
203202
// To keep things simple, we'll just delete the old guest user if it exists in the DB

0 commit comments

Comments
 (0)