From 11e003e71e2410d10478740c40440f1dcbb8a479 Mon Sep 17 00:00:00 2001 From: Corentin Date: Mon, 11 May 2026 09:54:39 +0000 Subject: [PATCH] refactor(graph): camelCase row keys and drop dead try/catch lazy import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../graph/components/graph-canvas.tsx | 25 ++---- .../acadenice/graph/services/graph.service.ts | 78 ++++++++++--------- .../graph/spec/graph.service.spec.ts | 16 ++-- 3 files changed, 54 insertions(+), 65 deletions(-) diff --git a/apps/client/src/features/acadenice/graph/components/graph-canvas.tsx b/apps/client/src/features/acadenice/graph/components/graph-canvas.tsx index f1a0afc9..79318aa7 100644 --- a/apps/client/src/features/acadenice/graph/components/graph-canvas.tsx +++ b/apps/client/src/features/acadenice/graph/components/graph-canvas.tsx @@ -114,25 +114,12 @@ function spaceColor(spaceId: string): string { /* -------------------------------------------------------------------------- */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -let ForceGraph2DLazy: React.LazyExoticComponent> | 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; - }>, - ); -} catch { - ForceGraph2DLazy = null; -} +const ForceGraph2DLazy: React.LazyExoticComponent> | 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; + }>); /* -------------------------------------------------------------------------- */ /* Fallback placeholder */ diff --git a/apps/server/src/core/acadenice/graph/services/graph.service.ts b/apps/server/src/core/acadenice/graph/services/graph.service.ts index 18b3ec7b..2e85b13e 100644 --- a/apps/server/src/core/acadenice/graph/services/graph.service.ts +++ b/apps/server/src/core/acadenice/graph/services/graph.service.ts @@ -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(); 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(); 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` 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` 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([rootId]); diff --git a/apps/server/src/core/acadenice/graph/spec/graph.service.spec.ts b/apps/server/src/core/acadenice/graph/spec/graph.service.spec.ts index 0733f1f5..9359051d 100644 --- a/apps/server/src/core/acadenice/graph/spec/graph.service.spec.ts +++ b/apps/server/src/core/acadenice/graph/spec/graph.service.spec.ts @@ -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 }, ], );