| import type { Db } from "@penclipai/db"; |
| import { companies, instanceSettings } from "@penclipai/db"; |
| import { |
| DEFAULT_UI_LOCALE, |
| DEFAULT_FEEDBACK_DATA_SHARING_PREFERENCE, |
| DEFAULT_BACKUP_RETENTION, |
| instanceGeneralSettingsSchema, |
| type InstanceGeneralSettings, |
| instanceExperimentalSettingsSchema, |
| type InstanceExperimentalSettings, |
| type PatchInstanceGeneralSettings, |
| type InstanceSettings, |
| type PatchInstanceExperimentalSettings, |
| } from "@penclipai/shared"; |
| import { eq } from "drizzle-orm"; |
|
|
| const DEFAULT_SINGLETON_KEY = "default"; |
|
|
| function normalizeGeneralSettings(raw: unknown): InstanceGeneralSettings { |
| const parsed = instanceGeneralSettingsSchema.safeParse(raw ?? {}); |
| if (parsed.success) { |
| return { |
| censorUsernameInLogs: parsed.data.censorUsernameInLogs ?? false, |
| keyboardShortcuts: parsed.data.keyboardShortcuts ?? false, |
| feedbackDataSharingPreference: |
| parsed.data.feedbackDataSharingPreference ?? DEFAULT_FEEDBACK_DATA_SHARING_PREFERENCE, |
| runtimeDefaultLocale: parsed.data.runtimeDefaultLocale ?? DEFAULT_UI_LOCALE, |
| backupRetention: parsed.data.backupRetention ?? DEFAULT_BACKUP_RETENTION, |
| }; |
| } |
| return { |
| censorUsernameInLogs: false, |
| keyboardShortcuts: false, |
| feedbackDataSharingPreference: DEFAULT_FEEDBACK_DATA_SHARING_PREFERENCE, |
| runtimeDefaultLocale: DEFAULT_UI_LOCALE, |
| backupRetention: DEFAULT_BACKUP_RETENTION, |
| }; |
| } |
|
|
| function normalizeExperimentalSettings(raw: unknown): InstanceExperimentalSettings { |
| const parsed = instanceExperimentalSettingsSchema.safeParse(raw ?? {}); |
| if (parsed.success) { |
| return { |
| enableEnvironments: parsed.data.enableEnvironments ?? false, |
| enableIsolatedWorkspaces: parsed.data.enableIsolatedWorkspaces ?? false, |
| autoRestartDevServerWhenIdle: parsed.data.autoRestartDevServerWhenIdle ?? false, |
| enableIssueGraphLivenessAutoRecovery: parsed.data.enableIssueGraphLivenessAutoRecovery ?? false, |
| }; |
| } |
| return { |
| enableEnvironments: false, |
| enableIsolatedWorkspaces: false, |
| autoRestartDevServerWhenIdle: false, |
| enableIssueGraphLivenessAutoRecovery: false, |
| }; |
| } |
|
|
| function toInstanceSettings(row: typeof instanceSettings.$inferSelect): InstanceSettings { |
| return { |
| id: row.id, |
| general: normalizeGeneralSettings(row.general), |
| experimental: normalizeExperimentalSettings(row.experimental), |
| createdAt: row.createdAt, |
| updatedAt: row.updatedAt, |
| }; |
| } |
|
|
| export function instanceSettingsService(db: Db) { |
| async function getOrCreateRow() { |
| const existing = await db |
| .select() |
| .from(instanceSettings) |
| .where(eq(instanceSettings.singletonKey, DEFAULT_SINGLETON_KEY)) |
| .then((rows) => rows[0] ?? null); |
| if (existing) return existing; |
|
|
| const now = new Date(); |
| const [created] = await db |
| .insert(instanceSettings) |
| .values({ |
| singletonKey: DEFAULT_SINGLETON_KEY, |
| general: {}, |
| experimental: {}, |
| createdAt: now, |
| updatedAt: now, |
| }) |
| .onConflictDoUpdate({ |
| target: [instanceSettings.singletonKey], |
| set: { |
| updatedAt: now, |
| }, |
| }) |
| .returning(); |
|
|
| if (created) return created; |
|
|
| const raced = await db |
| .select() |
| .from(instanceSettings) |
| .where(eq(instanceSettings.singletonKey, DEFAULT_SINGLETON_KEY)) |
| .then((rows) => rows[0] ?? null); |
| if (raced) return raced; |
|
|
| throw new Error("Failed to initialize instance settings row"); |
| } |
|
|
| return { |
| get: async (): Promise<InstanceSettings> => toInstanceSettings(await getOrCreateRow()), |
|
|
| getGeneral: async (): Promise<InstanceGeneralSettings> => { |
| const row = await getOrCreateRow(); |
| return normalizeGeneralSettings(row.general); |
| }, |
|
|
| getExperimental: async (): Promise<InstanceExperimentalSettings> => { |
| const row = await getOrCreateRow(); |
| return normalizeExperimentalSettings(row.experimental); |
| }, |
|
|
| updateGeneral: async (patch: PatchInstanceGeneralSettings): Promise<InstanceSettings> => { |
| const current = await getOrCreateRow(); |
| const nextGeneral = normalizeGeneralSettings({ |
| ...normalizeGeneralSettings(current.general), |
| ...patch, |
| }); |
| const now = new Date(); |
| const [updated] = await db |
| .update(instanceSettings) |
| .set({ |
| general: { ...nextGeneral }, |
| updatedAt: now, |
| }) |
| .where(eq(instanceSettings.id, current.id)) |
| .returning(); |
| return toInstanceSettings(updated ?? current); |
| }, |
|
|
| updateExperimental: async (patch: PatchInstanceExperimentalSettings): Promise<InstanceSettings> => { |
| const current = await getOrCreateRow(); |
| const nextExperimental = normalizeExperimentalSettings({ |
| ...normalizeExperimentalSettings(current.experimental), |
| ...patch, |
| }); |
| const now = new Date(); |
| const [updated] = await db |
| .update(instanceSettings) |
| .set({ |
| experimental: { ...nextExperimental }, |
| updatedAt: now, |
| }) |
| .where(eq(instanceSettings.id, current.id)) |
| .returning(); |
| return toInstanceSettings(updated ?? current); |
| }, |
|
|
| listCompanyIds: async (): Promise<string[]> => |
| db |
| .select({ id: companies.id }) |
| .from(companies) |
| .then((rows) => rows.map((row) => row.id)), |
| }; |
| } |
|
|