@lucern/graph-sync 0.3.0-alpha.10

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.
@@ -0,0 +1,1000 @@
1
+ "use node";
2
+ import { v } from 'convex/values';
3
+ import { NODE_TYPE_TO_LABEL, EDGE_TYPE_TO_REL } from '@lucern/graph-primitives/graphTypes';
4
+ import { permissiveReturn } from '@lucern/contracts/schema-helpers/validators';
5
+ import { componentsGeneric, anyApi, internalActionGeneric } from 'convex/server';
6
+ import neo4j from 'neo4j-driver';
7
+
8
+ componentsGeneric();
9
+ var internal = anyApi;
10
+ var internalAction = internalActionGeneric;
11
+ var VALID_NODE_LABELS = /* @__PURE__ */ new Set([
12
+ // Ontological
13
+ "ValueChain",
14
+ "Function",
15
+ "FinSector",
16
+ "Company",
17
+ "Person",
18
+ "Investor",
19
+ // Epistemic
20
+ "Theme",
21
+ "Belief",
22
+ "Question",
23
+ "Evidence",
24
+ "Source",
25
+ "Decision",
26
+ "Sprint",
27
+ "Claim",
28
+ "Synthesis",
29
+ "Answer"
30
+ ]);
31
+ var VALID_RELATIONSHIP_TYPES = /* @__PURE__ */ new Set([
32
+ // Cross-layer edges
33
+ "EXTRACTED_FROM",
34
+ "ANSWERS",
35
+ "RESPONDS_TO",
36
+ "INFORMS",
37
+ "QUALIFIES",
38
+ "TESTS",
39
+ "EXPLORES",
40
+ "BASED_ON",
41
+ "RELATES_TO_THESIS",
42
+ "BELONGS_TO",
43
+ "PLAYS_THEME",
44
+ // Same-layer edges
45
+ "SUPERSEDES",
46
+ "SAME_AS",
47
+ "DEPENDS_ON",
48
+ "REINFORCES",
49
+ "PARENT_OF",
50
+ "CHILD_OF",
51
+ "FALSIFIED_BY",
52
+ "EXCLUSIVE_WITH",
53
+ "COLLAPSES_IF",
54
+ "CASCADE_FROM",
55
+ "STRENGTHENED_BY",
56
+ "WEAKENED_BY",
57
+ "ALTERNATIVE_TO",
58
+ "SUBSUMES",
59
+ "VALIDATED_BY",
60
+ "REQUIRED_FOR",
61
+ "PREREQUISITE_FOR",
62
+ "PARALLEL_TO",
63
+ "CORROBORATES",
64
+ "EXTENDS",
65
+ "SAME_SOURCE_AS",
66
+ "SAME_THEME_AS",
67
+ // Ontological
68
+ "EVALUATES",
69
+ "PERSPECTIVE_ON",
70
+ "WORKS_AT",
71
+ "PARTICIPATES_IN",
72
+ "PERFORMS",
73
+ "FUNCTION_IN",
74
+ "IMPACTS",
75
+ "INVESTED_IN",
76
+ "RAISED_FROM",
77
+ "BASED_ON_BELIEF",
78
+ "BASED_ON_QUESTION",
79
+ "BLOCKED_BY_CONTRADICTION",
80
+ "INFORMED_BY_THEME"
81
+ ]);
82
+ function validateLabel(label) {
83
+ if (!VALID_NODE_LABELS.has(label)) {
84
+ throw new Error(
85
+ `[Neo4j Security] Invalid node label: ${label}. Must be one of: ${Array.from(VALID_NODE_LABELS).join(", ")}`
86
+ );
87
+ }
88
+ }
89
+ function validateRelType(relType) {
90
+ if (!VALID_RELATIONSHIP_TYPES.has(relType)) {
91
+ throw new Error(
92
+ `[Neo4j Security] Invalid relationship type: ${relType}. Must be one of: ${Array.from(VALID_RELATIONSHIP_TYPES).join(", ")}`
93
+ );
94
+ }
95
+ }
96
+ var driver = null;
97
+ function getDriver() {
98
+ if (!driver) {
99
+ const uri = process.env.NEO4J_URI;
100
+ const user = process.env.NEO4J_USER;
101
+ const password = process.env.NEO4J_PASSWORD;
102
+ if (!uri || !user || !password) {
103
+ throw new Error(
104
+ "[Neo4j Driver] Missing credentials. Set NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD via `npx convex env set`"
105
+ );
106
+ }
107
+ driver = neo4j.driver(uri, neo4j.auth.basic(user, password), {
108
+ // Connection pool settings
109
+ maxConnectionPoolSize: 50,
110
+ connectionAcquisitionTimeout: 3e4,
111
+ // Logging
112
+ logging: {
113
+ level: "warn",
114
+ logger: (level, message) => console.log(`[Neo4j ${level}] ${message}`)
115
+ }
116
+ });
117
+ }
118
+ return driver;
119
+ }
120
+ var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
121
+ var COMPLEX_QUERY_TIMEOUT_MS = 6e4;
122
+ function toNeo4jParams(params) {
123
+ const result = {};
124
+ for (const [key, value] of Object.entries(params)) {
125
+ if (typeof value === "number" && Number.isInteger(value)) {
126
+ result[key] = neo4j.int(value);
127
+ } else if (Array.isArray(value)) {
128
+ result[key] = value.map(
129
+ (v2) => typeof v2 === "number" && Number.isInteger(v2) ? neo4j.int(v2) : v2
130
+ );
131
+ } else {
132
+ result[key] = value;
133
+ }
134
+ }
135
+ return result;
136
+ }
137
+ async function runCypher(query, params = {}, timeoutMs = DEFAULT_QUERY_TIMEOUT_MS) {
138
+ const neo4jDriver = getDriver();
139
+ const session = neo4jDriver.session();
140
+ try {
141
+ const neo4jParams = toNeo4jParams(params);
142
+ const result = await session.run(query, neo4jParams, {
143
+ timeout: neo4j.int(timeoutMs)
144
+ });
145
+ return result.records.map((record) => {
146
+ const obj = {};
147
+ for (const key of record.keys) {
148
+ const field = String(key);
149
+ obj[field] = convertNeo4jValue(record.get(field));
150
+ }
151
+ return obj;
152
+ });
153
+ } finally {
154
+ await session.close();
155
+ }
156
+ }
157
+ async function runWriteTransaction(query, params = {}, timeoutMs = DEFAULT_QUERY_TIMEOUT_MS) {
158
+ const neo4jDriver = getDriver();
159
+ const session = neo4jDriver.session();
160
+ try {
161
+ const neo4jParams = toNeo4jParams(params);
162
+ const result = await session.executeWrite(
163
+ async (tx) => {
164
+ return await tx.run(query, neo4jParams);
165
+ },
166
+ { timeout: timeoutMs }
167
+ );
168
+ return result.records.map((record) => {
169
+ const obj = {};
170
+ for (const key of record.keys) {
171
+ const field = String(key);
172
+ obj[field] = convertNeo4jValue(record.get(field));
173
+ }
174
+ return obj;
175
+ });
176
+ } finally {
177
+ await session.close();
178
+ }
179
+ }
180
+ async function runBatchTransaction(queries, timeoutMs = COMPLEX_QUERY_TIMEOUT_MS) {
181
+ const neo4jDriver = getDriver();
182
+ const session = neo4jDriver.session();
183
+ try {
184
+ await session.executeWrite(
185
+ async (tx) => {
186
+ for (const { query, params } of queries) {
187
+ await tx.run(query, params);
188
+ }
189
+ },
190
+ { timeout: timeoutMs }
191
+ );
192
+ } finally {
193
+ await session.close();
194
+ }
195
+ }
196
+ async function upsertNode(label, globalId, properties) {
197
+ validateLabel(label);
198
+ await runWriteTransaction(
199
+ `
200
+ MERGE (n:${label} {globalId: $globalId})
201
+ SET n += $properties
202
+ SET n.updatedAt = timestamp()
203
+ `,
204
+ { globalId, properties }
205
+ );
206
+ }
207
+ async function deleteNode(globalId) {
208
+ await runWriteTransaction(
209
+ `
210
+ MATCH (n {globalId: $globalId})
211
+ DETACH DELETE n
212
+ `,
213
+ { globalId }
214
+ );
215
+ }
216
+ async function batchUpsertNodes(label, nodes) {
217
+ if (nodes.length === 0) {
218
+ return;
219
+ }
220
+ validateLabel(label);
221
+ await runWriteTransaction(
222
+ `
223
+ UNWIND $nodes as node
224
+ MERGE (n:${label} {globalId: node.globalId})
225
+ SET n += node.properties
226
+ SET n.updatedAt = timestamp()
227
+ `,
228
+ { nodes }
229
+ );
230
+ }
231
+ async function upsertEdge(relType, globalId, fromGlobalId, toGlobalId, properties = {}) {
232
+ validateRelType(relType);
233
+ await runWriteTransaction(
234
+ `
235
+ MATCH (from {globalId: $fromGlobalId})
236
+ MATCH (to {globalId: $toGlobalId})
237
+ MERGE (from)-[r:${relType} {globalId: $globalId}]->(to)
238
+ SET r += $properties
239
+ SET r.updatedAt = timestamp()
240
+ `,
241
+ { globalId, fromGlobalId, toGlobalId, properties }
242
+ );
243
+ }
244
+ async function deleteEdge(globalId) {
245
+ await runWriteTransaction(
246
+ `
247
+ MATCH ()-[r {globalId: $globalId}]->()
248
+ DELETE r
249
+ `,
250
+ { globalId }
251
+ );
252
+ }
253
+ async function batchUpsertEdges(edges) {
254
+ if (edges.length === 0) {
255
+ return;
256
+ }
257
+ const byType = /* @__PURE__ */ new Map();
258
+ for (const edge of edges) {
259
+ const existing = byType.get(edge.relType) || [];
260
+ existing.push(edge);
261
+ byType.set(edge.relType, existing);
262
+ }
263
+ const queries = [];
264
+ for (const [relType, typeEdges] of byType) {
265
+ queries.push({
266
+ query: `
267
+ UNWIND $edges as edge
268
+ MATCH (from {globalId: edge.fromGlobalId})
269
+ MATCH (to {globalId: edge.toGlobalId})
270
+ MERGE (from)-[r:${relType} {globalId: edge.globalId}]->(to)
271
+ SET r += edge.properties
272
+ SET r.updatedAt = timestamp()
273
+ `,
274
+ params: {
275
+ edges: typeEdges.map((e) => ({
276
+ globalId: e.globalId,
277
+ fromGlobalId: e.fromGlobalId,
278
+ toGlobalId: e.toGlobalId,
279
+ properties: e.properties || {}
280
+ }))
281
+ }
282
+ });
283
+ }
284
+ await runBatchTransaction(queries);
285
+ }
286
+ async function healthCheck() {
287
+ try {
288
+ const result = await runCypher(
289
+ "MATCH (n) RETURN count(n) as count LIMIT 1"
290
+ );
291
+ return {
292
+ healthy: true,
293
+ nodeCount: result[0]?.count || 0
294
+ };
295
+ } catch (error) {
296
+ return {
297
+ healthy: false,
298
+ error: error instanceof Error ? error.message : "Unknown error"
299
+ };
300
+ }
301
+ }
302
+ function getConnectionInfo() {
303
+ return {
304
+ uri: process.env.NEO4J_URI,
305
+ user: process.env.NEO4J_USER,
306
+ configured: Boolean(
307
+ process.env.NEO4J_URI && process.env.NEO4J_USER && process.env.NEO4J_PASSWORD
308
+ )
309
+ };
310
+ }
311
+ function convertNeo4jValue(value) {
312
+ if (value === null || value === void 0) {
313
+ return null;
314
+ }
315
+ if (neo4j.isInt(value)) {
316
+ return neo4j.integer.toNumber(value);
317
+ }
318
+ if (neo4j.isDate(value) || neo4j.isDateTime(value) || neo4j.isTime(value)) {
319
+ return value.toString();
320
+ }
321
+ if (Array.isArray(value)) {
322
+ return value.map(convertNeo4jValue);
323
+ }
324
+ if (value && typeof value === "object" && "properties" in value) {
325
+ const nodeObj = value;
326
+ const result = {};
327
+ for (const [k, v2] of Object.entries(nodeObj.properties)) {
328
+ result[k] = convertNeo4jValue(v2);
329
+ }
330
+ return result;
331
+ }
332
+ if (typeof value === "object") {
333
+ const result = {};
334
+ for (const [k, v2] of Object.entries(value)) {
335
+ result[k] = convertNeo4jValue(v2);
336
+ }
337
+ return result;
338
+ }
339
+ return value;
340
+ }
341
+
342
+ // src/neo4jSync.ts
343
+ function buildSyncResponse(entityType, operation, fields) {
344
+ return {
345
+ entityType,
346
+ operation,
347
+ ...fields
348
+ };
349
+ }
350
+ function buildSyncSuccess(entityType, operation, fields) {
351
+ return buildSyncResponse(entityType, operation, {
352
+ success: true,
353
+ ...fields
354
+ });
355
+ }
356
+ function buildSyncFailure(entityType, operation, error, fields) {
357
+ return buildSyncResponse(entityType, operation, {
358
+ success: false,
359
+ error,
360
+ ...fields
361
+ });
362
+ }
363
+ function buildNodeProperties(node) {
364
+ const metadata = node.metadata || {};
365
+ const pillar = metadata.pillar || metadata.topic || "";
366
+ const stage = metadata.stage || metadata.beliefStage || "";
367
+ const criticality = metadata.criticality || "";
368
+ const synthesizedFrom = metadata.synthesizedFrom || [];
369
+ const epistemicStatus = node.epistemicStatus || "";
370
+ const methodology = node.methodology || "";
371
+ const informationAsymmetry = node.informationAsymmetry || "";
372
+ const questionType = node.questionType || "";
373
+ const questionPriority = node.questionPriority || "";
374
+ const answerQuality = node.answerQuality || "";
375
+ const reversibility = node.reversibility || "";
376
+ const predictionMeta = node.predictionMeta;
377
+ return {
378
+ convexId: node._id,
379
+ canonicalText: node.canonicalText || "",
380
+ title: node.title || "",
381
+ status: node.status || "active",
382
+ subtype: node.subtype || "",
383
+ domain: node.domain || "",
384
+ confidence: node.confidence || 0,
385
+ verificationStatus: node.verificationStatus || "unverified",
386
+ sourceType: node.sourceType || "unknown",
387
+ createdAt: node.createdAt,
388
+ updatedAt: node.updatedAt || Date.now(),
389
+ createdBy: node.createdBy || "",
390
+ nodeType: node.nodeType,
391
+ epistemicLayer: node.epistemicLayer || "",
392
+ // Project and metadata fields
393
+ projectId: node.projectId || "",
394
+ tenantId: node.tenantId || "",
395
+ workspaceId: node.workspaceId || "",
396
+ pillar,
397
+ stage,
398
+ criticality,
399
+ synthesizedFromCount: synthesizedFrom.length,
400
+ // Classification fields (Logic Machine)
401
+ epistemicStatus,
402
+ methodology,
403
+ informationAsymmetry,
404
+ questionType,
405
+ questionPriority,
406
+ answerQuality,
407
+ reversibility,
408
+ isPrediction: predictionMeta?.isPrediction ? "true" : "false",
409
+ expectedBy: predictionMeta?.expectedBy ? String(predictionMeta.expectedBy) : ""
410
+ };
411
+ }
412
+ function buildEdgeProperties(edge) {
413
+ return {
414
+ convexId: edge._id,
415
+ weight: edge.weight || 1,
416
+ confidence: edge.confidence || 0,
417
+ context: edge.context || "",
418
+ derivationType: edge.derivationType || "",
419
+ createdAt: edge.createdAt,
420
+ createdBy: edge.createdBy || "",
421
+ edgeType: edge.edgeType,
422
+ fromLayer: edge.fromLayer || "",
423
+ toLayer: edge.toLayer || "",
424
+ projectId: edge.projectId || "",
425
+ tenantId: edge.tenantId || "",
426
+ workspaceId: edge.workspaceId || "",
427
+ // Classification fields (Logic Machine)
428
+ reasoningMethod: edge.reasoningMethod || "",
429
+ logicalRole: edge.logicalRole || "",
430
+ temporalClass: edge.temporalClass || ""
431
+ };
432
+ }
433
+ var syncNodeToNeo4j = internalAction({
434
+ args: {
435
+ nodeId: v.id("epistemicNodes"),
436
+ operation: v.union(v.literal("upsert"), v.literal("delete"))
437
+ },
438
+ returns: permissiveReturn,
439
+ handler: async (ctx, args) => {
440
+ const connInfo = getConnectionInfo();
441
+ if (!connInfo.configured) {
442
+ console.warn(
443
+ "[Neo4j Sync] Missing Neo4j credentials, skipping sync. Set via `npx convex env set`"
444
+ );
445
+ return buildSyncFailure("node", args.operation, "Missing credentials", {
446
+ skippedReason: "credentials_missing"
447
+ });
448
+ }
449
+ const node = await ctx.runQuery(internal.neo4jSyncHelpers.getNodeForSync, {
450
+ nodeId: args.nodeId
451
+ });
452
+ if (!node) {
453
+ if (args.operation === "upsert") {
454
+ return buildSyncFailure("node", args.operation, "Node not found", {
455
+ skippedReason: "source_node_missing"
456
+ });
457
+ }
458
+ console.log("[Neo4j Sync] Node not found for delete, skipping");
459
+ return buildSyncSuccess("node", args.operation, {
460
+ skipped: true,
461
+ skippedReason: "source_node_missing"
462
+ });
463
+ }
464
+ const label = NODE_TYPE_TO_LABEL[node.nodeType] || "Node";
465
+ try {
466
+ if (args.operation === "delete") {
467
+ await deleteNode(node.globalId);
468
+ console.log(`[Neo4j Sync] Deleted node ${node.globalId}`);
469
+ } else {
470
+ const props = buildNodeProperties(node);
471
+ const embedding = await ctx.runQuery(
472
+ internal.neo4jSyncHelpers.getEmbeddingForSync,
473
+ { nodeId: args.nodeId }
474
+ );
475
+ if (embedding) {
476
+ props.embedding = embedding;
477
+ }
478
+ await upsertNode(label, node.globalId, props);
479
+ console.log(
480
+ `[Neo4j Sync] Upserted node ${node.globalId} as ${label} with projectId=${node.projectId}` + (embedding ? ` (with ${embedding.length}-dim embedding)` : "")
481
+ );
482
+ }
483
+ await ctx.runMutation(internal.neo4jSyncHelpers.logSyncEvent, {
484
+ eventType: args.operation === "delete" ? "node_deleted" : node ? "node_updated" : "node_created",
485
+ entityId: args.nodeId,
486
+ entityType: node.nodeType,
487
+ status: "success"
488
+ });
489
+ return buildSyncSuccess("node", args.operation);
490
+ } catch (error) {
491
+ const errorMsg = error instanceof Error ? error.message : "Unknown error";
492
+ console.error("[Neo4j Sync] Node sync error:", errorMsg);
493
+ await ctx.runMutation(internal.neo4jSyncHelpers.logSyncEvent, {
494
+ eventType: args.operation === "delete" ? "node_deleted" : "node_updated",
495
+ entityId: args.nodeId,
496
+ entityType: node.nodeType,
497
+ status: "failed",
498
+ error: errorMsg
499
+ });
500
+ await ctx.runMutation(internal.neo4jSyncHelpers.queueForRetry, {
501
+ entityType: "node",
502
+ entityId: args.nodeId,
503
+ operation: args.operation,
504
+ error: errorMsg
505
+ });
506
+ return buildSyncFailure("node", args.operation, errorMsg);
507
+ }
508
+ }
509
+ });
510
+ var syncEdgeToNeo4j = internalAction({
511
+ args: {
512
+ edgeId: v.id("epistemicEdges"),
513
+ operation: v.union(v.literal("upsert"), v.literal("delete"))
514
+ },
515
+ returns: permissiveReturn,
516
+ handler: async (ctx, args) => {
517
+ const connInfo = getConnectionInfo();
518
+ if (!connInfo.configured) {
519
+ console.warn(
520
+ "[Neo4j Sync] Missing Neo4j credentials, skipping sync. Set via `npx convex env set`"
521
+ );
522
+ return buildSyncFailure("edge", args.operation, "Missing credentials", {
523
+ skippedReason: "credentials_missing"
524
+ });
525
+ }
526
+ const edge = await ctx.runQuery(internal.neo4jSyncHelpers.getEdgeForSync, {
527
+ edgeId: args.edgeId
528
+ });
529
+ if (!edge) {
530
+ if (args.operation === "upsert") {
531
+ return buildSyncFailure("edge", args.operation, "Edge not found", {
532
+ skippedReason: "source_edge_missing"
533
+ });
534
+ }
535
+ console.log("[Neo4j Sync] Edge not found for delete, skipping");
536
+ return buildSyncSuccess("edge", args.operation, {
537
+ skipped: true,
538
+ skippedReason: "source_edge_missing"
539
+ });
540
+ }
541
+ if (!edge.fromGlobalId || !edge.toGlobalId) {
542
+ console.warn(
543
+ "[Neo4j Sync] Edge missing fromGlobalId or toGlobalId, skipping"
544
+ );
545
+ return buildSyncFailure(
546
+ "edge",
547
+ args.operation,
548
+ "Edge missing connected node globalIds",
549
+ {
550
+ skippedReason: "edge_endpoint_missing"
551
+ }
552
+ );
553
+ }
554
+ const relType = EDGE_TYPE_TO_REL[edge.edgeType] || edge.edgeType.toUpperCase();
555
+ try {
556
+ if (args.operation === "delete") {
557
+ await deleteEdge(edge.globalId);
558
+ console.log(`[Neo4j Sync] Deleted edge ${edge.globalId}`);
559
+ } else {
560
+ await upsertEdge(
561
+ relType,
562
+ edge.globalId,
563
+ edge.fromGlobalId,
564
+ edge.toGlobalId,
565
+ {
566
+ convexId: args.edgeId,
567
+ weight: edge.weight || 1,
568
+ confidence: edge.confidence || 0,
569
+ context: edge.context || "",
570
+ derivationType: edge.derivationType || "",
571
+ createdAt: edge.createdAt,
572
+ createdBy: edge.createdBy || "",
573
+ edgeType: edge.edgeType,
574
+ fromLayer: edge.fromLayer || "",
575
+ toLayer: edge.toLayer || "",
576
+ // Classification fields (Logic Machine)
577
+ reasoningMethod: edge.reasoningMethod || "",
578
+ logicalRole: edge.logicalRole || "",
579
+ temporalClass: edge.temporalClass || ""
580
+ }
581
+ );
582
+ console.log(
583
+ `[Neo4j Sync] Upserted edge ${edge.globalId} as ${relType}`
584
+ );
585
+ }
586
+ await ctx.runMutation(internal.neo4jSyncHelpers.logSyncEvent, {
587
+ eventType: args.operation === "delete" ? "edge_deleted" : "edge_created",
588
+ entityId: args.edgeId,
589
+ entityType: edge.edgeType,
590
+ status: "success"
591
+ });
592
+ return buildSyncSuccess("edge", args.operation);
593
+ } catch (error) {
594
+ const errorMsg = error instanceof Error ? error.message : "Unknown error";
595
+ console.error("[Neo4j Sync] Edge sync error:", errorMsg);
596
+ await ctx.runMutation(internal.neo4jSyncHelpers.logSyncEvent, {
597
+ eventType: args.operation === "delete" ? "edge_deleted" : "edge_created",
598
+ entityId: args.edgeId,
599
+ entityType: edge.edgeType,
600
+ status: "failed",
601
+ error: errorMsg
602
+ });
603
+ await ctx.runMutation(internal.neo4jSyncHelpers.queueForRetry, {
604
+ entityType: "edge",
605
+ entityId: args.edgeId,
606
+ operation: args.operation,
607
+ error: errorMsg
608
+ });
609
+ return buildSyncFailure("edge", args.operation, errorMsg);
610
+ }
611
+ }
612
+ });
613
+ var syncAllNodesToNeo4j = internalAction({
614
+ args: {
615
+ batchSize: v.optional(v.number()),
616
+ cursor: v.optional(v.string())
617
+ },
618
+ returns: permissiveReturn,
619
+ handler: async (ctx, args) => {
620
+ const batchSize = args.batchSize ?? 100;
621
+ const result = await ctx.runQuery(
622
+ internal.neo4jSyncHelpers.getNodeBatchForSync,
623
+ {
624
+ limit: batchSize,
625
+ cursor: args.cursor
626
+ }
627
+ );
628
+ if (result.nodes.length === 0) {
629
+ return { synced: 0, failed: 0, hasMore: false };
630
+ }
631
+ const nodesByLabel = /* @__PURE__ */ new Map();
632
+ for (const node of result.nodes) {
633
+ const label = NODE_TYPE_TO_LABEL[node.nodeType] || "Node";
634
+ if (!nodesByLabel.has(label)) {
635
+ nodesByLabel.set(label, []);
636
+ }
637
+ nodesByLabel.get(label)?.push({
638
+ globalId: node.globalId,
639
+ properties: buildNodeProperties(node)
640
+ });
641
+ }
642
+ let synced = 0;
643
+ let failed = 0;
644
+ for (const [label, nodes] of nodesByLabel) {
645
+ try {
646
+ await batchUpsertNodes(label, nodes);
647
+ synced += nodes.length;
648
+ console.log(
649
+ `[Neo4j Sync] Batch upserted ${nodes.length} ${label} nodes`
650
+ );
651
+ } catch (error) {
652
+ console.error(`[Neo4j Sync] Batch upsert failed for ${label}:`, error);
653
+ failed += nodes.length;
654
+ }
655
+ }
656
+ return {
657
+ synced,
658
+ failed,
659
+ hasMore: result.hasMore,
660
+ nextCursor: result.nextCursor
661
+ };
662
+ }
663
+ });
664
+ var syncAllEdgesToNeo4j = internalAction({
665
+ args: {
666
+ batchSize: v.optional(v.number()),
667
+ cursor: v.optional(v.string())
668
+ },
669
+ returns: permissiveReturn,
670
+ handler: async (ctx, args) => {
671
+ const batchSize = args.batchSize ?? 100;
672
+ const result = await ctx.runQuery(
673
+ internal.neo4jSyncHelpers.getEdgeBatchForSync,
674
+ {
675
+ limit: batchSize,
676
+ cursor: args.cursor
677
+ }
678
+ );
679
+ if (result.edges.length === 0) {
680
+ return { synced: 0, failed: 0, hasMore: false };
681
+ }
682
+ const edgesToSync = [];
683
+ for (const edge of result.edges) {
684
+ if (!edge.fromGlobalId || !edge.toGlobalId) {
685
+ console.warn(
686
+ `[Neo4j Sync] Skipping edge ${edge.globalId} - missing globalIds`
687
+ );
688
+ continue;
689
+ }
690
+ const relType = EDGE_TYPE_TO_REL[edge.edgeType] || edge.edgeType.toUpperCase();
691
+ edgesToSync.push({
692
+ relType,
693
+ globalId: edge.globalId,
694
+ fromGlobalId: edge.fromGlobalId,
695
+ toGlobalId: edge.toGlobalId,
696
+ properties: buildEdgeProperties(edge)
697
+ });
698
+ }
699
+ let synced = 0;
700
+ let failed = 0;
701
+ try {
702
+ await batchUpsertEdges(edgesToSync);
703
+ synced = edgesToSync.length;
704
+ console.log(`[Neo4j Sync] Batch upserted ${synced} edges`);
705
+ } catch (error) {
706
+ console.error("[Neo4j Sync] Batch edge upsert failed:", error);
707
+ failed = edgesToSync.length;
708
+ }
709
+ return {
710
+ synced,
711
+ failed,
712
+ hasMore: result.hasMore,
713
+ nextCursor: result.nextCursor
714
+ };
715
+ }
716
+ });
717
+ var backfillAllToNeo4j = internalAction({
718
+ args: {
719
+ batchSize: v.optional(v.number())
720
+ },
721
+ returns: permissiveReturn,
722
+ handler: async (ctx, args) => {
723
+ const batchSize = args.batchSize ?? 100;
724
+ let totalNodes = 0;
725
+ let totalEdges = 0;
726
+ let totalFailed = 0;
727
+ console.log("[Neo4j Sync] Starting full backfill...");
728
+ let nodeCursor;
729
+ do {
730
+ const result = await ctx.runAction(
731
+ internal.neo4jSync.syncAllNodesToNeo4j,
732
+ {
733
+ batchSize,
734
+ cursor: nodeCursor
735
+ }
736
+ );
737
+ totalNodes += result.synced;
738
+ totalFailed += result.failed;
739
+ nodeCursor = result.hasMore ? result.nextCursor : void 0;
740
+ console.log(
741
+ `[Neo4j Sync] Nodes progress: ${totalNodes} synced, ${totalFailed} failed`
742
+ );
743
+ } while (nodeCursor);
744
+ console.log(`[Neo4j Sync] Finished nodes: ${totalNodes} synced`);
745
+ let edgeCursor;
746
+ do {
747
+ const result = await ctx.runAction(
748
+ internal.neo4jSync.syncAllEdgesToNeo4j,
749
+ {
750
+ batchSize,
751
+ cursor: edgeCursor
752
+ }
753
+ );
754
+ totalEdges += result.synced;
755
+ totalFailed += result.failed;
756
+ edgeCursor = result.hasMore ? result.nextCursor : void 0;
757
+ console.log(
758
+ `[Neo4j Sync] Edges progress: ${totalEdges} synced, ${totalFailed} failed`
759
+ );
760
+ } while (edgeCursor);
761
+ console.log(
762
+ `[Neo4j Sync] Backfill complete: ${totalNodes} nodes, ${totalEdges} edges, ${totalFailed} failed`
763
+ );
764
+ return {
765
+ totalNodes,
766
+ totalEdges,
767
+ totalFailed
768
+ };
769
+ }
770
+ });
771
+ var processRetryQueue = internalAction({
772
+ args: {
773
+ limit: v.optional(v.number())
774
+ },
775
+ returns: permissiveReturn,
776
+ handler: async (ctx, args) => {
777
+ const limit = args.limit ?? 10;
778
+ const pendingItems = await ctx.runQuery(
779
+ internal.neo4jSyncHelpers.getPendingRetries,
780
+ { limit }
781
+ );
782
+ if (pendingItems.length === 0) {
783
+ return { processed: 0, succeeded: 0, failed: 0 };
784
+ }
785
+ let succeeded = 0;
786
+ let failed = 0;
787
+ for (const item of pendingItems) {
788
+ await ctx.runMutation(internal.neo4jSyncHelpers.updateQueueStatus, {
789
+ queueId: item._id,
790
+ status: "in_progress"
791
+ });
792
+ let result;
793
+ if (item.entityType === "node") {
794
+ result = await ctx.runAction(internal.neo4jSync.syncNodeToNeo4j, {
795
+ nodeId: item.entityId,
796
+ operation: item.operation
797
+ });
798
+ } else {
799
+ const resolved = await ctx.runQuery(
800
+ internal.neo4jSyncHelpers.resolveEdgeRetryTarget,
801
+ {
802
+ entityId: item.entityId
803
+ }
804
+ );
805
+ if (resolved.mode === "convex_id" || resolved.mode === "global_id_in_convex") {
806
+ result = await ctx.runAction(internal.neo4jSync.syncEdgeToNeo4j, {
807
+ edgeId: resolved.edgeId,
808
+ operation: item.operation
809
+ });
810
+ } else {
811
+ result = await ctx.runAction(
812
+ internal.neo4jEdgeAPI.retryProjectionByGlobalId,
813
+ {
814
+ globalId: resolved.edgeGlobalId
815
+ }
816
+ );
817
+ }
818
+ }
819
+ if (result.success) {
820
+ await ctx.runMutation(internal.neo4jSyncHelpers.updateQueueStatus, {
821
+ queueId: item._id,
822
+ status: "succeeded"
823
+ });
824
+ succeeded++;
825
+ } else {
826
+ const updated = await ctx.runMutation(
827
+ internal.neo4jSyncHelpers.incrementAttempts,
828
+ {
829
+ queueId: item._id,
830
+ error: result.error || "Unknown error"
831
+ }
832
+ );
833
+ if (updated.failed) {
834
+ failed++;
835
+ }
836
+ }
837
+ }
838
+ return { processed: pendingItems.length, succeeded, failed };
839
+ }
840
+ });
841
+ var syncEmbeddingToNeo4j = internalAction({
842
+ args: {
843
+ nodeId: v.id("epistemicNodes")
844
+ },
845
+ returns: permissiveReturn,
846
+ handler: async (ctx, args) => {
847
+ const connInfo = getConnectionInfo();
848
+ if (!connInfo.configured) {
849
+ return buildSyncFailure("embedding", "sync", "Missing credentials", {
850
+ skippedReason: "credentials_missing"
851
+ });
852
+ }
853
+ const node = await ctx.runQuery(internal.neo4jSyncHelpers.getNodeForSync, {
854
+ nodeId: args.nodeId
855
+ });
856
+ if (!node?.globalId) {
857
+ return buildSyncFailure("embedding", "sync", "Node not found", {
858
+ skippedReason: "source_node_missing"
859
+ });
860
+ }
861
+ const embedding = await ctx.runQuery(
862
+ internal.neo4jSyncHelpers.getEmbeddingForSync,
863
+ { nodeId: args.nodeId }
864
+ );
865
+ if (!embedding) {
866
+ return buildSyncFailure("embedding", "sync", "Embedding not found", {
867
+ skippedReason: "embedding_missing"
868
+ });
869
+ }
870
+ try {
871
+ await runWriteTransaction(
872
+ "MATCH (n {globalId: $globalId}) SET n.embedding = $embedding",
873
+ { globalId: node.globalId, embedding }
874
+ );
875
+ console.log(
876
+ `[Neo4j Sync] Synced ${embedding.length}-dim embedding for node ${node.globalId}`
877
+ );
878
+ return buildSyncSuccess("embedding", "sync");
879
+ } catch (error) {
880
+ const errorMsg = error instanceof Error ? error.message : "Unknown error";
881
+ console.error("[Neo4j Sync] Embedding sync error:", errorMsg);
882
+ return buildSyncFailure("embedding", "sync", errorMsg);
883
+ }
884
+ }
885
+ });
886
+ var checkNeo4jHealth = internalAction({
887
+ args: {},
888
+ returns: permissiveReturn,
889
+ handler: async () => {
890
+ const connInfo = getConnectionInfo();
891
+ if (!connInfo.configured) {
892
+ return {
893
+ healthy: false,
894
+ error: "Neo4j not configured",
895
+ uri: connInfo.uri
896
+ };
897
+ }
898
+ const health = await healthCheck();
899
+ return {
900
+ ...health,
901
+ uri: connInfo.uri
902
+ };
903
+ }
904
+ });
905
+ var resyncAllNodes = internalAction({
906
+ args: {
907
+ batchSize: v.optional(v.number()),
908
+ nodeType: v.optional(v.string()),
909
+ cursor: v.optional(v.string())
910
+ },
911
+ returns: permissiveReturn,
912
+ handler: async (ctx, args) => {
913
+ const batchSize = args.batchSize ?? 50;
914
+ const result = await ctx.runQuery(
915
+ internal.neo4jSyncHelpers.getAllNodesForResync,
916
+ {
917
+ nodeType: args.nodeType,
918
+ limit: batchSize,
919
+ cursor: args.cursor
920
+ }
921
+ );
922
+ let synced = 0;
923
+ let failed = 0;
924
+ for (const node of result.nodes) {
925
+ try {
926
+ const syncResult = await ctx.runAction(
927
+ internal.neo4jSync.syncNodeToNeo4j,
928
+ {
929
+ nodeId: node._id,
930
+ operation: "upsert"
931
+ }
932
+ );
933
+ if (syncResult.success) {
934
+ synced++;
935
+ } else {
936
+ failed++;
937
+ }
938
+ } catch (error) {
939
+ console.error(`[Neo4j Resync] Failed to sync node ${node._id}:`, error);
940
+ failed++;
941
+ }
942
+ }
943
+ return {
944
+ synced,
945
+ failed,
946
+ total: result.nodes.length,
947
+ hasMore: result.hasMore,
948
+ nextCursor: result.nextCursor
949
+ };
950
+ }
951
+ });
952
+ var resyncAllEdges = internalAction({
953
+ args: {
954
+ batchSize: v.optional(v.number()),
955
+ cursor: v.optional(v.string())
956
+ },
957
+ returns: permissiveReturn,
958
+ handler: async (ctx, args) => {
959
+ const batchSize = args.batchSize ?? 50;
960
+ const result = await ctx.runQuery(
961
+ internal.neo4jSyncHelpers.getAllEdgesForResync,
962
+ {
963
+ limit: batchSize,
964
+ cursor: args.cursor
965
+ }
966
+ );
967
+ let synced = 0;
968
+ let failed = 0;
969
+ for (const edge of result.edges) {
970
+ try {
971
+ const syncResult = await ctx.runAction(
972
+ internal.neo4jSync.syncEdgeToNeo4j,
973
+ {
974
+ edgeId: edge._id,
975
+ operation: "upsert"
976
+ }
977
+ );
978
+ if (syncResult.success) {
979
+ synced++;
980
+ } else {
981
+ failed++;
982
+ }
983
+ } catch (error) {
984
+ console.error(`[Neo4j Resync] Failed to sync edge ${edge._id}:`, error);
985
+ failed++;
986
+ }
987
+ }
988
+ return {
989
+ synced,
990
+ failed,
991
+ total: result.edges.length,
992
+ hasMore: result.hasMore,
993
+ nextCursor: result.nextCursor
994
+ };
995
+ }
996
+ });
997
+
998
+ export { backfillAllToNeo4j, checkNeo4jHealth, processRetryQueue, resyncAllEdges, resyncAllNodes, syncAllEdgesToNeo4j, syncAllNodesToNeo4j, syncEdgeToNeo4j, syncEmbeddingToNeo4j, syncNodeToNeo4j };
999
+ //# sourceMappingURL=neo4jSync.js.map
1000
+ //# sourceMappingURL=neo4jSync.js.map