refactor(graph): camelCase row keys and drop dead try/catch lazy import
Server: rows from kysely with camelCase plugin already arrive as sourcePageId / targetPageId / spaceId / spaceName. Drop the snake_case indexing and update the spec accordingly. Client: remove the unreachable try/catch around React.lazy for react-force-graph-2d — lazy() never throws synchronously, the catch was dead code from an earlier wip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
47dee1eb12
commit
11e003e71e
3 changed files with 54 additions and 65 deletions
|
|
@ -114,25 +114,12 @@ function spaceColor(spaceId: string): string {
|
|||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let ForceGraph2DLazy: React.LazyExoticComponent<React.ComponentType<any>> | null = null;
|
||||
|
||||
try {
|
||||
ForceGraph2DLazy = React.lazy(
|
||||
() =>
|
||||
// The cast via unknown is required because the module shape is not known
|
||||
// at compile time (lib not installed). Replace with a typed import once
|
||||
// `pnpm add react-force-graph-2d` is run.
|
||||
import(
|
||||
/* @vite-ignore */
|
||||
"react-force-graph-2d"
|
||||
) as Promise<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
default: React.ComponentType<any>;
|
||||
}>,
|
||||
);
|
||||
} catch {
|
||||
ForceGraph2DLazy = null;
|
||||
}
|
||||
const ForceGraph2DLazy: React.LazyExoticComponent<React.ComponentType<any>> | null =
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
React.lazy(() => import("react-force-graph-2d") as Promise<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
default: React.ComponentType<any>;
|
||||
}>);
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Fallback placeholder */
|
||||
|
|
|
|||
|
|
@ -34,9 +34,9 @@ const PAGE_CONTENT_UPDATED_EVENT = 'acadenice.page.content.updated';
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface BacklinkAggRow {
|
||||
source_page_id: string;
|
||||
target_page_id: string;
|
||||
link_type: LinkType;
|
||||
sourcePageId: string;
|
||||
targetPageId: string;
|
||||
linkType: LinkType;
|
||||
weight: number;
|
||||
}
|
||||
|
||||
|
|
@ -44,8 +44,8 @@ interface PageMetaRow {
|
|||
id: string;
|
||||
title: string | null;
|
||||
slug: string | null;
|
||||
space_id: string;
|
||||
space_name: string | null;
|
||||
spaceId: string;
|
||||
spaceName: string | null;
|
||||
icon: string | null;
|
||||
}
|
||||
|
||||
|
|
@ -206,8 +206,8 @@ export class GraphService {
|
|||
reachablePageIds = this.bfsReachable(rawEdges, pageId, depth);
|
||||
filteredEdges = rawEdges.filter(
|
||||
(e) =>
|
||||
reachablePageIds!.has(e.source_page_id) &&
|
||||
reachablePageIds!.has(e.target_page_id),
|
||||
reachablePageIds!.has(e.sourcePageId) &&
|
||||
reachablePageIds!.has(e.targetPageId),
|
||||
);
|
||||
} else {
|
||||
filteredEdges = rawEdges;
|
||||
|
|
@ -216,8 +216,8 @@ export class GraphService {
|
|||
// Step 3: Derive the set of page IDs referenced in the edges.
|
||||
const connectedPageIds = new Set<string>();
|
||||
for (const e of filteredEdges) {
|
||||
connectedPageIds.add(e.source_page_id);
|
||||
connectedPageIds.add(e.target_page_id);
|
||||
connectedPageIds.add(e.sourcePageId);
|
||||
connectedPageIds.add(e.targetPageId);
|
||||
}
|
||||
|
||||
// Step 4: Load page metadata for all referenced pages.
|
||||
|
|
@ -230,12 +230,12 @@ export class GraphService {
|
|||
const outDegreeMap = new Map<string, number>();
|
||||
for (const e of filteredEdges) {
|
||||
outDegreeMap.set(
|
||||
e.source_page_id,
|
||||
(outDegreeMap.get(e.source_page_id) ?? 0) + 1,
|
||||
e.sourcePageId,
|
||||
(outDegreeMap.get(e.sourcePageId) ?? 0) + 1,
|
||||
);
|
||||
inDegreeMap.set(
|
||||
e.target_page_id,
|
||||
(inDegreeMap.get(e.target_page_id) ?? 0) + 1,
|
||||
e.targetPageId,
|
||||
(inDegreeMap.get(e.targetPageId) ?? 0) + 1,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -254,19 +254,19 @@ export class GraphService {
|
|||
// Re-filter edges and re-compute degrees.
|
||||
filteredEdges = filteredEdges.filter(
|
||||
(e) =>
|
||||
finalPageIds.has(e.source_page_id) &&
|
||||
finalPageIds.has(e.target_page_id),
|
||||
finalPageIds.has(e.sourcePageId) &&
|
||||
finalPageIds.has(e.targetPageId),
|
||||
);
|
||||
inDegreeMap.clear();
|
||||
outDegreeMap.clear();
|
||||
for (const e of filteredEdges) {
|
||||
outDegreeMap.set(
|
||||
e.source_page_id,
|
||||
(outDegreeMap.get(e.source_page_id) ?? 0) + 1,
|
||||
e.sourcePageId,
|
||||
(outDegreeMap.get(e.sourcePageId) ?? 0) + 1,
|
||||
);
|
||||
inDegreeMap.set(
|
||||
e.target_page_id,
|
||||
(inDegreeMap.get(e.target_page_id) ?? 0) + 1,
|
||||
e.targetPageId,
|
||||
(inDegreeMap.get(e.targetPageId) ?? 0) + 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -311,8 +311,8 @@ export class GraphService {
|
|||
label: meta.title,
|
||||
slug: meta.slug,
|
||||
type: 'page',
|
||||
spaceId: meta.space_id,
|
||||
spaceName: meta.space_name,
|
||||
spaceId: meta.spaceId,
|
||||
spaceName: meta.spaceName,
|
||||
icon: meta.icon,
|
||||
isOrphan,
|
||||
metrics: { inDegree: inDeg, outDegree: outDeg },
|
||||
|
|
@ -320,10 +320,10 @@ export class GraphService {
|
|||
}
|
||||
|
||||
const edges: GraphEdge[] = filteredEdges.map((e) => ({
|
||||
id: `${e.source_page_id}:${e.target_page_id}:${e.link_type}`,
|
||||
source: e.source_page_id,
|
||||
target: e.target_page_id,
|
||||
type: e.link_type,
|
||||
id: `${e.sourcePageId}:${e.targetPageId}:${e.linkType}`,
|
||||
source: e.sourcePageId,
|
||||
target: e.targetPageId,
|
||||
type: e.linkType,
|
||||
weight: e.weight,
|
||||
}));
|
||||
|
||||
|
|
@ -369,12 +369,14 @@ export class GraphService {
|
|||
? sql`AND src_sp.id = ${spaceId}`
|
||||
: sql``;
|
||||
|
||||
// Aliases en camelCase: les requêtes sql`...` brutes ne passent pas
|
||||
// par le CamelCasePlugin Kysely (seul le query builder typé le fait).
|
||||
const rows = await sql<BacklinkAggRow>`
|
||||
SELECT
|
||||
bl.source_page_id,
|
||||
bl.target_page_id,
|
||||
bl.link_type,
|
||||
COUNT(*)::int AS weight
|
||||
bl.source_page_id AS "sourcePageId",
|
||||
bl.target_page_id AS "targetPageId",
|
||||
bl.link_type AS "linkType",
|
||||
COUNT(*)::int AS weight
|
||||
FROM acadenice_backlink bl
|
||||
-- Source page permission check
|
||||
JOIN pages src_p ON src_p.id = bl.source_page_id
|
||||
|
|
@ -442,8 +444,8 @@ export class GraphService {
|
|||
p.id,
|
||||
p.title,
|
||||
p.slug_id AS slug,
|
||||
p.space_id,
|
||||
sp.name AS space_name,
|
||||
p.space_id AS "spaceId",
|
||||
sp.name AS "spaceName",
|
||||
p.icon
|
||||
FROM pages p
|
||||
JOIN spaces sp ON sp.id = p.space_id
|
||||
|
|
@ -487,8 +489,8 @@ export class GraphService {
|
|||
p.id,
|
||||
p.title,
|
||||
p.slug_id AS slug,
|
||||
p.space_id,
|
||||
sp.name AS space_name,
|
||||
p.space_id AS "spaceId",
|
||||
sp.name AS "spaceName",
|
||||
p.icon
|
||||
FROM pages p
|
||||
JOIN spaces sp ON sp.id = p.space_id
|
||||
|
|
@ -537,9 +539,9 @@ export class GraphService {
|
|||
|
||||
const rows = await sql<BacklinkAggRow>`
|
||||
SELECT
|
||||
child.parent_page_id AS source_page_id,
|
||||
child.id AS target_page_id,
|
||||
'parent_child'::text AS link_type,
|
||||
child.parent_page_id AS "sourcePageId",
|
||||
child.id AS "targetPageId",
|
||||
'parent_child'::text AS "linkType",
|
||||
1::int AS weight
|
||||
FROM pages child
|
||||
-- Child page space
|
||||
|
|
@ -599,8 +601,8 @@ export class GraphService {
|
|||
adj.get(a)!.add(b);
|
||||
};
|
||||
for (const e of edges) {
|
||||
addEdge(e.source_page_id, e.target_page_id);
|
||||
addEdge(e.target_page_id, e.source_page_id);
|
||||
addEdge(e.sourcePageId, e.targetPageId);
|
||||
addEdge(e.targetPageId, e.sourcePageId);
|
||||
}
|
||||
|
||||
const visited = new Set<string>([rootId]);
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ function row(
|
|||
type: 'wikilink' | 'mention' | 'database_embed' = 'wikilink',
|
||||
weight = 1,
|
||||
) {
|
||||
return { source_page_id: source, target_page_id: target, link_type: type, weight };
|
||||
return { sourcePageId: source, targetPageId: target, linkType: type, weight };
|
||||
}
|
||||
|
||||
/** Factory for a mock PageMetaRow. */
|
||||
|
|
@ -32,7 +32,7 @@ function pageMeta(
|
|||
spaceId = 'space-1',
|
||||
spaceName = 'Space 1',
|
||||
) {
|
||||
return { id, title, space_id: spaceId, space_name: spaceName, icon: null };
|
||||
return { id, title, slug: null, spaceId, spaceName, icon: null };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -507,8 +507,8 @@ describe('GraphService', () => {
|
|||
// Backlink table is empty — only parent-child edges exist
|
||||
jest.spyOn(service as any, 'loadEdges').mockResolvedValue([]);
|
||||
jest.spyOn(service as any, 'loadParentChildEdges').mockResolvedValue([
|
||||
{ source_page_id: 'parent-1', target_page_id: 'child-1', link_type: 'parent_child', weight: 1 },
|
||||
{ source_page_id: 'parent-1', target_page_id: 'child-2', link_type: 'parent_child', weight: 1 },
|
||||
{ sourcePageId: 'parent-1', targetPageId: 'child-1', linkType: 'parent_child', weight: 1 },
|
||||
{ sourcePageId: 'parent-1', targetPageId: 'child-2', linkType: 'parent_child', weight: 1 },
|
||||
]);
|
||||
jest.spyOn(service as any, 'loadPageMeta').mockResolvedValue([
|
||||
pageMeta('parent-1', 'Parent'),
|
||||
|
|
@ -529,7 +529,7 @@ describe('GraphService', () => {
|
|||
it('parent_child edges carry correct source -> target direction', async () => {
|
||||
jest.spyOn(service as any, 'loadEdges').mockResolvedValue([]);
|
||||
jest.spyOn(service as any, 'loadParentChildEdges').mockResolvedValue([
|
||||
{ source_page_id: 'parent-1', target_page_id: 'child-1', link_type: 'parent_child', weight: 1 },
|
||||
{ sourcePageId: 'parent-1', targetPageId: 'child-1', linkType: 'parent_child', weight: 1 },
|
||||
]);
|
||||
jest.spyOn(service as any, 'loadPageMeta').mockResolvedValue([
|
||||
pageMeta('parent-1', 'Parent'),
|
||||
|
|
@ -583,7 +583,7 @@ describe('GraphService', () => {
|
|||
row('p1', 'p2', 'wikilink'),
|
||||
]);
|
||||
jest.spyOn(service as any, 'loadParentChildEdges').mockResolvedValue([
|
||||
{ source_page_id: 'p2', target_page_id: 'p3', link_type: 'parent_child', weight: 1 },
|
||||
{ sourcePageId: 'p2', targetPageId: 'p3', linkType: 'parent_child', weight: 1 },
|
||||
]);
|
||||
jest.spyOn(service as any, 'loadPageMeta').mockResolvedValue([
|
||||
pageMeta('p1'),
|
||||
|
|
@ -607,8 +607,8 @@ describe('GraphService', () => {
|
|||
jest.spyOn(service as any, 'loadParentChildEdges').mockResolvedValue([]);
|
||||
jest.spyOn(service as any, 'loadPageMeta').mockImplementation(
|
||||
async (..._args: any[]) => [
|
||||
{ id: 'p1', title: 'Page 1', slug: 'page-1-slug', space_id: 'sp-1', space_name: 'S', icon: null },
|
||||
{ id: 'p2', title: 'Page 2', slug: null, space_id: 'sp-1', space_name: 'S', icon: null },
|
||||
{ id: 'p1', title: 'Page 1', slug: 'page-1-slug', spaceId: 'sp-1', spaceName: 'S', icon: null },
|
||||
{ id: 'p2', title: 'Page 2', slug: null, spaceId: 'sp-1', spaceName: 'S', icon: null },
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue