Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/obsidian/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
},
"dependencies": {
"@codemirror/view": "^6.38.8",
"@repo/database": "workspace:*",
"@repo/utils": "workspace:*",
"@supabase/supabase-js": "catalog:",
"date-fns": "^4.1.0",
"nanoid": "^4.0.2",
"react": "catalog:obsidian",
Expand Down
28 changes: 28 additions & 0 deletions apps/obsidian/scripts/compile.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/naming-convention */
import esbuild from "esbuild";
import fs from "fs";
import path from "path";
Expand All @@ -10,6 +11,19 @@ import autoprefixer from "autoprefixer";

dotenv.config();

// For local dev: Set SUPABASE_USE_DB=local and run `pnpm run genenv` in packages/database
let envContents: (() => Record<string, string>) | null = null;
try {
const dbDotEnv = require("@repo/database/dbDotEnv");
envContents = dbDotEnv.envContents;
} catch (error) {
if ((error as Error).message.includes("Cannot find module")) {
console.error("Build the database module before compiling obsidian");
process.exit(1);
}
throw error;
}

const DEFAULT_FILES_INCLUDED = ["manifest.json"];
const isProd = process.env.NODE_ENV === "production";

Expand Down Expand Up @@ -43,6 +57,7 @@ export const args = {
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
"tslib=window.TSLib",
...builtins,
],
} as CliOpts;
Expand Down Expand Up @@ -89,6 +104,10 @@ export const compile = ({
fs.mkdirSync(outdir, { recursive: true });

const buildPromises = [] as Promise<void>[];
if (!envContents) {
throw new Error("envContents not loaded. Build the database module first.");
}
const dbEnv = envContents();
buildPromises.push(
builder({
absWorkingDir: process.cwd(),
Expand All @@ -100,6 +119,15 @@ export const compile = ({
minify: isProd,
entryNames: out,
external: external,
define: {
"process.env.SUPABASE_URL": dbEnv.SUPABASE_URL
? `"${dbEnv.SUPABASE_URL}"`
: "null",
"process.env.SUPABASE_ANON_KEY": dbEnv.SUPABASE_ANON_KEY
? `"${dbEnv.SUPABASE_ANON_KEY}"`
: "null",
"process.env.NEXT_API_ROOT": `"${dbEnv.NEXT_API_ROOT || ""}"`,
},
plugins: [
{
name: "log",
Expand Down
41 changes: 39 additions & 2 deletions apps/obsidian/src/components/AdminPanelSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,57 @@
import { useState } from "react";
import { useState, useCallback } from "react";
import { usePlugin } from "./PluginContext";
import { Notice } from "obsidian";
import { initializeSupabaseSync } from "~/utils/syncDgNodesToSupabase";

export const AdminPanelSettings = () => {
const plugin = usePlugin();
const [syncModeEnabled, setSyncModeEnabled] = useState<boolean>(
plugin.settings.syncModeEnabled ?? false,
);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);

const handleSyncModeToggle = useCallback((newValue: boolean) => {
setSyncModeEnabled(newValue);
setHasUnsavedChanges(true);
}, []);

const handleSave = async () => {
plugin.settings.syncModeEnabled = syncModeEnabled;
await plugin.saveSettings();
new Notice("Admin panel settings saved");
setHasUnsavedChanges(false);

if (syncModeEnabled) {
try {
await initializeSupabaseSync(plugin);
new Notice("Sync mode initialized successfully");
} catch (error) {
console.error("Failed to initialize sync mode:", error);
new Notice(
`Failed to initialize sync mode: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
};

return (
<div className="general-settings">
{/* Add more admin panel settings sections here */}
<div className="setting-item">
<div className="setting-item-info">
<div className="setting-item-name">(BETA) Sync mode enable</div>
<div className="setting-item-description">
Enable synchronization with Discourse Graph database
</div>
</div>
<div className="setting-item-control">
<div
className={`checkbox-container ${syncModeEnabled ? "is-enabled" : ""}`}
onClick={() => handleSyncModeToggle(!syncModeEnabled)}
>
<input type="checkbox" checked={syncModeEnabled} />
</div>
</div>
</div>

<div className="setting-item">
<button
Expand Down
3 changes: 3 additions & 0 deletions apps/obsidian/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ export const DEFAULT_SETTINGS: Settings = {
canvasFolderPath: "Discourse Canvas",
canvasAttachmentsFolderPath: "attachments",
nodeTagHotkey: "\\",
spacePassword: undefined,
accountLocalId: undefined,
syncModeEnabled: false,
};

export const FEATURE_FLAGS = {
Expand Down
13 changes: 13 additions & 0 deletions apps/obsidian/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
TFile,
MarkdownView,
WorkspaceLeaf,
Notice,
} from "obsidian";
import { EditorView } from "@codemirror/view";
import { SettingsTab } from "~/components/Settings";
Expand All @@ -22,6 +23,7 @@ import ModifyNodeModal from "~/components/ModifyNodeModal";
import { TagNodeHandler } from "~/utils/tagNodeHandler";
import { TldrawView } from "~/components/canvas/TldrawView";
import { NodeTagSuggestPopover } from "~/components/NodeTagSuggestModal";
import { initializeSupabaseSync } from "~/utils/syncDgNodesToSupabase";

export default class DiscourseGraphPlugin extends Plugin {
settings: Settings = { ...DEFAULT_SETTINGS };
Expand All @@ -32,6 +34,17 @@ export default class DiscourseGraphPlugin extends Plugin {

async onload() {
await this.loadSettings();

if (this.settings.syncModeEnabled === true) {
void initializeSupabaseSync(this).catch((error) => {
console.error("Failed to initialize Supabase sync:", error);
new Notice(
`Failed to initialize Supabase sync: ${error instanceof Error ? error.message : String(error)}`,
5000,
);
});
}

registerCommands(this);
this.addSettingTab(new SettingsTab(this.app, this));

Expand Down
3 changes: 3 additions & 0 deletions apps/obsidian/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export type Settings = {
canvasFolderPath: string;
canvasAttachmentsFolderPath: string;
nodeTagHotkey: string;
spacePassword?: string;
accountLocalId?: string;
syncModeEnabled?: boolean;
};

export type BulkImportCandidate = {
Expand Down
168 changes: 168 additions & 0 deletions apps/obsidian/src/utils/supabaseContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import type { Enums } from "@repo/database/dbTypes";
import type { DGSupabaseClient } from "@repo/database/lib/client";
import {
fetchOrCreateSpaceDirect,
fetchOrCreatePlatformAccount,
createLoggedInClient,
} from "@repo/database/lib/contextFunctions";
import type DiscourseGraphPlugin from "~/index";

type Platform = Enums<"Platform">;

export type SupabaseContext = {
platform: Platform;
spaceId: number;
userId: number;
spacePassword: string;
};

let contextCache: SupabaseContext | null = null;

const generateAccountLocalId = (vaultName: string): string => {
const randomSuffix = Math.random().toString(36).substring(2, 8).toUpperCase();
const sanitizedVaultName = vaultName
.replace(/\s+/g, "")
.replace(/[^a-zA-Z0-9]/g, "")
.replace(/-+/g, "-");
return `${sanitizedVaultName}${randomSuffix}`;
};

const getOrCreateSpacePassword = async (
plugin: DiscourseGraphPlugin,
): Promise<string> => {
if (plugin.settings.spacePassword) {
return plugin.settings.spacePassword;
}
const password = crypto.randomUUID();
plugin.settings.spacePassword = password;
await plugin.saveSettings();
return password;
};

const getOrCreateAccountLocalId = async (
plugin: DiscourseGraphPlugin,
vaultName: string,
): Promise<string> => {
if (plugin.settings.accountLocalId) {
return plugin.settings.accountLocalId;
}
const accountLocalId = generateAccountLocalId(vaultName);
plugin.settings.accountLocalId = accountLocalId;
await plugin.saveSettings();
return accountLocalId;
};

/**
* Gets the unique vault ID from Obsidian's internal API.
* @see https://help.obsidian.md/Extending+Obsidian/Obsidian+URI
*/
const getVaultId = (app: DiscourseGraphPlugin["app"]): string => {
return (app as unknown as { appId: string }).appId;
};

const canonicalObsidianUrl = (vaultId: string): string => {
return `obsidian:${vaultId}`;
};

export const getSupabaseContext = async (
plugin: DiscourseGraphPlugin,
): Promise<SupabaseContext | null> => {
if (contextCache === null) {
try {
const vaultName = plugin.app.vault.getName() || "obsidian-vault";
const vaultId = getVaultId(plugin.app);

const spacePassword = await getOrCreateSpacePassword(plugin);
const accountLocalId = await getOrCreateAccountLocalId(plugin, vaultName);

const url = canonicalObsidianUrl(vaultId);
const platform: Platform = "Obsidian";

const spaceResult = await fetchOrCreateSpaceDirect({
password: spacePassword,
url,
name: vaultName,
platform,
});

if (!spaceResult.data) {
console.error("Failed to create space");
return null;
}

const spaceId = spaceResult.data.id;
const userId = await fetchOrCreatePlatformAccount({
platform: "Obsidian",
accountLocalId,
name: vaultName,
email: accountLocalId,
spaceId,
password: spacePassword,
});

contextCache = {
platform: "Obsidian",
spaceId,
userId,
spacePassword,
};
} catch (error) {
console.error(error);
return null;
}
}
return contextCache;
};

let loggedInClient: DGSupabaseClient | null = null;

export const getLoggedInClient = async (
plugin: DiscourseGraphPlugin,
): Promise<DGSupabaseClient | null> => {
if (loggedInClient === null) {
const context = await getSupabaseContext(plugin);
if (context === null) {
throw new Error("Could not create Supabase context");
}
try {
loggedInClient = await createLoggedInClient({
platform: context.platform,
spaceId: context.spaceId,
password: context.spacePassword,
});
if (!loggedInClient) {
throw new Error(
"Failed to create Supabase client - check environment variables",
);
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error("Failed to create logged-in client:", errorMessage);
throw new Error(`Supabase authentication failed: ${errorMessage}`);
}
} else {
// renew session
const { error } = await loggedInClient.auth.getSession();
if (error) {
console.warn("Session renewal failed, re-authenticating:", error);
loggedInClient = null;
const context = await getSupabaseContext(plugin);
if (context === null) {
throw new Error(
"Could not create Supabase context for re-authentication",
);
}

loggedInClient = await createLoggedInClient({
platform: context.platform,
spaceId: context.spaceId,
password: context.spacePassword,
});
if (!loggedInClient) {
throw new Error("Failed to re-authenticate Supabase client");
}
}
}
return loggedInClient;
};
15 changes: 15 additions & 0 deletions apps/obsidian/src/utils/syncDgNodesToSupabase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getSupabaseContext } from "./supabaseContext";
import type DiscourseGraphPlugin from "~/index";

export const initializeSupabaseSync = async (
plugin: DiscourseGraphPlugin,
): Promise<void> => {
const context = await getSupabaseContext(plugin);
if (!context) {
throw new Error("Failed to initialize Supabase sync: could not create context");
}
console.log("Supabase sync initialized successfully", {
spaceId: context.spaceId,
userId: context.userId,
});
};
10 changes: 5 additions & 5 deletions apps/roam/src/utils/supabaseContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,11 @@ export const getLoggedInClient = async (): Promise<DGSupabaseClient | null> => {
if (_loggedInClient === null) {
const context = await getSupabaseContext();
if (context === null) throw new Error("Could not create context");
_loggedInClient = await createLoggedInClient(
context.platform,
context.spaceId,
context.spacePassword,
);
_loggedInClient = await createLoggedInClient({
platform: context.platform,
spaceId: context.spaceId,
password: context.spacePassword,
});
} else {
// renew session
const { error } = await _loggedInClient.auth.getSession();
Expand Down
3 changes: 2 additions & 1 deletion packages/database/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
"@repo/utils": "workspace:*",
"@supabase/auth-js": "catalog:",
"@supabase/functions-js": "catalog:",
"@supabase/supabase-js": "catalog:"
"@supabase/supabase-js": "catalog:",
"tslib": "2.5.1"
},
"devDependencies": {
"@cucumber/cucumber": "^12.1.0",
Expand Down
Loading