Skip to content

Commit 1245d1f

Browse files
authored
ENG-451: cleanup orphaned nodes from supabase (#233)
* cleanup orphaned nodes * address review
1 parent 314e9f5 commit 1245d1f

3 files changed

Lines changed: 208 additions & 0 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { getSupabaseContext } from "./supabaseContext";
2+
import { getNodeEnv } from "roamjs-components/util/env";
3+
4+
const getAllNodesFromSupabase = async (): Promise<string[]> => {
5+
try {
6+
const context = await getSupabaseContext();
7+
if (!context) {
8+
console.error("Failed to get Supabase context");
9+
return [];
10+
}
11+
12+
const baseUrl =
13+
getNodeEnv() === "development"
14+
? "http://localhost:3000/api/supabase"
15+
: "https://discoursegraphs.com/api/supabase";
16+
17+
const getNodesResponse = await fetch(`${baseUrl}/get-all-discourse-nodes`, {
18+
method: "POST",
19+
headers: {
20+
"Content-Type": "application/json",
21+
},
22+
body: JSON.stringify({
23+
spaceId: context.spaceId,
24+
}),
25+
});
26+
27+
if (!getNodesResponse.ok) {
28+
const errorText = await getNodesResponse.text();
29+
console.error(
30+
`Failed to fetch nodes from Supabase: ${getNodesResponse.status} ${errorText}`,
31+
);
32+
return [];
33+
}
34+
35+
const supabaseUids = (await getNodesResponse.json()) as string[];
36+
return supabaseUids;
37+
} catch (error) {
38+
console.error("Error in getAllNodesFromSupabase:", error);
39+
return [];
40+
}
41+
};
42+
43+
const getNonExistentRoamUids = (nodeUids: string[]): string[] => {
44+
try {
45+
if (nodeUids.length === 0) {
46+
return [];
47+
}
48+
49+
const results = window.roamAlphaAPI.q(
50+
`[:find ?uid
51+
:in $ [?uid ...]
52+
:where (not [_ :block/uid ?uid])]`,
53+
nodeUids,
54+
) as string[][];
55+
56+
return results.map(([uid]) => uid);
57+
} catch (error) {
58+
console.error("Error checking existing Roam nodes:", error);
59+
return [];
60+
}
61+
};
62+
63+
const deleteNodesFromSupabase = async (uids: string[]): Promise<number> => {
64+
try {
65+
const context = await getSupabaseContext();
66+
if (!context) {
67+
console.error("Failed to get Supabase context");
68+
return 0;
69+
}
70+
71+
const baseUrl =
72+
getNodeEnv() === "development"
73+
? "http://localhost:3000/api/supabase"
74+
: "https://discoursegraphs.com/api/supabase";
75+
76+
const deleteNodesResponse = await fetch(`${baseUrl}/delete-discourse-nodes`, {
77+
method: "POST",
78+
headers: {
79+
"Content-Type": "application/json",
80+
},
81+
body: JSON.stringify({
82+
spaceId: context.spaceId,
83+
uids,
84+
}),
85+
});
86+
87+
if (!deleteNodesResponse.ok) {
88+
const errorText = await deleteNodesResponse.text();
89+
console.error(
90+
`Failed to delete nodes from Supabase: ${deleteNodesResponse.status} ${errorText}`,
91+
);
92+
return 0;
93+
}
94+
95+
const { count } = await deleteNodesResponse.json();
96+
return count;
97+
} catch (error) {
98+
console.error("Error in deleteNodesFromSupabase:", error);
99+
return 0;
100+
}
101+
};
102+
103+
export const cleanupOrphanedNodes = async (): Promise<void> => {
104+
try {
105+
const supabaseUids = await getAllNodesFromSupabase();
106+
if (supabaseUids.length === 0) {
107+
return;
108+
}
109+
const orphanedUids = getNonExistentRoamUids(supabaseUids);
110+
if (orphanedUids.length === 0) {
111+
return;
112+
}
113+
await deleteNodesFromSupabase(orphanedUids);
114+
} catch (error) {
115+
console.error("Error in cleanupOrphanedNodes:", error);
116+
}
117+
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { createClient } from "~/utils/supabase/server";
3+
import {
4+
createApiResponse,
5+
handleRouteError,
6+
defaultOptionsHandler,
7+
asPostgrestFailure,
8+
} from "~/utils/supabase/apiUtils";
9+
import cors from "~/utils/llm/cors";
10+
11+
type DeleteNodesRequest = {
12+
spaceId: number;
13+
uids: string[];
14+
};
15+
16+
export const POST = async (request: NextRequest): Promise<NextResponse> => {
17+
try {
18+
const body: DeleteNodesRequest = await request.json();
19+
const { spaceId, uids } = body;
20+
const supabase = await createClient();
21+
22+
// TODO - Later we need to delete a discoursenode's concept, content and document
23+
const { error, count } = await supabase
24+
.from("Document")
25+
.delete({ count: "exact" })
26+
.eq("space_id", spaceId)
27+
.in("source_local_id", uids);
28+
29+
if (error) {
30+
return createApiResponse(
31+
request,
32+
asPostgrestFailure(error.message, error.code, 500),
33+
);
34+
}
35+
36+
const response = NextResponse.json({ count }, { status: 200 });
37+
return cors(request, response) as NextResponse;
38+
} catch (e: unknown) {
39+
return handleRouteError(request, e, "/api/supabase/delete-discourse-nodes");
40+
}
41+
};
42+
43+
export const OPTIONS = defaultOptionsHandler;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { createClient } from "~/utils/supabase/server";
3+
import {
4+
createApiResponse,
5+
handleRouteError,
6+
defaultOptionsHandler,
7+
} from "~/utils/supabase/apiUtils";
8+
import cors from "~/utils/llm/cors";
9+
10+
type NodesRequest = {
11+
spaceId: number;
12+
};
13+
14+
export const POST = async (request: NextRequest): Promise<NextResponse> => {
15+
try {
16+
const body: NodesRequest = await request.json();
17+
const { spaceId } = body;
18+
19+
const supabase = await createClient();
20+
21+
// TODO - Later when we have discourse relations in Supabase, this function should only get the discourse nodes
22+
const documentsResponse = await supabase
23+
.from("Document")
24+
.select("source_local_id")
25+
.eq("space_id", spaceId)
26+
.not("source_local_id", "is", null);
27+
28+
if (documentsResponse.error) {
29+
return createApiResponse(request, documentsResponse);
30+
}
31+
32+
const result =
33+
documentsResponse.data
34+
?.filter((doc) => doc.source_local_id)
35+
.map((doc) => doc.source_local_id) || [];
36+
37+
const response = NextResponse.json(result, { status: 200 });
38+
return cors(request, response) as NextResponse;
39+
} catch (e: unknown) {
40+
return handleRouteError(
41+
request,
42+
e,
43+
"/api/supabase/get-all-discourse-nodes",
44+
);
45+
}
46+
};
47+
48+
export const OPTIONS = defaultOptionsHandler;

0 commit comments

Comments
 (0)