Kunnskapsgraf CRUD API: entities, edges, søk og traversering
Syv nye API-endepunkter for kunnskapsgrafen: Entities: - GET/POST /api/entities — list, søk (name+aliases), filtrer på type - GET/PATCH/DELETE /api/entities/:id — hent (m/edge_count), oppdater, slett - GET /api/entities/:id/edges — relasjoner med retningsfilter Graf: - POST /api/graph/edges — opprett relasjon (upsert) - DELETE /api/graph/edges/:id — slett relasjon - GET /api/graph/search — fulltekstsøk (entiteter + transkripsjoner FTS) - GET /api/graph/traverse/:nodeId — recursive CTE, D3.js/Vis.js-format Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1faef972dd
commit
1d47119b1e
7 changed files with 418 additions and 0 deletions
83
web/src/routes/api/entities/+server.ts
Normal file
83
web/src/routes/api/entities/+server.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { json, error } from '@sveltejs/kit';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { sql } from '$lib/server/db';
|
||||||
|
|
||||||
|
/** GET /api/entities?q=...&type=...&limit=... — List/søk entiteter */
|
||||||
|
export const GET: RequestHandler = async ({ url, locals }) => {
|
||||||
|
if (!locals.workspace || !locals.user) error(401);
|
||||||
|
|
||||||
|
const q = url.searchParams.get('q')?.trim();
|
||||||
|
const type = url.searchParams.get('type');
|
||||||
|
const limit = Math.min(Number(url.searchParams.get('limit') ?? 50), 100);
|
||||||
|
|
||||||
|
let entities;
|
||||||
|
if (q) {
|
||||||
|
// Søk i name og aliases
|
||||||
|
entities = await sql`
|
||||||
|
SELECT e.id, e.name, e.type, e.aliases, e.avatar_url, n.created_at, n.updated_at
|
||||||
|
FROM entities e
|
||||||
|
JOIN nodes n ON n.id = e.id
|
||||||
|
WHERE n.workspace_id = ${locals.workspace.id}
|
||||||
|
AND (
|
||||||
|
e.name ILIKE ${'%' + q + '%'}
|
||||||
|
OR EXISTS (SELECT 1 FROM unnest(e.aliases) AS a WHERE a ILIKE ${'%' + q + '%'})
|
||||||
|
)
|
||||||
|
${type ? sql`AND e.type = ${type}` : sql``}
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN e.name ILIKE ${q + '%'} THEN 0 ELSE 1 END,
|
||||||
|
e.name
|
||||||
|
LIMIT ${limit}
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
entities = await sql`
|
||||||
|
SELECT e.id, e.name, e.type, e.aliases, e.avatar_url, n.created_at, n.updated_at
|
||||||
|
FROM entities e
|
||||||
|
JOIN nodes n ON n.id = e.id
|
||||||
|
WHERE n.workspace_id = ${locals.workspace.id}
|
||||||
|
${type ? sql`AND e.type = ${type}` : sql``}
|
||||||
|
ORDER BY e.name
|
||||||
|
LIMIT ${limit}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return json(entities);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** POST /api/entities — Opprett entitet */
|
||||||
|
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||||
|
if (!locals.workspace || !locals.user) error(401);
|
||||||
|
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
if (!body.name?.trim()) error(400, 'name er påkrevd');
|
||||||
|
if (!body.type?.trim()) error(400, 'type er påkrevd');
|
||||||
|
|
||||||
|
const validTypes = ['person', 'organisasjon', 'sted', 'tema', 'konsept'];
|
||||||
|
if (!validTypes.includes(body.type)) {
|
||||||
|
error(400, `type må være en av: ${validTypes.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [entity] = await sql`
|
||||||
|
WITH new_node AS (
|
||||||
|
INSERT INTO nodes (workspace_id, node_type)
|
||||||
|
VALUES (${locals.workspace.id}, 'entitet')
|
||||||
|
RETURNING id, created_at, updated_at
|
||||||
|
),
|
||||||
|
new_entity AS (
|
||||||
|
INSERT INTO entities (id, name, type, aliases, avatar_url)
|
||||||
|
SELECT
|
||||||
|
new_node.id,
|
||||||
|
${body.name.trim()},
|
||||||
|
${body.type},
|
||||||
|
${body.aliases ?? []},
|
||||||
|
${body.avatar_url ?? null}
|
||||||
|
FROM new_node
|
||||||
|
RETURNING id, name, type, aliases, avatar_url
|
||||||
|
)
|
||||||
|
SELECT ne.*, nn.created_at, nn.updated_at
|
||||||
|
FROM new_entity ne
|
||||||
|
JOIN new_node nn ON nn.id = ne.id
|
||||||
|
`;
|
||||||
|
|
||||||
|
return json(entity, { status: 201 });
|
||||||
|
};
|
||||||
73
web/src/routes/api/entities/[entityId]/+server.ts
Normal file
73
web/src/routes/api/entities/[entityId]/+server.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { json, error } from '@sveltejs/kit';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { sql } from '$lib/server/db';
|
||||||
|
|
||||||
|
/** GET /api/entities/:entityId — Hent entitet med edge-count */
|
||||||
|
export const GET: RequestHandler = async ({ params, locals }) => {
|
||||||
|
if (!locals.workspace || !locals.user) error(401);
|
||||||
|
|
||||||
|
const [entity] = await sql`
|
||||||
|
SELECT e.id, e.name, e.type, e.aliases, e.avatar_url,
|
||||||
|
n.created_at, n.updated_at,
|
||||||
|
(SELECT COUNT(*) FROM graph_edges ge
|
||||||
|
WHERE ge.source_id = e.id OR ge.target_id = e.id) AS edge_count
|
||||||
|
FROM entities e
|
||||||
|
JOIN nodes n ON n.id = e.id
|
||||||
|
WHERE e.id = ${params.entityId} AND n.workspace_id = ${locals.workspace.id}
|
||||||
|
`;
|
||||||
|
if (!entity) error(404, 'Entitet ikke funnet');
|
||||||
|
|
||||||
|
return json(entity);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** PATCH /api/entities/:entityId — Oppdater entitet */
|
||||||
|
export const PATCH: RequestHandler = async ({ params, request, locals }) => {
|
||||||
|
if (!locals.workspace || !locals.user) error(401);
|
||||||
|
|
||||||
|
const updates = await request.json();
|
||||||
|
|
||||||
|
const [existing] = await sql`
|
||||||
|
SELECT e.id FROM entities e
|
||||||
|
JOIN nodes n ON n.id = e.id
|
||||||
|
WHERE e.id = ${params.entityId} AND n.workspace_id = ${locals.workspace.id}
|
||||||
|
`;
|
||||||
|
if (!existing) error(404, 'Entitet ikke funnet');
|
||||||
|
|
||||||
|
if (updates.type) {
|
||||||
|
const validTypes = ['person', 'organisasjon', 'sted', 'tema', 'konsept'];
|
||||||
|
if (!validTypes.includes(updates.type)) {
|
||||||
|
error(400, `type må være en av: ${validTypes.join(', ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [updated] = await sql`
|
||||||
|
UPDATE entities SET
|
||||||
|
name = COALESCE(${updates.name?.trim() ?? null}, name),
|
||||||
|
type = COALESCE(${updates.type ?? null}, type),
|
||||||
|
aliases = CASE WHEN ${updates.aliases !== undefined} THEN ${updates.aliases ?? []} ELSE aliases END,
|
||||||
|
avatar_url = CASE WHEN ${updates.avatar_url !== undefined} THEN ${updates.avatar_url ?? null} ELSE avatar_url END
|
||||||
|
WHERE id = ${params.entityId}
|
||||||
|
RETURNING id, name, type, aliases, avatar_url,
|
||||||
|
(SELECT created_at FROM nodes WHERE id = entities.id) AS created_at,
|
||||||
|
(SELECT updated_at FROM nodes WHERE id = entities.id) AS updated_at
|
||||||
|
`;
|
||||||
|
|
||||||
|
return json(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** DELETE /api/entities/:entityId — Slett entitet */
|
||||||
|
export const DELETE: RequestHandler = async ({ params, locals }) => {
|
||||||
|
if (!locals.workspace || !locals.user) error(401);
|
||||||
|
|
||||||
|
const [entity] = await sql`
|
||||||
|
SELECT e.id FROM entities e
|
||||||
|
JOIN nodes n ON n.id = e.id
|
||||||
|
WHERE e.id = ${params.entityId} AND n.workspace_id = ${locals.workspace.id}
|
||||||
|
`;
|
||||||
|
if (!entity) error(404, 'Entitet ikke funnet');
|
||||||
|
|
||||||
|
// Slett node (cascader til entities + graph_edges)
|
||||||
|
await sql`DELETE FROM nodes WHERE id = ${params.entityId}`;
|
||||||
|
|
||||||
|
return new Response(null, { status: 204 });
|
||||||
|
};
|
||||||
58
web/src/routes/api/entities/[entityId]/edges/+server.ts
Normal file
58
web/src/routes/api/entities/[entityId]/edges/+server.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { json, error } from '@sveltejs/kit';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { sql } from '$lib/server/db';
|
||||||
|
|
||||||
|
/** GET /api/entities/:entityId/edges — Hent alle relasjoner for en entitet */
|
||||||
|
export const GET: RequestHandler = async ({ params, url, locals }) => {
|
||||||
|
if (!locals.workspace || !locals.user) error(401);
|
||||||
|
|
||||||
|
const entityId = params.entityId;
|
||||||
|
const direction = url.searchParams.get('direction'); // 'outgoing', 'incoming', null=both
|
||||||
|
const relationType = url.searchParams.get('relation_type');
|
||||||
|
|
||||||
|
// Verifiser tilgang
|
||||||
|
const [entity] = await sql`
|
||||||
|
SELECT e.id FROM entities e
|
||||||
|
JOIN nodes n ON n.id = e.id
|
||||||
|
WHERE e.id = ${entityId} AND n.workspace_id = ${locals.workspace.id}
|
||||||
|
`;
|
||||||
|
if (!entity) error(404, 'Entitet ikke funnet');
|
||||||
|
|
||||||
|
const edges = await sql`
|
||||||
|
SELECT
|
||||||
|
ge.id AS edge_id,
|
||||||
|
ge.source_id,
|
||||||
|
ge.target_id,
|
||||||
|
ge.relation_type,
|
||||||
|
ge.confidence,
|
||||||
|
ge.origin,
|
||||||
|
ge.created_at,
|
||||||
|
rt.label AS relation_label,
|
||||||
|
CASE
|
||||||
|
WHEN ge.source_id = ${entityId} THEN target_e.name
|
||||||
|
ELSE source_e.name
|
||||||
|
END AS connected_name,
|
||||||
|
CASE
|
||||||
|
WHEN ge.source_id = ${entityId} THEN target_e.type
|
||||||
|
ELSE source_e.type
|
||||||
|
END AS connected_type,
|
||||||
|
CASE
|
||||||
|
WHEN ge.source_id = ${entityId} THEN ge.target_id
|
||||||
|
ELSE ge.source_id
|
||||||
|
END AS connected_id
|
||||||
|
FROM graph_edges ge
|
||||||
|
JOIN relation_types rt ON rt.name = ge.relation_type
|
||||||
|
LEFT JOIN entities source_e ON source_e.id = ge.source_id
|
||||||
|
LEFT JOIN entities target_e ON target_e.id = ge.target_id
|
||||||
|
WHERE ge.workspace_id = ${locals.workspace.id}
|
||||||
|
AND (
|
||||||
|
${direction === 'outgoing' ? sql`ge.source_id = ${entityId}` :
|
||||||
|
direction === 'incoming' ? sql`ge.target_id = ${entityId}` :
|
||||||
|
sql`(ge.source_id = ${entityId} OR ge.target_id = ${entityId})`}
|
||||||
|
)
|
||||||
|
${relationType ? sql`AND ge.relation_type = ${relationType}` : sql``}
|
||||||
|
ORDER BY ge.created_at DESC
|
||||||
|
`;
|
||||||
|
|
||||||
|
return json(edges);
|
||||||
|
};
|
||||||
50
web/src/routes/api/graph/edges/+server.ts
Normal file
50
web/src/routes/api/graph/edges/+server.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { json, error } from '@sveltejs/kit';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { sql } from '$lib/server/db';
|
||||||
|
|
||||||
|
/** POST /api/graph/edges — Opprett graf-relasjon */
|
||||||
|
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||||
|
if (!locals.workspace || !locals.user) error(401);
|
||||||
|
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
if (!body.source_id || !body.target_id || !body.relation_type) {
|
||||||
|
error(400, 'source_id, target_id og relation_type er påkrevd');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.source_id === body.target_id) {
|
||||||
|
error(400, 'En node kan ikke relateres til seg selv');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifiser at begge noder tilhører workspace
|
||||||
|
const nodes = await sql`
|
||||||
|
SELECT id FROM nodes
|
||||||
|
WHERE id IN (${body.source_id}, ${body.target_id})
|
||||||
|
AND workspace_id = ${locals.workspace.id}
|
||||||
|
`;
|
||||||
|
if (nodes.length !== 2) error(404, 'En eller begge noder ikke funnet i workspace');
|
||||||
|
|
||||||
|
// Verifiser at relation_type er gyldig
|
||||||
|
const [relType] = await sql`
|
||||||
|
SELECT name FROM relation_types WHERE name = ${body.relation_type}
|
||||||
|
`;
|
||||||
|
if (!relType) error(400, `Ugyldig relation_type: ${body.relation_type}`);
|
||||||
|
|
||||||
|
const [edge] = await sql`
|
||||||
|
INSERT INTO graph_edges (workspace_id, source_id, target_id, relation_type, confidence, created_by, origin)
|
||||||
|
VALUES (
|
||||||
|
${locals.workspace.id},
|
||||||
|
${body.source_id},
|
||||||
|
${body.target_id},
|
||||||
|
${body.relation_type},
|
||||||
|
${body.confidence ?? null},
|
||||||
|
${locals.user.id},
|
||||||
|
${body.origin ?? 'user'}
|
||||||
|
)
|
||||||
|
ON CONFLICT (source_id, target_id, relation_type) DO UPDATE SET
|
||||||
|
confidence = COALESCE(EXCLUDED.confidence, graph_edges.confidence)
|
||||||
|
RETURNING id, source_id, target_id, relation_type, confidence, origin, created_at
|
||||||
|
`;
|
||||||
|
|
||||||
|
return json(edge, { status: 201 });
|
||||||
|
};
|
||||||
18
web/src/routes/api/graph/edges/[edgeId]/+server.ts
Normal file
18
web/src/routes/api/graph/edges/[edgeId]/+server.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { json, error } from '@sveltejs/kit';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { sql } from '$lib/server/db';
|
||||||
|
|
||||||
|
/** DELETE /api/graph/edges/:edgeId — Slett graf-relasjon */
|
||||||
|
export const DELETE: RequestHandler = async ({ params, locals }) => {
|
||||||
|
if (!locals.workspace || !locals.user) error(401);
|
||||||
|
|
||||||
|
const [edge] = await sql`
|
||||||
|
SELECT id FROM graph_edges
|
||||||
|
WHERE id = ${params.edgeId} AND workspace_id = ${locals.workspace.id}
|
||||||
|
`;
|
||||||
|
if (!edge) error(404, 'Relasjon ikke funnet');
|
||||||
|
|
||||||
|
await sql`DELETE FROM graph_edges WHERE id = ${params.edgeId}`;
|
||||||
|
|
||||||
|
return new Response(null, { status: 204 });
|
||||||
|
};
|
||||||
53
web/src/routes/api/graph/search/+server.ts
Normal file
53
web/src/routes/api/graph/search/+server.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { json, error } from '@sveltejs/kit';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { sql } from '$lib/server/db';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/graph/search?q=...&limit=...
|
||||||
|
* Fulltekstsøk på tvers av entiteter og segmenter (transkripsjoner).
|
||||||
|
* Entiteter søkes via ILIKE (name + aliases).
|
||||||
|
* Segmenter søkes via norsk full-text search (to_tsvector).
|
||||||
|
*/
|
||||||
|
export const GET: RequestHandler = async ({ url, locals }) => {
|
||||||
|
if (!locals.workspace || !locals.user) error(401);
|
||||||
|
|
||||||
|
const q = url.searchParams.get('q')?.trim();
|
||||||
|
if (!q) error(400, 'q (søkeord) er påkrevd');
|
||||||
|
|
||||||
|
const limit = Math.min(Number(url.searchParams.get('limit') ?? 20), 50);
|
||||||
|
|
||||||
|
// Søk entiteter
|
||||||
|
const entities = await sql`
|
||||||
|
SELECT e.id, e.name, e.type, e.aliases, 'entitet' AS result_type,
|
||||||
|
n.created_at
|
||||||
|
FROM entities e
|
||||||
|
JOIN nodes n ON n.id = e.id
|
||||||
|
WHERE n.workspace_id = ${locals.workspace.id}
|
||||||
|
AND (
|
||||||
|
e.name ILIKE ${'%' + q + '%'}
|
||||||
|
OR EXISTS (SELECT 1 FROM unnest(e.aliases) AS a WHERE a ILIKE ${'%' + q + '%'})
|
||||||
|
)
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN e.name ILIKE ${q + '%'} THEN 0 ELSE 1 END,
|
||||||
|
e.name
|
||||||
|
LIMIT ${limit}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Søk segmenter (transkripsjoner) med norsk FTS
|
||||||
|
const segments = await sql`
|
||||||
|
SELECT s.id, s.transcript, s.start_time, s.end_time,
|
||||||
|
ep.title AS episode_title, ep.id AS episode_id,
|
||||||
|
'segment' AS result_type,
|
||||||
|
ts_headline('norwegian', s.transcript, plainto_tsquery('norwegian', ${q}),
|
||||||
|
'MaxWords=40, MinWords=20, StartSel=**, StopSel=**') AS highlight
|
||||||
|
FROM segments s
|
||||||
|
JOIN episodes ep ON ep.id = s.episode_id
|
||||||
|
JOIN nodes n ON n.id = s.id
|
||||||
|
WHERE n.workspace_id = ${locals.workspace.id}
|
||||||
|
AND to_tsvector('norwegian', s.transcript) @@ plainto_tsquery('norwegian', ${q})
|
||||||
|
ORDER BY ts_rank(to_tsvector('norwegian', s.transcript), plainto_tsquery('norwegian', ${q})) DESC
|
||||||
|
LIMIT ${limit}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return json({ entities, segments });
|
||||||
|
};
|
||||||
83
web/src/routes/api/graph/traverse/[nodeId]/+server.ts
Normal file
83
web/src/routes/api/graph/traverse/[nodeId]/+server.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { json, error } from '@sveltejs/kit';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { sql } from '$lib/server/db';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/graph/traverse/:nodeId?depth=2
|
||||||
|
* Traverser grafen fra en node og returner nettverket.
|
||||||
|
* Bruker recursive CTE for å følge edges opp til N ledd.
|
||||||
|
* Returformat: { nodes: [...], edges: [...] } (D3.js/Vis.js-kompatibelt)
|
||||||
|
*/
|
||||||
|
export const GET: RequestHandler = async ({ params, url, locals }) => {
|
||||||
|
if (!locals.workspace || !locals.user) error(401);
|
||||||
|
|
||||||
|
const nodeId = params.nodeId;
|
||||||
|
const depth = Math.min(Number(url.searchParams.get('depth') ?? 2), 3);
|
||||||
|
|
||||||
|
// Verifiser at startnoden tilhører workspace
|
||||||
|
const [startNode] = await sql`
|
||||||
|
SELECT id FROM nodes
|
||||||
|
WHERE id = ${nodeId} AND workspace_id = ${locals.workspace.id}
|
||||||
|
`;
|
||||||
|
if (!startNode) error(404, 'Node ikke funnet');
|
||||||
|
|
||||||
|
// Recursive CTE: finn alle noder innenfor N ledd
|
||||||
|
const result = await sql`
|
||||||
|
WITH RECURSIVE traversal AS (
|
||||||
|
-- Startnode
|
||||||
|
SELECT ${nodeId}::uuid AS node_id, 0 AS depth
|
||||||
|
UNION
|
||||||
|
-- Følg edges i begge retninger
|
||||||
|
SELECT
|
||||||
|
CASE
|
||||||
|
WHEN ge.source_id = t.node_id THEN ge.target_id
|
||||||
|
ELSE ge.source_id
|
||||||
|
END AS node_id,
|
||||||
|
t.depth + 1
|
||||||
|
FROM traversal t
|
||||||
|
JOIN graph_edges ge ON (ge.source_id = t.node_id OR ge.target_id = t.node_id)
|
||||||
|
WHERE t.depth < ${depth}
|
||||||
|
AND ge.workspace_id = ${locals.workspace.id}
|
||||||
|
),
|
||||||
|
reachable_nodes AS (
|
||||||
|
SELECT DISTINCT node_id, MIN(depth) AS depth FROM traversal GROUP BY node_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
rn.node_id AS id,
|
||||||
|
rn.depth,
|
||||||
|
n.node_type,
|
||||||
|
e.name,
|
||||||
|
e.type AS entity_type,
|
||||||
|
e.avatar_url
|
||||||
|
FROM reachable_nodes rn
|
||||||
|
JOIN nodes n ON n.id = rn.node_id
|
||||||
|
LEFT JOIN entities e ON e.id = rn.node_id
|
||||||
|
ORDER BY rn.depth, e.name NULLS LAST
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Hent alle edges mellom de nåbare nodene
|
||||||
|
const nodeIds = result.map((r) => r.id);
|
||||||
|
const edges = nodeIds.length > 0
|
||||||
|
? await sql`
|
||||||
|
SELECT ge.id, ge.source_id, ge.target_id, ge.relation_type,
|
||||||
|
ge.confidence, ge.origin, rt.label AS relation_label
|
||||||
|
FROM graph_edges ge
|
||||||
|
JOIN relation_types rt ON rt.name = ge.relation_type
|
||||||
|
WHERE ge.workspace_id = ${locals.workspace.id}
|
||||||
|
AND ge.source_id = ANY(${nodeIds})
|
||||||
|
AND ge.target_id = ANY(${nodeIds})
|
||||||
|
`
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return json({
|
||||||
|
nodes: result.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
depth: r.depth,
|
||||||
|
node_type: r.node_type,
|
||||||
|
name: r.name,
|
||||||
|
entity_type: r.entity_type,
|
||||||
|
avatar_url: r.avatar_url
|
||||||
|
})),
|
||||||
|
edges
|
||||||
|
});
|
||||||
|
};
|
||||||
Loading…
Add table
Reference in a new issue