@lucern/graph-sync 1.0.28 → 1.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,7 @@ function getDriver() {
7
7
  const uri = process.env.NEO4J_URI;
8
8
  const user = process.env.NEO4J_USER;
9
9
  const password = process.env.NEO4J_PASSWORD;
10
- if (!uri || !user || !password) {
10
+ if (!(uri && user && password)) {
11
11
  throw new Error(
12
12
  "[Neo4j Driver] Missing credentials. Set NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD via `npx convex env set`"
13
13
  );
@@ -93,6 +93,7 @@ function convertNeo4jValue(value) {
93
93
  }
94
94
 
95
95
  // src/neo4jQueryRoute.ts
96
+ var BEARER_AUTHORIZATION_PATTERN = /^Bearer\s+(.+)$/iu;
96
97
  function jsonResponse(body, init) {
97
98
  return new Response(JSON.stringify(body), {
98
99
  ...init,
@@ -104,9 +105,38 @@ function jsonResponse(body, init) {
104
105
  }
105
106
  function readBearerSecret(request) {
106
107
  const authorization = request.headers.get("authorization") ?? "";
107
- const match = authorization.match(/^Bearer\s+(.+)$/iu);
108
+ const match = authorization.match(BEARER_AUTHORIZATION_PATTERN);
108
109
  return match?.[1]?.trim() || null;
109
110
  }
111
+ function validateRouteSecret(request, options) {
112
+ const expectedSecret = options.syncSecret ?? process.env.NEO4J_SYNC_SECRET?.trim();
113
+ if (!expectedSecret) {
114
+ return jsonResponse(
115
+ { error: "Neo4j sync secret not configured" },
116
+ { status: 500 }
117
+ );
118
+ }
119
+ if (readBearerSecret(request) !== expectedSecret) {
120
+ return jsonResponse({ error: "Unauthorized" }, { status: 401 });
121
+ }
122
+ return null;
123
+ }
124
+ async function readQueryBody(request) {
125
+ try {
126
+ return {
127
+ ok: true,
128
+ body: await request.json()
129
+ };
130
+ } catch {
131
+ return {
132
+ ok: false,
133
+ response: jsonResponse({ error: "Invalid JSON body" }, { status: 400 })
134
+ };
135
+ }
136
+ }
137
+ function normalizeQueryParams(params) {
138
+ return params && typeof params === "object" && !Array.isArray(params) ? params : {};
139
+ }
110
140
  function hasTenantContext(params) {
111
141
  const tenantId = params.tenantId;
112
142
  const topicId = params.topicId;
@@ -123,62 +153,79 @@ function normalizeConnectedNodesQuery(queryName, params, queries) {
123
153
  const aliased = `connectedNodes${hops}`;
124
154
  return queries[aliased] ? aliased : queryName;
125
155
  }
156
+ function requireTenantContext(options, params) {
157
+ if ((options.requireTenantContext ?? true) && !hasTenantContext(params)) {
158
+ return jsonResponse(
159
+ { error: "Missing required tenant context" },
160
+ { status: 400 }
161
+ );
162
+ }
163
+ return null;
164
+ }
165
+ function resolveNamedQuery(queryName, params, queries) {
166
+ const normalizedQueryName = normalizeConnectedNodesQuery(
167
+ queryName,
168
+ params,
169
+ queries
170
+ );
171
+ const query = queries[normalizedQueryName];
172
+ if (!query) {
173
+ return {
174
+ ok: false,
175
+ response: jsonResponse(
176
+ { error: `Unknown query: ${queryName}` },
177
+ { status: 400 }
178
+ )
179
+ };
180
+ }
181
+ return { ok: true, query, queryName: normalizedQueryName };
182
+ }
183
+ async function executeNamedQuery(queryName, query, params) {
184
+ try {
185
+ const data = await runCypher(
186
+ query.cypher,
187
+ params,
188
+ query.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS
189
+ );
190
+ return jsonResponse({ data, queryName });
191
+ } catch (error) {
192
+ return jsonResponse(
193
+ {
194
+ error: error instanceof Error ? error.message : "Neo4j query failed",
195
+ queryName
196
+ },
197
+ { status: 500 }
198
+ );
199
+ }
200
+ }
126
201
  function createNeo4jQueryRouteHandler(options) {
127
202
  return async function handleNeo4jQuery(request) {
128
- const expectedSecret = options.syncSecret ?? process.env.NEO4J_SYNC_SECRET?.trim();
129
- if (!expectedSecret) {
130
- return jsonResponse(
131
- { error: "Neo4j sync secret not configured" },
132
- { status: 500 }
133
- );
134
- }
135
- if (readBearerSecret(request) !== expectedSecret) {
136
- return jsonResponse({ error: "Unauthorized" }, { status: 401 });
203
+ const authError = validateRouteSecret(request, options);
204
+ if (authError) {
205
+ return authError;
137
206
  }
138
- let body;
139
- try {
140
- body = await request.json();
141
- } catch {
142
- return jsonResponse({ error: "Invalid JSON body" }, { status: 400 });
207
+ const bodyResult = await readQueryBody(request);
208
+ if (!bodyResult.ok) {
209
+ return bodyResult.response;
143
210
  }
211
+ const { body } = bodyResult;
144
212
  if (typeof body.queryName !== "string" || body.queryName.length === 0) {
145
213
  return jsonResponse({ error: "Missing queryName" }, { status: 400 });
146
214
  }
147
- const params = body.params && typeof body.params === "object" && !Array.isArray(body.params) ? body.params : {};
148
- if ((options.requireTenantContext ?? true) && !hasTenantContext(params)) {
149
- return jsonResponse(
150
- { error: "Missing required tenant context" },
151
- { status: 400 }
152
- );
215
+ const params = normalizeQueryParams(body.params);
216
+ const tenantContextError = requireTenantContext(options, params);
217
+ if (tenantContextError) {
218
+ return tenantContextError;
153
219
  }
154
- const queryName = normalizeConnectedNodesQuery(
220
+ const namedQuery = resolveNamedQuery(
155
221
  body.queryName,
156
222
  params,
157
223
  options.queries
158
224
  );
159
- const query = options.queries[queryName];
160
- if (!query) {
161
- return jsonResponse(
162
- { error: `Unknown query: ${body.queryName}` },
163
- { status: 400 }
164
- );
165
- }
166
- try {
167
- const data = await runCypher(
168
- query.cypher,
169
- params,
170
- query.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS
171
- );
172
- return jsonResponse({ data, queryName });
173
- } catch (error) {
174
- return jsonResponse(
175
- {
176
- error: error instanceof Error ? error.message : "Neo4j query failed",
177
- queryName
178
- },
179
- { status: 500 }
180
- );
225
+ if (!namedQuery.ok) {
226
+ return namedQuery.response;
181
227
  }
228
+ return executeNamedQuery(namedQuery.queryName, namedQuery.query, params);
182
229
  };
183
230
  }
184
231
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/neo4jDriver.ts","../src/neo4jQueryRoute.ts"],"names":[],"mappings":"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","file":"neo4jQueryRoute.js","sourcesContent":["/**\n * neo4jDriver module implementation.\n */\n\n\"use node\";\n/**\n * Direct Neo4j Driver for Convex\n *\n * Uses the \"use node\" directive to enable Node.js runtime, allowing\n * direct use of the neo4j-driver package instead of HTTP proxies.\n *\n * Environment Variables (set per deployment via `npx convex env set`):\n * - NEO4J_URI: neo4j+s://xxx.databases.neo4j.io\n * - NEO4J_USER: neo4j\n * - NEO4J_PASSWORD: your-password\n *\n * @see /docs/architecture/UNIFIED_GRAPH_ARCHITECTURE.md\n */\n\nimport neo4j, { type Driver } from \"neo4j-driver\";\n\n// =============================================================================\n// VALID LABELS AND RELATIONSHIP TYPES (Security: Prevent Cypher Injection)\n// =============================================================================\n\nconst VALID_NODE_LABELS = new Set([\n // Ontological\n \"ValueChain\",\n \"Function\",\n \"FinSector\",\n \"Company\",\n \"Person\",\n \"Investor\",\n // Epistemic\n \"Theme\",\n \"Belief\",\n \"Question\",\n \"Evidence\",\n \"Source\",\n \"Decision\",\n \"Sprint\",\n \"Claim\",\n \"Synthesis\",\n \"Answer\",\n]);\n\nconst VALID_RELATIONSHIP_TYPES = new Set([\n // Cross-layer edges\n \"EXTRACTED_FROM\",\n \"ANSWERS\",\n \"RESPONDS_TO\",\n \"INFORMS\",\n \"QUALIFIES\",\n \"TESTS\",\n \"EXPLORES\",\n \"BASED_ON\",\n \"RELATES_TO_THESIS\",\n \"BELONGS_TO\",\n \"PLAYS_THEME\",\n // Same-layer edges\n \"SUPERSEDES\",\n \"SAME_AS\",\n \"DEPENDS_ON\",\n \"REINFORCES\",\n \"PARENT_OF\",\n \"CHILD_OF\",\n \"FALSIFIED_BY\",\n \"EXCLUSIVE_WITH\",\n \"COLLAPSES_IF\",\n \"CASCADE_FROM\",\n \"STRENGTHENED_BY\",\n \"WEAKENED_BY\",\n \"ALTERNATIVE_TO\",\n \"SUBSUMES\",\n \"VALIDATED_BY\",\n \"REQUIRED_FOR\",\n \"PREREQUISITE_FOR\",\n \"PARALLEL_TO\",\n \"CORROBORATES\",\n \"EXTENDS\",\n \"SAME_SOURCE_AS\",\n \"SAME_THEME_AS\",\n // Ontological\n \"EVALUATES\",\n \"PERSPECTIVE_ON\",\n \"WORKS_AT\",\n \"PARTICIPATES_IN\",\n \"PERFORMS\",\n \"FUNCTION_IN\",\n \"IMPACTS\",\n \"INVESTED_IN\",\n \"RAISED_FROM\",\n \"BASED_ON_BELIEF\",\n \"BASED_ON_QUESTION\",\n \"BLOCKED_BY_CONTRADICTION\",\n \"INFORMED_BY_THEME\",\n]);\n\nexport function validateLabel(label: string): void {\n if (!VALID_NODE_LABELS.has(label)) {\n throw new Error(\n `[Neo4j Security] Invalid node label: ${label}. Must be one of: ${Array.from(VALID_NODE_LABELS).join(\", \")}`\n );\n }\n}\n\nexport function validateRelType(relType: string): void {\n if (!VALID_RELATIONSHIP_TYPES.has(relType)) {\n throw new Error(\n `[Neo4j Security] Invalid relationship type: ${relType}. Must be one of: ${Array.from(VALID_RELATIONSHIP_TYPES).join(\", \")}`\n );\n }\n}\n\n// =============================================================================\n// DRIVER SINGLETON\n// =============================================================================\n\nlet driver: Driver | null = null;\n\nfunction getDriver(): Driver {\n if (!driver) {\n const uri = process.env.NEO4J_URI;\n const user = process.env.NEO4J_USER;\n const password = process.env.NEO4J_PASSWORD;\n\n if (!uri || !user || !password) {\n throw new Error(\n \"[Neo4j Driver] Missing credentials. Set NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD via `npx convex env set`\"\n );\n }\n\n driver = neo4j.driver(uri, neo4j.auth.basic(user, password), {\n // Connection pool settings\n maxConnectionPoolSize: 50,\n connectionAcquisitionTimeout: 30_000,\n // Logging\n logging: {\n level: \"warn\",\n logger: (level, message) => console.log(`[Neo4j ${level}] ${message}`),\n },\n });\n }\n return driver;\n}\n\n// =============================================================================\n// QUERY CONFIGURATION\n// =============================================================================\n\n/**\n * Default query timeout in milliseconds.\n * Prevents expensive graph traversals from hanging indefinitely.\n */\nexport const DEFAULT_QUERY_TIMEOUT_MS = 30_000; // 30 seconds\n\n/**\n * Timeout for complex graph queries (cascade simulation, lineage traversal)\n */\nexport const COMPLEX_QUERY_TIMEOUT_MS = 60_000; // 60 seconds\n\n// =============================================================================\n// QUERY EXECUTION\n// =============================================================================\n\n/**\n * Convert JavaScript values to Neo4j-compatible types\n * Neo4j requires explicit integers, not floats\n */\nfunction toNeo4jParams(\n params: Record<string, unknown>\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(params)) {\n if (typeof value === \"number\" && Number.isInteger(value)) {\n // Convert JavaScript integers to Neo4j integers\n result[key] = neo4j.int(value);\n } else if (Array.isArray(value)) {\n result[key] = value.map((v) =>\n typeof v === \"number\" && Number.isInteger(v) ? neo4j.int(v) : v\n );\n } else {\n result[key] = value;\n }\n }\n return result;\n}\n\n/**\n * Execute a Cypher query and return results as typed objects\n *\n * @param query - Cypher query string\n * @param params - Query parameters\n * @param timeoutMs - Query timeout in milliseconds (default: 30s)\n */\nexport async function runCypher<T = Record<string, unknown>>(\n query: string,\n params: Record<string, unknown> = {},\n timeoutMs: number = DEFAULT_QUERY_TIMEOUT_MS\n): Promise<T[]> {\n const neo4jDriver = getDriver();\n const session = neo4jDriver.session();\n\n try {\n const neo4jParams = toNeo4jParams(params);\n const result = await session.run(query, neo4jParams, {\n timeout: neo4j.int(timeoutMs),\n });\n return result.records.map((record) => {\n const obj: Record<string, unknown> = {};\n for (const key of record.keys) {\n const field = String(key);\n obj[field] = convertNeo4jValue(record.get(field));\n }\n return obj as T;\n });\n } finally {\n await session.close();\n }\n}\n\n/**\n * Execute a write transaction (for mutations)\n *\n * @param query - Cypher query string\n * @param params - Query parameters\n * @param timeoutMs - Transaction timeout in milliseconds (default: 30s)\n */\nexport async function runWriteTransaction<T = Record<string, unknown>>(\n query: string,\n params: Record<string, unknown> = {},\n timeoutMs: number = DEFAULT_QUERY_TIMEOUT_MS\n): Promise<T[]> {\n const neo4jDriver = getDriver();\n const session = neo4jDriver.session();\n\n try {\n const neo4jParams = toNeo4jParams(params);\n const result = await session.executeWrite(\n async (tx) => {\n return await tx.run(query, neo4jParams);\n },\n { timeout: timeoutMs }\n );\n return result.records.map((record) => {\n const obj: Record<string, unknown> = {};\n for (const key of record.keys) {\n const field = String(key);\n obj[field] = convertNeo4jValue(record.get(field));\n }\n return obj as T;\n });\n } finally {\n await session.close();\n }\n}\n\n/**\n * Execute multiple queries in a single transaction\n *\n * @param queries - Array of queries with parameters\n * @param timeoutMs - Transaction timeout in milliseconds (default: 60s for batch)\n */\nexport async function runBatchTransaction(\n queries: Array<{ query: string; params: Record<string, unknown> }>,\n timeoutMs: number = COMPLEX_QUERY_TIMEOUT_MS\n): Promise<void> {\n const neo4jDriver = getDriver();\n const session = neo4jDriver.session();\n\n try {\n await session.executeWrite(\n async (tx) => {\n for (const { query, params } of queries) {\n await tx.run(query, params);\n }\n },\n { timeout: timeoutMs }\n );\n } finally {\n await session.close();\n }\n}\n\n// =============================================================================\n// NODE OPERATIONS\n// =============================================================================\n\n/**\n * Upsert a node by globalId\n */\nexport async function upsertNode(\n label: string,\n globalId: string,\n properties: Record<string, unknown>\n): Promise<void> {\n validateLabel(label); // Security: prevent Cypher injection\n await runWriteTransaction(\n `\n MERGE (n:${label} {globalId: $globalId})\n SET n += $properties\n SET n.updatedAt = timestamp()\n `,\n { globalId, properties }\n );\n}\n\n/**\n * Delete a node by globalId\n */\nexport async function deleteNode(globalId: string): Promise<void> {\n await runWriteTransaction(\n `\n MATCH (n {globalId: $globalId})\n DETACH DELETE n\n `,\n { globalId }\n );\n}\n\n/**\n * Batch upsert nodes\n */\nexport async function batchUpsertNodes(\n label: string,\n nodes: Array<{ globalId: string; properties: Record<string, unknown> }>\n): Promise<void> {\n if (nodes.length === 0) {\n return;\n }\n\n validateLabel(label); // Security: prevent Cypher injection\n await runWriteTransaction(\n `\n UNWIND $nodes as node\n MERGE (n:${label} {globalId: node.globalId})\n SET n += node.properties\n SET n.updatedAt = timestamp()\n `,\n { nodes }\n );\n}\n\n// =============================================================================\n// EDGE OPERATIONS\n// =============================================================================\n\n/**\n * Upsert an edge by globalId\n */\nexport async function upsertEdge(\n relType: string,\n globalId: string,\n fromGlobalId: string,\n toGlobalId: string,\n properties: Record<string, unknown> = {}\n): Promise<void> {\n validateRelType(relType); // Security: prevent Cypher injection\n await runWriteTransaction(\n `\n MATCH (from {globalId: $fromGlobalId})\n MATCH (to {globalId: $toGlobalId})\n MERGE (from)-[r:${relType} {globalId: $globalId}]->(to)\n SET r += $properties\n SET r.updatedAt = timestamp()\n `,\n { globalId, fromGlobalId, toGlobalId, properties }\n );\n}\n\n/**\n * Delete an edge by globalId\n */\nexport async function deleteEdge(globalId: string): Promise<void> {\n await runWriteTransaction(\n `\n MATCH ()-[r {globalId: $globalId}]->()\n DELETE r\n `,\n { globalId }\n );\n}\n\n/**\n * Batch upsert edges\n */\nexport async function batchUpsertEdges(\n edges: Array<{\n relType: string;\n globalId: string;\n fromGlobalId: string;\n toGlobalId: string;\n properties?: Record<string, unknown>;\n }>\n): Promise<void> {\n if (edges.length === 0) {\n return;\n }\n\n // Group by relationship type for efficient batching\n const byType = new Map<string, typeof edges>();\n for (const edge of edges) {\n const existing = byType.get(edge.relType) || [];\n existing.push(edge);\n byType.set(edge.relType, existing);\n }\n\n const queries: Array<{ query: string; params: Record<string, unknown> }> = [];\n for (const [relType, typeEdges] of byType) {\n queries.push({\n query: `\n UNWIND $edges as edge\n MATCH (from {globalId: edge.fromGlobalId})\n MATCH (to {globalId: edge.toGlobalId})\n MERGE (from)-[r:${relType} {globalId: edge.globalId}]->(to)\n SET r += edge.properties\n SET r.updatedAt = timestamp()\n `,\n params: {\n edges: typeEdges.map((e) => ({\n globalId: e.globalId,\n fromGlobalId: e.fromGlobalId,\n toGlobalId: e.toGlobalId,\n properties: e.properties || {},\n })),\n },\n });\n }\n\n await runBatchTransaction(queries);\n}\n\n// =============================================================================\n// HEALTH CHECK\n// =============================================================================\n\n/**\n * Check if Neo4j connection is healthy\n */\nexport async function healthCheck(): Promise<{\n healthy: boolean;\n nodeCount?: number;\n error?: string;\n}> {\n try {\n const result = await runCypher<{ count: number }>(\n \"MATCH (n) RETURN count(n) as count LIMIT 1\"\n );\n return {\n healthy: true,\n nodeCount: result[0]?.count || 0,\n };\n } catch (error) {\n return {\n healthy: false,\n error: error instanceof Error ? error.message : \"Unknown error\",\n };\n }\n}\n\n/**\n * Get connection info (for debugging)\n */\nexport function getConnectionInfo(): {\n uri: string | undefined;\n user: string | undefined;\n configured: boolean;\n} {\n return {\n uri: process.env.NEO4J_URI,\n user: process.env.NEO4J_USER,\n configured: Boolean(\n process.env.NEO4J_URI &&\n process.env.NEO4J_USER &&\n process.env.NEO4J_PASSWORD\n ),\n };\n}\n\n// =============================================================================\n// VALUE CONVERSION\n// =============================================================================\n\n/**\n * Convert Neo4j types to plain JavaScript\n */\nfunction convertNeo4jValue(value: unknown): unknown {\n if (value === null || value === undefined) {\n return null;\n }\n\n // Handle Neo4j Integer\n if (neo4j.isInt(value)) {\n return neo4j.integer.toNumber(value);\n }\n\n // Handle Neo4j Date/Time types\n if (neo4j.isDate(value) || neo4j.isDateTime(value) || neo4j.isTime(value)) {\n return value.toString();\n }\n\n // Handle arrays\n if (Array.isArray(value)) {\n return value.map(convertNeo4jValue);\n }\n\n // Handle Node objects\n if (value && typeof value === \"object\" && \"properties\" in value) {\n const nodeObj = value as { properties: Record<string, unknown> };\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(nodeObj.properties)) {\n result[k] = convertNeo4jValue(v);\n }\n return result;\n }\n\n // Handle plain objects\n if (typeof value === \"object\") {\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n result[k] = convertNeo4jValue(v);\n }\n return result;\n }\n\n return value;\n}\n\n// =============================================================================\n// CLEANUP\n// =============================================================================\n\n/**\n * Close the driver connection (for graceful shutdown)\n */\nexport async function closeDriver(): Promise<void> {\n if (driver) {\n await driver.close();\n driver = null;\n }\n}\n","/**\n * Vercel/Next route helper for the Neo4j query proxy.\n *\n * The helper accepts named Cypher only. It never accepts raw Cypher from a\n * request body, which keeps tenant routes small without opening an injection\n * surface.\n */\n\n\"use node\";\n\nimport { runCypher, DEFAULT_QUERY_TIMEOUT_MS } from \"./neo4jDriver\";\n\nexport type Neo4jNamedQuery = Readonly<{\n cypher: string;\n timeoutMs?: number;\n}>;\n\nexport type Neo4jQueryRegistry = Readonly<Record<string, Neo4jNamedQuery>>;\n\nexport type Neo4jQueryRouteOptions = Readonly<{\n queries: Neo4jQueryRegistry;\n syncSecret?: string;\n requireTenantContext?: boolean;\n}>;\n\ntype Neo4jQueryRequestBody = Readonly<{\n queryName?: unknown;\n params?: unknown;\n}>;\n\nfunction jsonResponse(body: unknown, init?: ResponseInit): Response {\n return new Response(JSON.stringify(body), {\n ...init,\n headers: {\n \"Content-Type\": \"application/json\",\n ...init?.headers,\n },\n });\n}\n\nfunction readBearerSecret(request: Request): string | null {\n const authorization = request.headers.get(\"authorization\") ?? \"\";\n const match = authorization.match(/^Bearer\\s+(.+)$/iu);\n return match?.[1]?.trim() || null;\n}\n\nfunction hasTenantContext(params: Record<string, unknown>): boolean {\n const tenantId = params.tenantId;\n const topicId = params.topicId;\n const projectId = params.projectId;\n return [tenantId, topicId, projectId].some(\n (value) => typeof value === \"string\" && value.trim().length > 0\n );\n}\n\nfunction normalizeConnectedNodesQuery(\n queryName: string,\n params: Record<string, unknown>,\n queries: Neo4jQueryRegistry\n): string {\n if (queryName !== \"connectedNodes\") {\n return queryName;\n }\n\n const hops =\n typeof params.maxHops === \"number\" && Number.isFinite(params.maxHops)\n ? Math.min(Math.max(Math.floor(params.maxHops), 1), 5)\n : 2;\n const aliased = `connectedNodes${hops}`;\n return queries[aliased] ? aliased : queryName;\n}\n\nexport function createNeo4jQueryRouteHandler(\n options: Neo4jQueryRouteOptions\n): (request: Request) => Promise<Response> {\n return async function handleNeo4jQuery(request: Request): Promise<Response> {\n const expectedSecret =\n options.syncSecret ?? process.env.NEO4J_SYNC_SECRET?.trim();\n if (!expectedSecret) {\n return jsonResponse(\n { error: \"Neo4j sync secret not configured\" },\n { status: 500 }\n );\n }\n\n if (readBearerSecret(request) !== expectedSecret) {\n return jsonResponse({ error: \"Unauthorized\" }, { status: 401 });\n }\n\n let body: Neo4jQueryRequestBody;\n try {\n body = (await request.json()) as Neo4jQueryRequestBody;\n } catch {\n return jsonResponse({ error: \"Invalid JSON body\" }, { status: 400 });\n }\n\n if (typeof body.queryName !== \"string\" || body.queryName.length === 0) {\n return jsonResponse({ error: \"Missing queryName\" }, { status: 400 });\n }\n\n const params =\n body.params && typeof body.params === \"object\" && !Array.isArray(body.params)\n ? (body.params as Record<string, unknown>)\n : {};\n if ((options.requireTenantContext ?? true) && !hasTenantContext(params)) {\n return jsonResponse(\n { error: \"Missing required tenant context\" },\n { status: 400 }\n );\n }\n\n const queryName = normalizeConnectedNodesQuery(\n body.queryName,\n params,\n options.queries\n );\n const query = options.queries[queryName];\n if (!query) {\n return jsonResponse(\n { error: `Unknown query: ${body.queryName}` },\n { status: 400 }\n );\n }\n\n try {\n const data = await runCypher(\n query.cypher,\n params,\n query.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS\n );\n return jsonResponse({ data, queryName });\n } catch (error) {\n return jsonResponse(\n {\n error: error instanceof Error ? error.message : \"Neo4j query failed\",\n queryName,\n },\n { status: 500 }\n );\n }\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/neo4jDriver.ts","../src/neo4jQueryRoute.ts"],"names":[],"mappings":"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","file":"neo4jQueryRoute.js","sourcesContent":["// biome-ignore-all lint/style/useFilenamingConvention: Published @lucern/graph-sync/neo4jDriver subpath; rename needs an export-map migration.\n/**\n * neo4jDriver module implementation.\n */\n\n\"use node\";\n/**\n * Direct Neo4j Driver for Convex\n *\n * Uses the \"use node\" directive to enable Node.js runtime, allowing\n * direct use of the neo4j-driver package instead of HTTP proxies.\n *\n * Environment Variables (set per deployment via `npx convex env set`):\n * - NEO4J_URI: neo4j+s://xxx.databases.neo4j.io\n * - NEO4J_USER: neo4j\n * - NEO4J_PASSWORD: your-password\n *\n * @see /docs/architecture/UNIFIED_GRAPH_ARCHITECTURE.md\n */\n\nimport neo4j, { type Driver } from \"neo4j-driver\";\n\n// =============================================================================\n// VALID LABELS AND RELATIONSHIP TYPES (Security: Prevent Cypher Injection)\n// =============================================================================\n\nconst VALID_NODE_LABELS = new Set([\n // Ontological\n \"ValueChain\",\n \"Function\",\n \"FinSector\",\n \"Company\",\n \"Person\",\n \"Investor\",\n // Epistemic\n \"Theme\",\n \"Belief\",\n \"Question\",\n \"Evidence\",\n \"Source\",\n \"Decision\",\n \"Sprint\",\n \"Claim\",\n \"Synthesis\",\n \"Answer\",\n]);\n\nconst VALID_RELATIONSHIP_TYPES = new Set([\n // Cross-layer edges\n \"EXTRACTED_FROM\",\n \"ANSWERS\",\n \"RESPONDS_TO\",\n \"INFORMS\",\n \"QUALIFIES\",\n \"TESTS\",\n \"EXPLORES\",\n \"BASED_ON\",\n \"RELATES_TO_THESIS\",\n \"BELONGS_TO\",\n \"PLAYS_THEME\",\n // Same-layer edges\n \"SUPERSEDES\",\n \"SAME_AS\",\n \"DEPENDS_ON\",\n \"REINFORCES\",\n \"PARENT_OF\",\n \"CHILD_OF\",\n \"FALSIFIED_BY\",\n \"EXCLUSIVE_WITH\",\n \"COLLAPSES_IF\",\n \"CASCADE_FROM\",\n \"STRENGTHENED_BY\",\n \"WEAKENED_BY\",\n \"ALTERNATIVE_TO\",\n \"SUBSUMES\",\n \"VALIDATED_BY\",\n \"REQUIRED_FOR\",\n \"PREREQUISITE_FOR\",\n \"PARALLEL_TO\",\n \"CORROBORATES\",\n \"EXTENDS\",\n \"SAME_SOURCE_AS\",\n \"SAME_THEME_AS\",\n // Ontological\n \"EVALUATES\",\n \"PERSPECTIVE_ON\",\n \"WORKS_AT\",\n \"PARTICIPATES_IN\",\n \"PERFORMS\",\n \"FUNCTION_IN\",\n \"IMPACTS\",\n \"INVESTED_IN\",\n \"RAISED_FROM\",\n \"BASED_ON_BELIEF\",\n \"BASED_ON_QUESTION\",\n \"BLOCKED_BY_CONTRADICTION\",\n \"INFORMED_BY_THEME\",\n]);\n\nexport function validateLabel(label: string): void {\n if (!VALID_NODE_LABELS.has(label)) {\n throw new Error(\n `[Neo4j Security] Invalid node label: ${label}. Must be one of: ${Array.from(VALID_NODE_LABELS).join(\", \")}`\n );\n }\n}\n\nexport function validateRelType(relType: string): void {\n if (!VALID_RELATIONSHIP_TYPES.has(relType)) {\n throw new Error(\n `[Neo4j Security] Invalid relationship type: ${relType}. Must be one of: ${Array.from(VALID_RELATIONSHIP_TYPES).join(\", \")}`\n );\n }\n}\n\n// =============================================================================\n// DRIVER SINGLETON\n// =============================================================================\n\nlet driver: Driver | null = null;\n\nfunction getDriver(): Driver {\n if (!driver) {\n const uri = process.env.NEO4J_URI;\n const user = process.env.NEO4J_USER;\n const password = process.env.NEO4J_PASSWORD;\n\n if (!(uri && user && password)) {\n throw new Error(\n \"[Neo4j Driver] Missing credentials. Set NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD via `npx convex env set`\"\n );\n }\n\n driver = neo4j.driver(uri, neo4j.auth.basic(user, password), {\n // Connection pool settings\n maxConnectionPoolSize: 50,\n connectionAcquisitionTimeout: 30_000,\n // Logging\n logging: {\n level: \"warn\",\n logger: (level, message) => console.log(`[Neo4j ${level}] ${message}`),\n },\n });\n }\n return driver;\n}\n\n// =============================================================================\n// QUERY CONFIGURATION\n// =============================================================================\n\n/**\n * Default query timeout in milliseconds.\n * Prevents expensive graph traversals from hanging indefinitely.\n */\nexport const DEFAULT_QUERY_TIMEOUT_MS = 30_000; // 30 seconds\n\n/**\n * Timeout for complex graph queries (cascade simulation, lineage traversal)\n */\nexport const COMPLEX_QUERY_TIMEOUT_MS = 60_000; // 60 seconds\n\n// =============================================================================\n// QUERY EXECUTION\n// =============================================================================\n\n/**\n * Convert JavaScript values to Neo4j-compatible types\n * Neo4j requires explicit integers, not floats\n */\nfunction toNeo4jParams(\n params: Record<string, unknown>\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(params)) {\n if (typeof value === \"number\" && Number.isInteger(value)) {\n // Convert JavaScript integers to Neo4j integers\n result[key] = neo4j.int(value);\n } else if (Array.isArray(value)) {\n result[key] = value.map((v) =>\n typeof v === \"number\" && Number.isInteger(v) ? neo4j.int(v) : v\n );\n } else {\n result[key] = value;\n }\n }\n return result;\n}\n\n/**\n * Execute a Cypher query and return results as typed objects\n *\n * @param query - Cypher query string\n * @param params - Query parameters\n * @param timeoutMs - Query timeout in milliseconds (default: 30s)\n */\nexport async function runCypher<T = Record<string, unknown>>(\n query: string,\n params: Record<string, unknown> = {},\n timeoutMs: number = DEFAULT_QUERY_TIMEOUT_MS\n): Promise<T[]> {\n const neo4jDriver = getDriver();\n const session = neo4jDriver.session();\n\n try {\n const neo4jParams = toNeo4jParams(params);\n const result = await session.run(query, neo4jParams, {\n timeout: neo4j.int(timeoutMs),\n });\n return result.records.map((record) => {\n const obj: Record<string, unknown> = {};\n for (const key of record.keys) {\n const field = String(key);\n obj[field] = convertNeo4jValue(record.get(field));\n }\n return obj as T;\n });\n } finally {\n await session.close();\n }\n}\n\n/**\n * Execute a write transaction (for mutations)\n *\n * @param query - Cypher query string\n * @param params - Query parameters\n * @param timeoutMs - Transaction timeout in milliseconds (default: 30s)\n */\nexport async function runWriteTransaction<T = Record<string, unknown>>(\n query: string,\n params: Record<string, unknown> = {},\n timeoutMs: number = DEFAULT_QUERY_TIMEOUT_MS\n): Promise<T[]> {\n const neo4jDriver = getDriver();\n const session = neo4jDriver.session();\n\n try {\n const neo4jParams = toNeo4jParams(params);\n const result = await session.executeWrite(\n async (tx) => await tx.run(query, neo4jParams),\n { timeout: timeoutMs }\n );\n return result.records.map((record) => {\n const obj: Record<string, unknown> = {};\n for (const key of record.keys) {\n const field = String(key);\n obj[field] = convertNeo4jValue(record.get(field));\n }\n return obj as T;\n });\n } finally {\n await session.close();\n }\n}\n\n/**\n * Execute multiple queries in a single transaction\n *\n * @param queries - Array of queries with parameters\n * @param timeoutMs - Transaction timeout in milliseconds (default: 60s for batch)\n */\nexport async function runBatchTransaction(\n queries: Array<{ query: string; params: Record<string, unknown> }>,\n timeoutMs: number = COMPLEX_QUERY_TIMEOUT_MS\n): Promise<void> {\n const neo4jDriver = getDriver();\n const session = neo4jDriver.session();\n\n try {\n await session.executeWrite(\n async (tx) => {\n for (const { query, params } of queries) {\n await tx.run(query, params);\n }\n },\n { timeout: timeoutMs }\n );\n } finally {\n await session.close();\n }\n}\n\n// =============================================================================\n// NODE OPERATIONS\n// =============================================================================\n\n/**\n * Upsert a node by globalId\n */\nexport async function upsertNode(\n label: string,\n globalId: string,\n properties: Record<string, unknown>\n): Promise<void> {\n validateLabel(label); // Security: prevent Cypher injection\n await runWriteTransaction(\n `\n MERGE (n:${label} {globalId: $globalId})\n SET n += $properties\n SET n.updatedAt = timestamp()\n `,\n { globalId, properties }\n );\n}\n\n/**\n * Delete a node by globalId\n */\nexport async function deleteNode(globalId: string): Promise<void> {\n await runWriteTransaction(\n `\n MATCH (n {globalId: $globalId})\n DETACH DELETE n\n `,\n { globalId }\n );\n}\n\n/**\n * Batch upsert nodes\n */\nexport async function batchUpsertNodes(\n label: string,\n nodes: Array<{ globalId: string; properties: Record<string, unknown> }>\n): Promise<void> {\n if (nodes.length === 0) {\n return;\n }\n\n validateLabel(label); // Security: prevent Cypher injection\n await runWriteTransaction(\n `\n UNWIND $nodes as node\n MERGE (n:${label} {globalId: node.globalId})\n SET n += node.properties\n SET n.updatedAt = timestamp()\n `,\n { nodes }\n );\n}\n\n// =============================================================================\n// EDGE OPERATIONS\n// =============================================================================\n\n/**\n * Upsert an edge by globalId\n */\nexport async function upsertEdge(\n relType: string,\n globalId: string,\n fromGlobalId: string,\n toGlobalId: string,\n properties: Record<string, unknown> = {}\n): Promise<void> {\n validateRelType(relType); // Security: prevent Cypher injection\n await runWriteTransaction(\n `\n MATCH (from {globalId: $fromGlobalId})\n MATCH (to {globalId: $toGlobalId})\n MERGE (from)-[r:${relType} {globalId: $globalId}]->(to)\n SET r += $properties\n SET r.updatedAt = timestamp()\n `,\n { globalId, fromGlobalId, toGlobalId, properties }\n );\n}\n\n/**\n * Delete an edge by globalId\n */\nexport async function deleteEdge(globalId: string): Promise<void> {\n await runWriteTransaction(\n `\n MATCH ()-[r {globalId: $globalId}]->()\n DELETE r\n `,\n { globalId }\n );\n}\n\n/**\n * Batch upsert edges\n */\nexport async function batchUpsertEdges(\n edges: Array<{\n relType: string;\n globalId: string;\n fromGlobalId: string;\n toGlobalId: string;\n properties?: Record<string, unknown>;\n }>\n): Promise<void> {\n if (edges.length === 0) {\n return;\n }\n\n // Group by relationship type for efficient batching\n const byType = new Map<string, typeof edges>();\n for (const edge of edges) {\n const existing = byType.get(edge.relType) || [];\n existing.push(edge);\n byType.set(edge.relType, existing);\n }\n\n const queries: Array<{ query: string; params: Record<string, unknown> }> = [];\n for (const [relType, typeEdges] of byType) {\n queries.push({\n query: `\n UNWIND $edges as edge\n MATCH (from {globalId: edge.fromGlobalId})\n MATCH (to {globalId: edge.toGlobalId})\n MERGE (from)-[r:${relType} {globalId: edge.globalId}]->(to)\n SET r += edge.properties\n SET r.updatedAt = timestamp()\n `,\n params: {\n edges: typeEdges.map((e) => ({\n globalId: e.globalId,\n fromGlobalId: e.fromGlobalId,\n toGlobalId: e.toGlobalId,\n properties: e.properties || {},\n })),\n },\n });\n }\n\n await runBatchTransaction(queries);\n}\n\n// =============================================================================\n// HEALTH CHECK\n// =============================================================================\n\n/**\n * Check if Neo4j connection is healthy\n */\nexport async function healthCheck(): Promise<{\n healthy: boolean;\n nodeCount?: number;\n error?: string;\n}> {\n try {\n const result = await runCypher<{ count: number }>(\n \"MATCH (n) RETURN count(n) as count LIMIT 1\"\n );\n return {\n healthy: true,\n nodeCount: result[0]?.count || 0,\n };\n } catch (error) {\n return {\n healthy: false,\n error: error instanceof Error ? error.message : \"Unknown error\",\n };\n }\n}\n\n/**\n * Get connection info (for debugging)\n */\nexport function getConnectionInfo(): {\n uri: string | undefined;\n user: string | undefined;\n configured: boolean;\n} {\n return {\n uri: process.env.NEO4J_URI,\n user: process.env.NEO4J_USER,\n configured: Boolean(\n process.env.NEO4J_URI &&\n process.env.NEO4J_USER &&\n process.env.NEO4J_PASSWORD\n ),\n };\n}\n\n// =============================================================================\n// VALUE CONVERSION\n// =============================================================================\n\n/**\n * Convert Neo4j types to plain JavaScript\n */\nfunction convertNeo4jValue(value: unknown): unknown {\n if (value === null || value === undefined) {\n return null;\n }\n\n // Handle Neo4j Integer\n if (neo4j.isInt(value)) {\n return neo4j.integer.toNumber(value);\n }\n\n // Handle Neo4j Date/Time types\n if (neo4j.isDate(value) || neo4j.isDateTime(value) || neo4j.isTime(value)) {\n return value.toString();\n }\n\n // Handle arrays\n if (Array.isArray(value)) {\n return value.map(convertNeo4jValue);\n }\n\n // Handle Node objects\n if (value && typeof value === \"object\" && \"properties\" in value) {\n const nodeObj = value as { properties: Record<string, unknown> };\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(nodeObj.properties)) {\n result[k] = convertNeo4jValue(v);\n }\n return result;\n }\n\n // Handle plain objects\n if (typeof value === \"object\") {\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n result[k] = convertNeo4jValue(v);\n }\n return result;\n }\n\n return value;\n}\n\n// =============================================================================\n// CLEANUP\n// =============================================================================\n\n/**\n * Close the driver connection (for graceful shutdown)\n */\nexport async function closeDriver(): Promise<void> {\n if (driver) {\n await driver.close();\n driver = null;\n }\n}\n","// biome-ignore-all lint/style/useFilenamingConvention: Published @lucern/graph-sync/neo4jQueryRoute subpath; rename needs an export-map migration.\n/**\n * Vercel/Next route helper for the Neo4j query proxy.\n *\n * The helper accepts named Cypher only. It never accepts raw Cypher from a\n * request body, which keeps tenant routes small without opening an injection\n * surface.\n */\n\n\"use node\";\n\nimport { DEFAULT_QUERY_TIMEOUT_MS, runCypher } from \"./neo4jDriver\";\n\nconst BEARER_AUTHORIZATION_PATTERN = /^Bearer\\s+(.+)$/iu;\n\nexport type Neo4jNamedQuery = Readonly<{\n cypher: string;\n timeoutMs?: number;\n}>;\n\nexport type Neo4jQueryRegistry = Readonly<Record<string, Neo4jNamedQuery>>;\n\nexport type Neo4jQueryRouteOptions = Readonly<{\n queries: Neo4jQueryRegistry;\n syncSecret?: string;\n requireTenantContext?: boolean;\n}>;\n\ntype Neo4jQueryRequestBody = Readonly<{\n queryName?: unknown;\n params?: unknown;\n}>;\n\nfunction jsonResponse(body: unknown, init?: ResponseInit): Response {\n return new Response(JSON.stringify(body), {\n ...init,\n headers: {\n \"Content-Type\": \"application/json\",\n ...init?.headers,\n },\n });\n}\n\nfunction readBearerSecret(request: Request): string | null {\n const authorization = request.headers.get(\"authorization\") ?? \"\";\n const match = authorization.match(BEARER_AUTHORIZATION_PATTERN);\n return match?.[1]?.trim() || null;\n}\n\nfunction validateRouteSecret(\n request: Request,\n options: Neo4jQueryRouteOptions\n): Response | null {\n const expectedSecret =\n options.syncSecret ?? process.env.NEO4J_SYNC_SECRET?.trim();\n if (!expectedSecret) {\n return jsonResponse(\n { error: \"Neo4j sync secret not configured\" },\n { status: 500 }\n );\n }\n if (readBearerSecret(request) !== expectedSecret) {\n return jsonResponse({ error: \"Unauthorized\" }, { status: 401 });\n }\n return null;\n}\n\nasync function readQueryBody(\n request: Request\n): Promise<\n { ok: true; body: Neo4jQueryRequestBody } | { ok: false; response: Response }\n> {\n try {\n return {\n ok: true,\n body: (await request.json()) as Neo4jQueryRequestBody,\n };\n } catch {\n return {\n ok: false,\n response: jsonResponse({ error: \"Invalid JSON body\" }, { status: 400 }),\n };\n }\n}\n\nfunction normalizeQueryParams(params: unknown): Record<string, unknown> {\n return params && typeof params === \"object\" && !Array.isArray(params)\n ? (params as Record<string, unknown>)\n : {};\n}\n\nfunction hasTenantContext(params: Record<string, unknown>): boolean {\n const tenantId = params.tenantId;\n const topicId = params.topicId;\n const projectId = params.projectId;\n return [tenantId, topicId, projectId].some(\n (value) => typeof value === \"string\" && value.trim().length > 0\n );\n}\n\nfunction normalizeConnectedNodesQuery(\n queryName: string,\n params: Record<string, unknown>,\n queries: Neo4jQueryRegistry\n): string {\n if (queryName !== \"connectedNodes\") {\n return queryName;\n }\n\n const hops =\n typeof params.maxHops === \"number\" && Number.isFinite(params.maxHops)\n ? Math.min(Math.max(Math.floor(params.maxHops), 1), 5)\n : 2;\n const aliased = `connectedNodes${hops}`;\n return queries[aliased] ? aliased : queryName;\n}\n\nfunction requireTenantContext(\n options: Neo4jQueryRouteOptions,\n params: Record<string, unknown>\n): Response | null {\n if ((options.requireTenantContext ?? true) && !hasTenantContext(params)) {\n return jsonResponse(\n { error: \"Missing required tenant context\" },\n { status: 400 }\n );\n }\n return null;\n}\n\nfunction resolveNamedQuery(\n queryName: string,\n params: Record<string, unknown>,\n queries: Neo4jQueryRegistry\n):\n | { ok: true; queryName: string; query: Neo4jNamedQuery }\n | { ok: false; response: Response } {\n const normalizedQueryName = normalizeConnectedNodesQuery(\n queryName,\n params,\n queries\n );\n const query = queries[normalizedQueryName];\n if (!query) {\n return {\n ok: false,\n response: jsonResponse(\n { error: `Unknown query: ${queryName}` },\n { status: 400 }\n ),\n };\n }\n return { ok: true, query, queryName: normalizedQueryName };\n}\n\nasync function executeNamedQuery(\n queryName: string,\n query: Neo4jNamedQuery,\n params: Record<string, unknown>\n): Promise<Response> {\n try {\n const data = await runCypher(\n query.cypher,\n params,\n query.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS\n );\n return jsonResponse({ data, queryName });\n } catch (error) {\n return jsonResponse(\n {\n error: error instanceof Error ? error.message : \"Neo4j query failed\",\n queryName,\n },\n { status: 500 }\n );\n }\n}\n\nexport function createNeo4jQueryRouteHandler(\n options: Neo4jQueryRouteOptions\n): (request: Request) => Promise<Response> {\n return async function handleNeo4jQuery(request: Request): Promise<Response> {\n const authError = validateRouteSecret(request, options);\n if (authError) {\n return authError;\n }\n\n const bodyResult = await readQueryBody(request);\n if (!bodyResult.ok) {\n return bodyResult.response;\n }\n const { body } = bodyResult;\n if (typeof body.queryName !== \"string\" || body.queryName.length === 0) {\n return jsonResponse({ error: \"Missing queryName\" }, { status: 400 });\n }\n\n const params = normalizeQueryParams(body.params);\n const tenantContextError = requireTenantContext(options, params);\n if (tenantContextError) {\n return tenantContextError;\n }\n\n const namedQuery = resolveNamedQuery(\n body.queryName,\n params,\n options.queries\n );\n if (!namedQuery.ok) {\n return namedQuery.response;\n }\n return executeNamedQuery(namedQuery.queryName, namedQuery.query, params);\n };\n}\n"]}
@@ -1,29 +1,125 @@
1
+ import * as convex_server from 'convex/server';
2
+ import * as convex_values from 'convex/values';
3
+
1
4
  /**
2
5
  * neo4jSync module implementation.
3
6
  */
4
- declare const syncNodeToNeo4j: any;
5
- declare const syncEdgeToNeo4j: any;
6
- declare const syncAllNodesToNeo4j: any;
7
- declare const syncAllEdgesToNeo4j: any;
8
- declare const backfillAllToNeo4j: any;
9
- declare const processRetryQueue: any;
7
+ type SyncEntityType = "node" | "edge" | "embedding";
8
+ type SyncOperation = "upsert" | "delete" | "sync";
9
+ declare const syncNodeToNeo4j: convex_server.RegisteredAction<"internal", {
10
+ nodeId: convex_values.GenericId<"epistemicNodes">;
11
+ operation: "upsert" | "delete";
12
+ }, Promise<Readonly<{
13
+ error?: string;
14
+ entityType: SyncEntityType;
15
+ operation: SyncOperation;
16
+ skipped?: boolean;
17
+ skippedReason?: string;
18
+ success: boolean;
19
+ }>>>;
20
+ declare const syncEdgeToNeo4j: convex_server.RegisteredAction<"internal", {
21
+ operation: "upsert" | "delete";
22
+ edgeId: convex_values.GenericId<"epistemicEdges">;
23
+ }, Promise<Readonly<{
24
+ error?: string;
25
+ entityType: SyncEntityType;
26
+ operation: SyncOperation;
27
+ skipped?: boolean;
28
+ skippedReason?: string;
29
+ success: boolean;
30
+ }>>>;
31
+ declare const syncAllNodesToNeo4j: convex_server.RegisteredAction<"internal", {
32
+ cursor?: string | undefined;
33
+ batchSize?: number | undefined;
34
+ }, Promise<{
35
+ synced: number;
36
+ failed: number;
37
+ hasMore: boolean;
38
+ nextCursor?: undefined;
39
+ } | {
40
+ synced: number;
41
+ failed: number;
42
+ hasMore: boolean;
43
+ nextCursor: string | null | undefined;
44
+ }>>;
45
+ declare const syncAllEdgesToNeo4j: convex_server.RegisteredAction<"internal", {
46
+ cursor?: string | undefined;
47
+ batchSize?: number | undefined;
48
+ }, Promise<{
49
+ synced: number;
50
+ failed: number;
51
+ hasMore: boolean;
52
+ nextCursor?: undefined;
53
+ } | {
54
+ synced: number;
55
+ failed: number;
56
+ hasMore: boolean;
57
+ nextCursor: string | null | undefined;
58
+ }>>;
59
+ declare const backfillAllToNeo4j: convex_server.RegisteredAction<"internal", {
60
+ batchSize?: number | undefined;
61
+ }, Promise<{
62
+ totalNodes: number;
63
+ totalEdges: number;
64
+ totalFailed: number;
65
+ }>>;
66
+ declare const processRetryQueue: convex_server.RegisteredAction<"internal", {
67
+ limit?: number | undefined;
68
+ }, Promise<{
69
+ processed: number;
70
+ succeeded: number;
71
+ failed: number;
72
+ }>>;
10
73
  /**
11
74
  * Sync an embedding vector to an existing Neo4j node.
12
75
  * Called after saveEpistemicNodeEmbedding completes, ensuring the
13
76
  * embedding reaches Neo4j even if the initial node sync ran before
14
77
  * the embedding was generated.
15
78
  */
16
- declare const syncEmbeddingToNeo4j: any;
17
- declare const checkNeo4jHealth: any;
79
+ declare const syncEmbeddingToNeo4j: convex_server.RegisteredAction<"internal", {
80
+ nodeId: convex_values.GenericId<"epistemicNodes">;
81
+ }, Promise<Readonly<{
82
+ error?: string;
83
+ entityType: SyncEntityType;
84
+ operation: SyncOperation;
85
+ skipped?: boolean;
86
+ skippedReason?: string;
87
+ success: boolean;
88
+ }>>>;
89
+ declare const checkNeo4jHealth: convex_server.RegisteredAction<"internal", {}, Promise<{
90
+ uri: string | undefined;
91
+ healthy: boolean;
92
+ nodeCount?: number;
93
+ error?: string;
94
+ }>>;
18
95
  /**
19
96
  * Re-sync all epistemic nodes to Neo4j with updated properties (paginated)
20
97
  * Use after adding new fields to the sync
21
98
  */
22
- declare const resyncAllNodes: any;
99
+ declare const resyncAllNodes: convex_server.RegisteredAction<"internal", {
100
+ nodeType?: string | undefined;
101
+ cursor?: string | undefined;
102
+ batchSize?: number | undefined;
103
+ }, Promise<{
104
+ synced: number;
105
+ failed: number;
106
+ total: number;
107
+ hasMore: boolean;
108
+ nextCursor: string | null | undefined;
109
+ }>>;
23
110
  /**
24
111
  * Re-sync all epistemic edges to Neo4j with updated properties (paginated)
25
112
  * Use after adding new fields to the sync (e.g., classification fields)
26
113
  */
27
- declare const resyncAllEdges: any;
114
+ declare const resyncAllEdges: convex_server.RegisteredAction<"internal", {
115
+ cursor?: string | undefined;
116
+ batchSize?: number | undefined;
117
+ }, Promise<{
118
+ synced: number;
119
+ failed: number;
120
+ total: number;
121
+ hasMore: boolean;
122
+ nextCursor: string | null | undefined;
123
+ }>>;
28
124
 
29
125
  export { backfillAllToNeo4j, checkNeo4jHealth, processRetryQueue, resyncAllEdges, resyncAllNodes, syncAllEdgesToNeo4j, syncAllNodesToNeo4j, syncEdgeToNeo4j, syncEmbeddingToNeo4j, syncNodeToNeo4j };