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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
let ForceGraph2DLazy: React.LazyExoticComponent<React.ComponentType<any>> | null = null;
|
const ForceGraph2DLazy: React.LazyExoticComponent<React.ComponentType<any>> | null =
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
try {
|
React.lazy(() => import("react-force-graph-2d") as Promise<{
|
||||||
ForceGraph2DLazy = React.lazy(
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
() =>
|
default: React.ComponentType<any>;
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* Fallback placeholder */
|
/* Fallback placeholder */
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,9 @@ const PAGE_CONTENT_UPDATED_EVENT = 'acadenice.page.content.updated';
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
interface BacklinkAggRow {
|
interface BacklinkAggRow {
|
||||||
source_page_id: string;
|
sourcePageId: string;
|
||||||
target_page_id: string;
|
targetPageId: string;
|
||||||
link_type: LinkType;
|
linkType: LinkType;
|
||||||
weight: number;
|
weight: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,8 +44,8 @@ interface PageMetaRow {
|
||||||
id: string;
|
id: string;
|
||||||
title: string | null;
|
title: string | null;
|
||||||
slug: string | null;
|
slug: string | null;
|
||||||
space_id: string;
|
spaceId: string;
|
||||||
space_name: string | null;
|
spaceName: string | null;
|
||||||
icon: string | null;
|
icon: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -206,8 +206,8 @@ export class GraphService {
|
||||||
reachablePageIds = this.bfsReachable(rawEdges, pageId, depth);
|
reachablePageIds = this.bfsReachable(rawEdges, pageId, depth);
|
||||||
filteredEdges = rawEdges.filter(
|
filteredEdges = rawEdges.filter(
|
||||||
(e) =>
|
(e) =>
|
||||||
reachablePageIds!.has(e.source_page_id) &&
|
reachablePageIds!.has(e.sourcePageId) &&
|
||||||
reachablePageIds!.has(e.target_page_id),
|
reachablePageIds!.has(e.targetPageId),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
filteredEdges = rawEdges;
|
filteredEdges = rawEdges;
|
||||||
|
|
@ -216,8 +216,8 @@ export class GraphService {
|
||||||
// Step 3: Derive the set of page IDs referenced in the edges.
|
// Step 3: Derive the set of page IDs referenced in the edges.
|
||||||
const connectedPageIds = new Set<string>();
|
const connectedPageIds = new Set<string>();
|
||||||
for (const e of filteredEdges) {
|
for (const e of filteredEdges) {
|
||||||
connectedPageIds.add(e.source_page_id);
|
connectedPageIds.add(e.sourcePageId);
|
||||||
connectedPageIds.add(e.target_page_id);
|
connectedPageIds.add(e.targetPageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: Load page metadata for all referenced pages.
|
// Step 4: Load page metadata for all referenced pages.
|
||||||
|
|
@ -230,12 +230,12 @@ export class GraphService {
|
||||||
const outDegreeMap = new Map<string, number>();
|
const outDegreeMap = new Map<string, number>();
|
||||||
for (const e of filteredEdges) {
|
for (const e of filteredEdges) {
|
||||||
outDegreeMap.set(
|
outDegreeMap.set(
|
||||||
e.source_page_id,
|
e.sourcePageId,
|
||||||
(outDegreeMap.get(e.source_page_id) ?? 0) + 1,
|
(outDegreeMap.get(e.sourcePageId) ?? 0) + 1,
|
||||||
);
|
);
|
||||||
inDegreeMap.set(
|
inDegreeMap.set(
|
||||||
e.target_page_id,
|
e.targetPageId,
|
||||||
(inDegreeMap.get(e.target_page_id) ?? 0) + 1,
|
(inDegreeMap.get(e.targetPageId) ?? 0) + 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -254,19 +254,19 @@ export class GraphService {
|
||||||
// Re-filter edges and re-compute degrees.
|
// Re-filter edges and re-compute degrees.
|
||||||
filteredEdges = filteredEdges.filter(
|
filteredEdges = filteredEdges.filter(
|
||||||
(e) =>
|
(e) =>
|
||||||
finalPageIds.has(e.source_page_id) &&
|
finalPageIds.has(e.sourcePageId) &&
|
||||||
finalPageIds.has(e.target_page_id),
|
finalPageIds.has(e.targetPageId),
|
||||||
);
|
);
|
||||||
inDegreeMap.clear();
|
inDegreeMap.clear();
|
||||||
outDegreeMap.clear();
|
outDegreeMap.clear();
|
||||||
for (const e of filteredEdges) {
|
for (const e of filteredEdges) {
|
||||||
outDegreeMap.set(
|
outDegreeMap.set(
|
||||||
e.source_page_id,
|
e.sourcePageId,
|
||||||
(outDegreeMap.get(e.source_page_id) ?? 0) + 1,
|
(outDegreeMap.get(e.sourcePageId) ?? 0) + 1,
|
||||||
);
|
);
|
||||||
inDegreeMap.set(
|
inDegreeMap.set(
|
||||||
e.target_page_id,
|
e.targetPageId,
|
||||||
(inDegreeMap.get(e.target_page_id) ?? 0) + 1,
|
(inDegreeMap.get(e.targetPageId) ?? 0) + 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -311,8 +311,8 @@ export class GraphService {
|
||||||
label: meta.title,
|
label: meta.title,
|
||||||
slug: meta.slug,
|
slug: meta.slug,
|
||||||
type: 'page',
|
type: 'page',
|
||||||
spaceId: meta.space_id,
|
spaceId: meta.spaceId,
|
||||||
spaceName: meta.space_name,
|
spaceName: meta.spaceName,
|
||||||
icon: meta.icon,
|
icon: meta.icon,
|
||||||
isOrphan,
|
isOrphan,
|
||||||
metrics: { inDegree: inDeg, outDegree: outDeg },
|
metrics: { inDegree: inDeg, outDegree: outDeg },
|
||||||
|
|
@ -320,10 +320,10 @@ export class GraphService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const edges: GraphEdge[] = filteredEdges.map((e) => ({
|
const edges: GraphEdge[] = filteredEdges.map((e) => ({
|
||||||
id: `${e.source_page_id}:${e.target_page_id}:${e.link_type}`,
|
id: `${e.sourcePageId}:${e.targetPageId}:${e.linkType}`,
|
||||||
source: e.source_page_id,
|
source: e.sourcePageId,
|
||||||
target: e.target_page_id,
|
target: e.targetPageId,
|
||||||
type: e.link_type,
|
type: e.linkType,
|
||||||
weight: e.weight,
|
weight: e.weight,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -369,12 +369,14 @@ export class GraphService {
|
||||||
? sql`AND src_sp.id = ${spaceId}`
|
? sql`AND src_sp.id = ${spaceId}`
|
||||||
: sql``;
|
: 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>`
|
const rows = await sql<BacklinkAggRow>`
|
||||||
SELECT
|
SELECT
|
||||||
bl.source_page_id,
|
bl.source_page_id AS "sourcePageId",
|
||||||
bl.target_page_id,
|
bl.target_page_id AS "targetPageId",
|
||||||
bl.link_type,
|
bl.link_type AS "linkType",
|
||||||
COUNT(*)::int AS weight
|
COUNT(*)::int AS weight
|
||||||
FROM acadenice_backlink bl
|
FROM acadenice_backlink bl
|
||||||
-- Source page permission check
|
-- Source page permission check
|
||||||
JOIN pages src_p ON src_p.id = bl.source_page_id
|
JOIN pages src_p ON src_p.id = bl.source_page_id
|
||||||
|
|
@ -442,8 +444,8 @@ export class GraphService {
|
||||||
p.id,
|
p.id,
|
||||||
p.title,
|
p.title,
|
||||||
p.slug_id AS slug,
|
p.slug_id AS slug,
|
||||||
p.space_id,
|
p.space_id AS "spaceId",
|
||||||
sp.name AS space_name,
|
sp.name AS "spaceName",
|
||||||
p.icon
|
p.icon
|
||||||
FROM pages p
|
FROM pages p
|
||||||
JOIN spaces sp ON sp.id = p.space_id
|
JOIN spaces sp ON sp.id = p.space_id
|
||||||
|
|
@ -487,8 +489,8 @@ export class GraphService {
|
||||||
p.id,
|
p.id,
|
||||||
p.title,
|
p.title,
|
||||||
p.slug_id AS slug,
|
p.slug_id AS slug,
|
||||||
p.space_id,
|
p.space_id AS "spaceId",
|
||||||
sp.name AS space_name,
|
sp.name AS "spaceName",
|
||||||
p.icon
|
p.icon
|
||||||
FROM pages p
|
FROM pages p
|
||||||
JOIN spaces sp ON sp.id = p.space_id
|
JOIN spaces sp ON sp.id = p.space_id
|
||||||
|
|
@ -537,9 +539,9 @@ export class GraphService {
|
||||||
|
|
||||||
const rows = await sql<BacklinkAggRow>`
|
const rows = await sql<BacklinkAggRow>`
|
||||||
SELECT
|
SELECT
|
||||||
child.parent_page_id AS source_page_id,
|
child.parent_page_id AS "sourcePageId",
|
||||||
child.id AS target_page_id,
|
child.id AS "targetPageId",
|
||||||
'parent_child'::text AS link_type,
|
'parent_child'::text AS "linkType",
|
||||||
1::int AS weight
|
1::int AS weight
|
||||||
FROM pages child
|
FROM pages child
|
||||||
-- Child page space
|
-- Child page space
|
||||||
|
|
@ -599,8 +601,8 @@ export class GraphService {
|
||||||
adj.get(a)!.add(b);
|
adj.get(a)!.add(b);
|
||||||
};
|
};
|
||||||
for (const e of edges) {
|
for (const e of edges) {
|
||||||
addEdge(e.source_page_id, e.target_page_id);
|
addEdge(e.sourcePageId, e.targetPageId);
|
||||||
addEdge(e.target_page_id, e.source_page_id);
|
addEdge(e.targetPageId, e.sourcePageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const visited = new Set<string>([rootId]);
|
const visited = new Set<string>([rootId]);
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ function row(
|
||||||
type: 'wikilink' | 'mention' | 'database_embed' = 'wikilink',
|
type: 'wikilink' | 'mention' | 'database_embed' = 'wikilink',
|
||||||
weight = 1,
|
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. */
|
/** Factory for a mock PageMetaRow. */
|
||||||
|
|
@ -32,7 +32,7 @@ function pageMeta(
|
||||||
spaceId = 'space-1',
|
spaceId = 'space-1',
|
||||||
spaceName = '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
|
// Backlink table is empty — only parent-child edges exist
|
||||||
jest.spyOn(service as any, 'loadEdges').mockResolvedValue([]);
|
jest.spyOn(service as any, 'loadEdges').mockResolvedValue([]);
|
||||||
jest.spyOn(service as any, 'loadParentChildEdges').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 },
|
||||||
{ source_page_id: 'parent-1', target_page_id: 'child-2', link_type: 'parent_child', weight: 1 },
|
{ sourcePageId: 'parent-1', targetPageId: 'child-2', linkType: 'parent_child', weight: 1 },
|
||||||
]);
|
]);
|
||||||
jest.spyOn(service as any, 'loadPageMeta').mockResolvedValue([
|
jest.spyOn(service as any, 'loadPageMeta').mockResolvedValue([
|
||||||
pageMeta('parent-1', 'Parent'),
|
pageMeta('parent-1', 'Parent'),
|
||||||
|
|
@ -529,7 +529,7 @@ describe('GraphService', () => {
|
||||||
it('parent_child edges carry correct source -> target direction', async () => {
|
it('parent_child edges carry correct source -> target direction', async () => {
|
||||||
jest.spyOn(service as any, 'loadEdges').mockResolvedValue([]);
|
jest.spyOn(service as any, 'loadEdges').mockResolvedValue([]);
|
||||||
jest.spyOn(service as any, 'loadParentChildEdges').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([
|
jest.spyOn(service as any, 'loadPageMeta').mockResolvedValue([
|
||||||
pageMeta('parent-1', 'Parent'),
|
pageMeta('parent-1', 'Parent'),
|
||||||
|
|
@ -583,7 +583,7 @@ describe('GraphService', () => {
|
||||||
row('p1', 'p2', 'wikilink'),
|
row('p1', 'p2', 'wikilink'),
|
||||||
]);
|
]);
|
||||||
jest.spyOn(service as any, 'loadParentChildEdges').mockResolvedValue([
|
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([
|
jest.spyOn(service as any, 'loadPageMeta').mockResolvedValue([
|
||||||
pageMeta('p1'),
|
pageMeta('p1'),
|
||||||
|
|
@ -607,8 +607,8 @@ describe('GraphService', () => {
|
||||||
jest.spyOn(service as any, 'loadParentChildEdges').mockResolvedValue([]);
|
jest.spyOn(service as any, 'loadParentChildEdges').mockResolvedValue([]);
|
||||||
jest.spyOn(service as any, 'loadPageMeta').mockImplementation(
|
jest.spyOn(service as any, 'loadPageMeta').mockImplementation(
|
||||||
async (..._args: any[]) => [
|
async (..._args: any[]) => [
|
||||||
{ id: 'p1', title: 'Page 1', slug: 'page-1-slug', 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, space_id: 'sp-1', space_name: '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