@lucern/graph-sync 1.0.29 → 1.0.31
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.
- package/CHANGELOG.md +10 -0
- package/dist/index.d.ts +198 -17
- package/dist/index.js +465 -373
- package/dist/index.js.map +1 -1
- package/dist/neo4jDriver.js +2 -4
- package/dist/neo4jDriver.js.map +1 -1
- package/dist/neo4jEdgeAPI.d.ts +93 -5
- package/dist/neo4jEdgeAPI.js +118 -107
- package/dist/neo4jEdgeAPI.js.map +1 -1
- package/dist/neo4jQueries-D14Putpd.d.ts +839 -0
- package/dist/neo4jQueries.d.ts +2 -1
- package/dist/neo4jQueries.js +72 -44
- package/dist/neo4jQueries.js.map +1 -1
- package/dist/neo4jQueryRoute.js +92 -45
- package/dist/neo4jQueryRoute.js.map +1 -1
- package/dist/neo4jSync.d.ts +106 -10
- package/dist/neo4jSync.js +159 -162
- package/dist/neo4jSync.js.map +1 -1
- package/dist/neo4jSyncHelpers-DWr-lF-A.d.ts +208 -0
- package/dist/neo4jSyncHelpers.d.ts +3 -1
- package/dist/neo4jSyncHelpers.js +44 -26
- package/dist/neo4jSyncHelpers.js.map +1 -1
- package/dist/proof-attestation.json +1 -1
- package/package.json +3 -3
- package/dist/neo4jQueries-j3LrFKpY.d.ts +0 -301
- package/dist/neo4jSyncHelpers-vxe1-Gvw.d.ts +0 -58
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import neo4j from 'neo4j-driver';
|
|
2
|
-
import { v } from 'convex/values';
|
|
3
2
|
import { permissiveReturn } from '@lucern/contracts/schema-helpers/validators';
|
|
4
3
|
import { NODE_TYPE_TO_LABEL, EDGE_TYPE_TO_REL, getNeo4jRelType } from '@lucern/graph-primitives/graphTypes';
|
|
5
|
-
import {
|
|
4
|
+
import { v } from 'convex/values';
|
|
5
|
+
import { unsafeConvexAnyApi } from '@lucern/contracts/convex/unsafeAnyApi';
|
|
6
|
+
import { internalActionGeneric, actionGeneric, internalMutationGeneric, internalQueryGeneric } from 'convex/server';
|
|
6
7
|
|
|
7
8
|
var __defProp = Object.defineProperty;
|
|
8
9
|
var __export = (target, all) => {
|
|
@@ -121,7 +122,7 @@ function getDriver() {
|
|
|
121
122
|
const uri = process.env.NEO4J_URI;
|
|
122
123
|
const user = process.env.NEO4J_USER;
|
|
123
124
|
const password = process.env.NEO4J_PASSWORD;
|
|
124
|
-
if (!uri
|
|
125
|
+
if (!(uri && user && password)) {
|
|
125
126
|
throw new Error(
|
|
126
127
|
"[Neo4j Driver] Missing credentials. Set NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD via `npx convex env set`"
|
|
127
128
|
);
|
|
@@ -182,9 +183,7 @@ async function runWriteTransaction(query, params = {}, timeoutMs = DEFAULT_QUERY
|
|
|
182
183
|
try {
|
|
183
184
|
const neo4jParams = toNeo4jParams(params);
|
|
184
185
|
const result = await session.executeWrite(
|
|
185
|
-
async (tx) =>
|
|
186
|
-
return await tx.run(query, neo4jParams);
|
|
187
|
-
},
|
|
186
|
+
async (tx) => await tx.run(query, neo4jParams),
|
|
188
187
|
{ timeout: timeoutMs }
|
|
189
188
|
);
|
|
190
189
|
return result.records.map((record) => {
|
|
@@ -378,7 +377,9 @@ __export(neo4jEdgeAPI_exports, {
|
|
|
378
377
|
retryProjectionByGlobalId: () => retryProjectionByGlobalId,
|
|
379
378
|
updateEdge: () => updateEdge
|
|
380
379
|
});
|
|
381
|
-
var internal =
|
|
380
|
+
var internal = unsafeConvexAnyApi(
|
|
381
|
+
"graph-sync top-level module bundle lacks a committed Convex _generated/api surface"
|
|
382
|
+
);
|
|
382
383
|
var action = actionGeneric;
|
|
383
384
|
var internalAction = internalActionGeneric;
|
|
384
385
|
var internalMutation = internalMutationGeneric;
|
|
@@ -409,19 +410,21 @@ var DUAL_WRITE_EDGE_TYPES = [
|
|
|
409
410
|
"mentioned_in",
|
|
410
411
|
"perspective_on"
|
|
411
412
|
];
|
|
413
|
+
var LOWER_EDGE_TYPE_REGEX = /^[a-z0-9_]+$/u;
|
|
414
|
+
var UPPER_EDGE_TYPE_REGEX = /^[A-Z0-9_]+$/u;
|
|
412
415
|
function needsDualWrite(edgeType) {
|
|
413
416
|
return DUAL_WRITE_EDGE_TYPES.includes(edgeType);
|
|
414
417
|
}
|
|
415
418
|
function normalizeEdgeType(edgeType) {
|
|
416
419
|
const normalized = edgeType.trim().toLowerCase();
|
|
417
|
-
if (
|
|
420
|
+
if (!LOWER_EDGE_TYPE_REGEX.test(normalized)) {
|
|
418
421
|
throw new Error(`[Neo4j Edge API] Invalid edge type: ${edgeType}`);
|
|
419
422
|
}
|
|
420
423
|
return normalized;
|
|
421
424
|
}
|
|
422
425
|
function resolveRelationshipType(edgeType) {
|
|
423
426
|
const relType = getNeo4jRelType(edgeType);
|
|
424
|
-
if (
|
|
427
|
+
if (!UPPER_EDGE_TYPE_REGEX.test(relType)) {
|
|
425
428
|
throw new Error(`[Neo4j Edge API] Invalid relationship type: ${relType}`);
|
|
426
429
|
}
|
|
427
430
|
return relType;
|
|
@@ -436,10 +439,61 @@ function readNumberProperty(source, key) {
|
|
|
436
439
|
}
|
|
437
440
|
function metadataSummary(metadata) {
|
|
438
441
|
if (!metadata) {
|
|
439
|
-
return
|
|
442
|
+
return;
|
|
440
443
|
}
|
|
441
444
|
return Object.entries(metadata).map(([key, value]) => `${key}=${String(value)}`).slice(0, 8).join(" | ");
|
|
442
445
|
}
|
|
446
|
+
function readMetadata(metadata) {
|
|
447
|
+
return metadata && typeof metadata === "object" && !Array.isArray(metadata) ? metadata : void 0;
|
|
448
|
+
}
|
|
449
|
+
function neo4jEdgeProperties(args, edgeType, metadata, now) {
|
|
450
|
+
return {
|
|
451
|
+
globalId: args.globalId,
|
|
452
|
+
edgeType,
|
|
453
|
+
weight: args.weight ?? 1,
|
|
454
|
+
confidence: args.confidence ?? readNumberProperty(metadata, "confidence") ?? 1,
|
|
455
|
+
context: args.context ?? metadataSummary(metadata) ?? "",
|
|
456
|
+
derivationType: args.derivationType ?? "",
|
|
457
|
+
createdBy: args.createdBy,
|
|
458
|
+
createdAt: now,
|
|
459
|
+
updatedAt: now,
|
|
460
|
+
topicId: args.topicId ?? readStringProperty(metadata, "topicId") ?? "",
|
|
461
|
+
tenantId: args.tenantId ?? readStringProperty(metadata, "tenantId") ?? "",
|
|
462
|
+
workspaceId: args.workspaceId ?? readStringProperty(metadata, "workspaceId") ?? "",
|
|
463
|
+
fromLayer: args.fromLayer ?? "",
|
|
464
|
+
toLayer: args.toLayer ?? "",
|
|
465
|
+
fromNodeType: args.fromNodeType ?? "",
|
|
466
|
+
toNodeType: args.toNodeType ?? "",
|
|
467
|
+
reasoningMethod: args.reasoningMethod ?? "",
|
|
468
|
+
logicalRole: args.logicalRole ?? "",
|
|
469
|
+
temporalClass: args.temporalClass ?? "structural",
|
|
470
|
+
validFrom: args.validFrom ?? now,
|
|
471
|
+
validUntil: args.validUntil ?? null
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
function mirrorInputFromCreateArgs(args, edgeType) {
|
|
475
|
+
return {
|
|
476
|
+
globalId: args.globalId,
|
|
477
|
+
fromGlobalId: args.fromGlobalId,
|
|
478
|
+
toGlobalId: args.toGlobalId,
|
|
479
|
+
edgeType,
|
|
480
|
+
weight: args.weight,
|
|
481
|
+
confidence: args.confidence,
|
|
482
|
+
context: args.context,
|
|
483
|
+
derivationType: args.derivationType,
|
|
484
|
+
createdBy: args.createdBy,
|
|
485
|
+
topicId: args.topicId,
|
|
486
|
+
fromLayer: args.fromLayer,
|
|
487
|
+
toLayer: args.toLayer,
|
|
488
|
+
fromNodeType: args.fromNodeType,
|
|
489
|
+
toNodeType: args.toNodeType,
|
|
490
|
+
reasoningMethod: args.reasoningMethod,
|
|
491
|
+
logicalRole: args.logicalRole,
|
|
492
|
+
temporalClass: args.temporalClass,
|
|
493
|
+
validFrom: args.validFrom,
|
|
494
|
+
validUntil: args.validUntil
|
|
495
|
+
};
|
|
496
|
+
}
|
|
443
497
|
async function mirrorEdgeToConvex(ctx, args) {
|
|
444
498
|
await ctx.runMutation(internal.epistemicEdges.mirrorEdgeToConvex, args);
|
|
445
499
|
}
|
|
@@ -451,6 +505,40 @@ async function queueEdgeRetry(ctx, args) {
|
|
|
451
505
|
error: args.error
|
|
452
506
|
});
|
|
453
507
|
}
|
|
508
|
+
async function mirrorCreatedEdgeIfNeeded(ctx, args, edgeType) {
|
|
509
|
+
if (!needsDualWrite(edgeType)) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
await mirrorEdgeToConvex(ctx, mirrorInputFromCreateArgs(args, edgeType));
|
|
514
|
+
} catch (error) {
|
|
515
|
+
await queueEdgeRetry(ctx, {
|
|
516
|
+
globalId: args.globalId,
|
|
517
|
+
operation: "upsert",
|
|
518
|
+
error: `Convex mirror failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
async function handleMissingNeo4jEndpoints(ctx, args, edgeType) {
|
|
523
|
+
await queueEdgeRetry(ctx, {
|
|
524
|
+
globalId: args.globalId,
|
|
525
|
+
operation: "upsert",
|
|
526
|
+
error: `Source or target node not yet synced to Neo4j (from: ${args.fromGlobalId}, to: ${args.toGlobalId})`
|
|
527
|
+
});
|
|
528
|
+
if (needsDualWrite(edgeType)) {
|
|
529
|
+
try {
|
|
530
|
+
await mirrorEdgeToConvex(ctx, mirrorInputFromCreateArgs(args, edgeType));
|
|
531
|
+
} catch {
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
success: false,
|
|
536
|
+
globalId: args.globalId,
|
|
537
|
+
edgeType,
|
|
538
|
+
queuedForRetry: true,
|
|
539
|
+
reason: "nodes_not_synced"
|
|
540
|
+
};
|
|
541
|
+
}
|
|
454
542
|
var createEdge = internalAction({
|
|
455
543
|
args: {
|
|
456
544
|
globalId: v.string(),
|
|
@@ -486,31 +574,9 @@ var createEdge = internalAction({
|
|
|
486
574
|
}
|
|
487
575
|
const edgeType = normalizeEdgeType(args.edgeType);
|
|
488
576
|
const relType = resolveRelationshipType(edgeType);
|
|
489
|
-
const metadata = args.metadata && typeof args.metadata === "object" ? args.metadata : void 0;
|
|
490
577
|
const now = Date.now();
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
edgeType,
|
|
494
|
-
weight: args.weight ?? 1,
|
|
495
|
-
confidence: args.confidence ?? readNumberProperty(metadata, "confidence") ?? 1,
|
|
496
|
-
context: args.context ?? metadataSummary(metadata) ?? "",
|
|
497
|
-
derivationType: args.derivationType ?? "",
|
|
498
|
-
createdBy: args.createdBy,
|
|
499
|
-
createdAt: now,
|
|
500
|
-
updatedAt: now,
|
|
501
|
-
topicId: args.topicId ?? readStringProperty(metadata, "topicId") ?? "",
|
|
502
|
-
tenantId: args.tenantId ?? readStringProperty(metadata, "tenantId") ?? "",
|
|
503
|
-
workspaceId: args.workspaceId ?? readStringProperty(metadata, "workspaceId") ?? "",
|
|
504
|
-
fromLayer: args.fromLayer ?? "",
|
|
505
|
-
toLayer: args.toLayer ?? "",
|
|
506
|
-
fromNodeType: args.fromNodeType ?? "",
|
|
507
|
-
toNodeType: args.toNodeType ?? "",
|
|
508
|
-
reasoningMethod: args.reasoningMethod ?? "",
|
|
509
|
-
logicalRole: args.logicalRole ?? "",
|
|
510
|
-
temporalClass: args.temporalClass ?? "structural",
|
|
511
|
-
validFrom: args.validFrom ?? now,
|
|
512
|
-
validUntil: args.validUntil ?? null
|
|
513
|
-
};
|
|
578
|
+
const metadata = readMetadata(args.metadata);
|
|
579
|
+
const properties = neo4jEdgeProperties(args, edgeType, metadata, now);
|
|
514
580
|
const result = await runWriteTransaction(
|
|
515
581
|
`
|
|
516
582
|
MATCH (from {globalId: $fromGlobalId})
|
|
@@ -527,76 +593,9 @@ var createEdge = internalAction({
|
|
|
527
593
|
}
|
|
528
594
|
);
|
|
529
595
|
if (result.length === 0) {
|
|
530
|
-
|
|
531
|
-
globalId: args.globalId,
|
|
532
|
-
operation: "upsert",
|
|
533
|
-
error: `Source or target node not yet synced to Neo4j (from: ${args.fromGlobalId}, to: ${args.toGlobalId})`
|
|
534
|
-
});
|
|
535
|
-
if (needsDualWrite(edgeType)) {
|
|
536
|
-
try {
|
|
537
|
-
await mirrorEdgeToConvex(ctx, {
|
|
538
|
-
globalId: args.globalId,
|
|
539
|
-
fromGlobalId: args.fromGlobalId,
|
|
540
|
-
toGlobalId: args.toGlobalId,
|
|
541
|
-
edgeType,
|
|
542
|
-
weight: args.weight,
|
|
543
|
-
confidence: args.confidence,
|
|
544
|
-
context: args.context,
|
|
545
|
-
derivationType: args.derivationType,
|
|
546
|
-
createdBy: args.createdBy,
|
|
547
|
-
topicId: args.topicId,
|
|
548
|
-
fromLayer: args.fromLayer,
|
|
549
|
-
toLayer: args.toLayer,
|
|
550
|
-
fromNodeType: args.fromNodeType,
|
|
551
|
-
toNodeType: args.toNodeType,
|
|
552
|
-
reasoningMethod: args.reasoningMethod,
|
|
553
|
-
logicalRole: args.logicalRole,
|
|
554
|
-
temporalClass: args.temporalClass,
|
|
555
|
-
validFrom: args.validFrom,
|
|
556
|
-
validUntil: args.validUntil
|
|
557
|
-
});
|
|
558
|
-
} catch {
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
return {
|
|
562
|
-
success: false,
|
|
563
|
-
globalId: args.globalId,
|
|
564
|
-
edgeType,
|
|
565
|
-
queuedForRetry: true,
|
|
566
|
-
reason: "nodes_not_synced"
|
|
567
|
-
};
|
|
568
|
-
}
|
|
569
|
-
if (needsDualWrite(edgeType)) {
|
|
570
|
-
try {
|
|
571
|
-
await mirrorEdgeToConvex(ctx, {
|
|
572
|
-
globalId: args.globalId,
|
|
573
|
-
fromGlobalId: args.fromGlobalId,
|
|
574
|
-
toGlobalId: args.toGlobalId,
|
|
575
|
-
edgeType,
|
|
576
|
-
weight: args.weight,
|
|
577
|
-
confidence: args.confidence,
|
|
578
|
-
context: args.context,
|
|
579
|
-
derivationType: args.derivationType,
|
|
580
|
-
createdBy: args.createdBy,
|
|
581
|
-
topicId: args.topicId,
|
|
582
|
-
fromLayer: args.fromLayer,
|
|
583
|
-
toLayer: args.toLayer,
|
|
584
|
-
fromNodeType: args.fromNodeType,
|
|
585
|
-
toNodeType: args.toNodeType,
|
|
586
|
-
reasoningMethod: args.reasoningMethod,
|
|
587
|
-
logicalRole: args.logicalRole,
|
|
588
|
-
temporalClass: args.temporalClass,
|
|
589
|
-
validFrom: args.validFrom,
|
|
590
|
-
validUntil: args.validUntil
|
|
591
|
-
});
|
|
592
|
-
} catch (error) {
|
|
593
|
-
await queueEdgeRetry(ctx, {
|
|
594
|
-
globalId: args.globalId,
|
|
595
|
-
operation: "upsert",
|
|
596
|
-
error: `Convex mirror failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
597
|
-
});
|
|
598
|
-
}
|
|
596
|
+
return handleMissingNeo4jEndpoints(ctx, args, edgeType);
|
|
599
597
|
}
|
|
598
|
+
await mirrorCreatedEdgeIfNeeded(ctx, args, edgeType);
|
|
600
599
|
return {
|
|
601
600
|
success: true,
|
|
602
601
|
globalId: args.globalId,
|
|
@@ -646,9 +645,15 @@ var updateEdge = internalAction({
|
|
|
646
645
|
throw new Error("[Neo4j Edge API] Neo4j not configured");
|
|
647
646
|
}
|
|
648
647
|
const updates = { updatedAt: Date.now() };
|
|
649
|
-
if (args.weight !== void 0)
|
|
650
|
-
|
|
651
|
-
|
|
648
|
+
if (args.weight !== void 0) {
|
|
649
|
+
updates.weight = args.weight;
|
|
650
|
+
}
|
|
651
|
+
if (args.confidence !== void 0) {
|
|
652
|
+
updates.confidence = args.confidence;
|
|
653
|
+
}
|
|
654
|
+
if (args.context !== void 0) {
|
|
655
|
+
updates.context = args.context;
|
|
656
|
+
}
|
|
652
657
|
if (args.derivationType !== void 0) {
|
|
653
658
|
updates.derivationType = args.derivationType;
|
|
654
659
|
}
|
|
@@ -736,7 +741,13 @@ var retryProjectionByGlobalId = internalAction({
|
|
|
736
741
|
error: `[Neo4j Edge API] Edge not found in Neo4j: ${args.globalId}`
|
|
737
742
|
};
|
|
738
743
|
}
|
|
739
|
-
const edge = result
|
|
744
|
+
const [edge] = result;
|
|
745
|
+
if (!edge) {
|
|
746
|
+
return {
|
|
747
|
+
success: false,
|
|
748
|
+
error: `[Neo4j Edge API] Edge not found in Neo4j: ${args.globalId}`
|
|
749
|
+
};
|
|
750
|
+
}
|
|
740
751
|
if (!needsDualWrite(edge.edgeType)) {
|
|
741
752
|
return {
|
|
742
753
|
success: true,
|
|
@@ -830,6 +841,8 @@ __export(neo4jQueries_exports, {
|
|
|
830
841
|
});
|
|
831
842
|
|
|
832
843
|
// src/neo4jQueriesCore.ts
|
|
844
|
+
var TRAILING_SLASH_REGEX = /\/+$/u;
|
|
845
|
+
var NEO4J_QUERY_TIMEOUT_MS = 1e4;
|
|
833
846
|
function toInt(value, defaultValue) {
|
|
834
847
|
if (value === void 0 || value === null) {
|
|
835
848
|
return defaultValue;
|
|
@@ -869,15 +882,69 @@ function createNeo4jQueryTransportFailure(error) {
|
|
|
869
882
|
function resolveProxyBaseUrl(apiBaseUrl) {
|
|
870
883
|
const resolved = apiBaseUrl || process.env.LUCERN_GRAPH_SYNC_QUERY_BASE_URL || process.env.NEXT_PUBLIC_APP_URL;
|
|
871
884
|
const normalized = resolved?.trim();
|
|
872
|
-
return normalized ? normalized.replace(
|
|
885
|
+
return normalized ? normalized.replace(TRAILING_SLASH_REGEX, "") : null;
|
|
873
886
|
}
|
|
874
887
|
function buildProxyEndpoint(proxyBaseUrl) {
|
|
875
888
|
const endpoint = new URL(proxyBaseUrl);
|
|
876
889
|
if (!endpoint.pathname.endsWith("/api/neo4j-query")) {
|
|
877
|
-
endpoint.pathname = `${endpoint.pathname.replace(
|
|
890
|
+
endpoint.pathname = `${endpoint.pathname.replace(
|
|
891
|
+
TRAILING_SLASH_REGEX,
|
|
892
|
+
""
|
|
893
|
+
)}/api/neo4j-query`;
|
|
878
894
|
}
|
|
879
895
|
return endpoint.toString();
|
|
880
896
|
}
|
|
897
|
+
function tenantContextFromParams(params) {
|
|
898
|
+
const explicitTenant = typeof params.tenantId === "string" ? params.tenantId.trim() : "";
|
|
899
|
+
const scopedTopic = typeof params.topicId === "string" ? params.topicId.trim() : "";
|
|
900
|
+
const legacyProjectScope = typeof params.projectId === "string" ? params.projectId.trim() : "";
|
|
901
|
+
const scopedTopicId = scopedTopic || legacyProjectScope;
|
|
902
|
+
const projectTenant = scopedTopicId ? `project:${scopedTopicId}` : "";
|
|
903
|
+
const defaultTenant = process.env.LUCERN_DEFAULT_TENANT_ID?.trim() || "";
|
|
904
|
+
return explicitTenant || projectTenant || defaultTenant;
|
|
905
|
+
}
|
|
906
|
+
function neo4jQueryHeaders(syncSecret, apiBaseUrl) {
|
|
907
|
+
const headers = {
|
|
908
|
+
"Content-Type": "application/json",
|
|
909
|
+
Authorization: `Bearer ${syncSecret}`
|
|
910
|
+
};
|
|
911
|
+
const bypassSecret = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
|
|
912
|
+
if (bypassSecret && apiBaseUrl) {
|
|
913
|
+
headers["x-vercel-protection-bypass"] = bypassSecret;
|
|
914
|
+
}
|
|
915
|
+
return headers;
|
|
916
|
+
}
|
|
917
|
+
async function postNeo4jQuery(args) {
|
|
918
|
+
const controller = new AbortController();
|
|
919
|
+
const timeoutId = setTimeout(
|
|
920
|
+
() => controller.abort(),
|
|
921
|
+
NEO4J_QUERY_TIMEOUT_MS
|
|
922
|
+
);
|
|
923
|
+
try {
|
|
924
|
+
const response = await fetch(buildProxyEndpoint(args.proxyUrl), {
|
|
925
|
+
method: "POST",
|
|
926
|
+
headers: neo4jQueryHeaders(args.syncSecret, args.apiBaseUrl),
|
|
927
|
+
body: JSON.stringify({
|
|
928
|
+
queryName: args.queryName,
|
|
929
|
+
params: args.params
|
|
930
|
+
}),
|
|
931
|
+
signal: controller.signal
|
|
932
|
+
});
|
|
933
|
+
const result = await response.json();
|
|
934
|
+
if (!response.ok) {
|
|
935
|
+
return createNeo4jQueryTransportFailure(result.error || "Query failed");
|
|
936
|
+
}
|
|
937
|
+
return createNeo4jQueryTransportSuccess(result.data || []);
|
|
938
|
+
} catch (fetchError) {
|
|
939
|
+
if (fetchError instanceof Error && fetchError.name === "AbortError") {
|
|
940
|
+
console.warn("[Neo4j Queries] Request timed out for:", args.queryName);
|
|
941
|
+
return createNeo4jQueryTransportFailure("Query timed out (10s)");
|
|
942
|
+
}
|
|
943
|
+
throw fetchError;
|
|
944
|
+
} finally {
|
|
945
|
+
clearTimeout(timeoutId);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
881
948
|
function withTopicScope(args, params) {
|
|
882
949
|
return args.topicId ? { ...params, topicId: args.topicId } : params;
|
|
883
950
|
}
|
|
@@ -898,55 +965,23 @@ async function callNeo4jQuery(queryName, params, apiBaseUrl) {
|
|
|
898
965
|
const syncSecret = process.env.NEO4J_SYNC_SECRET;
|
|
899
966
|
if (!syncSecret) {
|
|
900
967
|
console.error("[Neo4j Queries] NEO4J_SYNC_SECRET not configured");
|
|
901
|
-
return createNeo4jQueryTransportFailure(
|
|
902
|
-
"Neo4j sync secret not configured"
|
|
903
|
-
);
|
|
968
|
+
return createNeo4jQueryTransportFailure("Neo4j sync secret not configured");
|
|
904
969
|
}
|
|
905
970
|
try {
|
|
906
|
-
const
|
|
907
|
-
const scopedTopic = typeof params.topicId === "string" ? params.topicId.trim() : "";
|
|
908
|
-
const legacyProject = typeof params.projectId === "string" ? params.projectId.trim() : "";
|
|
909
|
-
const scopedTopicId = scopedTopic || legacyProject;
|
|
910
|
-
const projectTenant = scopedTopicId ? `project:${scopedTopicId}` : "";
|
|
911
|
-
const defaultTenant = process.env.LUCERN_DEFAULT_TENANT_ID?.trim() || "";
|
|
912
|
-
const tenantId = explicitTenant || projectTenant || defaultTenant;
|
|
971
|
+
const tenantId = tenantContextFromParams(params);
|
|
913
972
|
if (!tenantId) {
|
|
914
973
|
return createNeo4jQueryTransportFailure(
|
|
915
974
|
"Missing required tenant context (tenantId or topicId)"
|
|
916
975
|
);
|
|
917
976
|
}
|
|
918
977
|
const scopedParams = { ...params, tenantId };
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
}
|
|
927
|
-
const controller = new AbortController();
|
|
928
|
-
const timeoutId = setTimeout(() => controller.abort(), 1e4);
|
|
929
|
-
try {
|
|
930
|
-
const response = await fetch(buildProxyEndpoint(proxyUrl), {
|
|
931
|
-
method: "POST",
|
|
932
|
-
headers,
|
|
933
|
-
body: JSON.stringify({ queryName, params: scopedParams }),
|
|
934
|
-
signal: controller.signal
|
|
935
|
-
});
|
|
936
|
-
clearTimeout(timeoutId);
|
|
937
|
-
const result = await response.json();
|
|
938
|
-
if (!response.ok) {
|
|
939
|
-
return createNeo4jQueryTransportFailure(result.error || "Query failed");
|
|
940
|
-
}
|
|
941
|
-
return createNeo4jQueryTransportSuccess(result.data || []);
|
|
942
|
-
} catch (fetchError) {
|
|
943
|
-
clearTimeout(timeoutId);
|
|
944
|
-
if (fetchError instanceof Error && fetchError.name === "AbortError") {
|
|
945
|
-
console.warn("[Neo4j Queries] Request timed out for:", queryName);
|
|
946
|
-
return createNeo4jQueryTransportFailure("Query timed out (10s)");
|
|
947
|
-
}
|
|
948
|
-
throw fetchError;
|
|
949
|
-
}
|
|
978
|
+
return await postNeo4jQuery({
|
|
979
|
+
apiBaseUrl,
|
|
980
|
+
params: scopedParams,
|
|
981
|
+
proxyUrl,
|
|
982
|
+
queryName,
|
|
983
|
+
syncSecret
|
|
984
|
+
});
|
|
950
985
|
} catch (error) {
|
|
951
986
|
console.error("[Neo4j Queries] Error in callNeo4jQuery:", queryName, error);
|
|
952
987
|
return createNeo4jQueryTransportFailure(
|
|
@@ -2154,6 +2189,7 @@ var neo4jQueryRoute_exports = {};
|
|
|
2154
2189
|
__export(neo4jQueryRoute_exports, {
|
|
2155
2190
|
createNeo4jQueryRouteHandler: () => createNeo4jQueryRouteHandler
|
|
2156
2191
|
});
|
|
2192
|
+
var BEARER_AUTHORIZATION_PATTERN = /^Bearer\s+(.+)$/iu;
|
|
2157
2193
|
function jsonResponse(body, init) {
|
|
2158
2194
|
return new Response(JSON.stringify(body), {
|
|
2159
2195
|
...init,
|
|
@@ -2165,9 +2201,38 @@ function jsonResponse(body, init) {
|
|
|
2165
2201
|
}
|
|
2166
2202
|
function readBearerSecret(request) {
|
|
2167
2203
|
const authorization = request.headers.get("authorization") ?? "";
|
|
2168
|
-
const match = authorization.match(
|
|
2204
|
+
const match = authorization.match(BEARER_AUTHORIZATION_PATTERN);
|
|
2169
2205
|
return match?.[1]?.trim() || null;
|
|
2170
2206
|
}
|
|
2207
|
+
function validateRouteSecret(request, options) {
|
|
2208
|
+
const expectedSecret = options.syncSecret ?? process.env.NEO4J_SYNC_SECRET?.trim();
|
|
2209
|
+
if (!expectedSecret) {
|
|
2210
|
+
return jsonResponse(
|
|
2211
|
+
{ error: "Neo4j sync secret not configured" },
|
|
2212
|
+
{ status: 500 }
|
|
2213
|
+
);
|
|
2214
|
+
}
|
|
2215
|
+
if (readBearerSecret(request) !== expectedSecret) {
|
|
2216
|
+
return jsonResponse({ error: "Unauthorized" }, { status: 401 });
|
|
2217
|
+
}
|
|
2218
|
+
return null;
|
|
2219
|
+
}
|
|
2220
|
+
async function readQueryBody(request) {
|
|
2221
|
+
try {
|
|
2222
|
+
return {
|
|
2223
|
+
ok: true,
|
|
2224
|
+
body: await request.json()
|
|
2225
|
+
};
|
|
2226
|
+
} catch {
|
|
2227
|
+
return {
|
|
2228
|
+
ok: false,
|
|
2229
|
+
response: jsonResponse({ error: "Invalid JSON body" }, { status: 400 })
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
function normalizeQueryParams(params) {
|
|
2234
|
+
return params && typeof params === "object" && !Array.isArray(params) ? params : {};
|
|
2235
|
+
}
|
|
2171
2236
|
function hasTenantContext(params) {
|
|
2172
2237
|
const tenantId = params.tenantId;
|
|
2173
2238
|
const topicId = params.topicId;
|
|
@@ -2184,62 +2249,79 @@ function normalizeConnectedNodesQuery(queryName, params, queries) {
|
|
|
2184
2249
|
const aliased = `connectedNodes${hops}`;
|
|
2185
2250
|
return queries[aliased] ? aliased : queryName;
|
|
2186
2251
|
}
|
|
2252
|
+
function requireTenantContext(options, params) {
|
|
2253
|
+
if ((options.requireTenantContext ?? true) && !hasTenantContext(params)) {
|
|
2254
|
+
return jsonResponse(
|
|
2255
|
+
{ error: "Missing required tenant context" },
|
|
2256
|
+
{ status: 400 }
|
|
2257
|
+
);
|
|
2258
|
+
}
|
|
2259
|
+
return null;
|
|
2260
|
+
}
|
|
2261
|
+
function resolveNamedQuery(queryName, params, queries) {
|
|
2262
|
+
const normalizedQueryName = normalizeConnectedNodesQuery(
|
|
2263
|
+
queryName,
|
|
2264
|
+
params,
|
|
2265
|
+
queries
|
|
2266
|
+
);
|
|
2267
|
+
const query = queries[normalizedQueryName];
|
|
2268
|
+
if (!query) {
|
|
2269
|
+
return {
|
|
2270
|
+
ok: false,
|
|
2271
|
+
response: jsonResponse(
|
|
2272
|
+
{ error: `Unknown query: ${queryName}` },
|
|
2273
|
+
{ status: 400 }
|
|
2274
|
+
)
|
|
2275
|
+
};
|
|
2276
|
+
}
|
|
2277
|
+
return { ok: true, query, queryName: normalizedQueryName };
|
|
2278
|
+
}
|
|
2279
|
+
async function executeNamedQuery(queryName, query, params) {
|
|
2280
|
+
try {
|
|
2281
|
+
const data = await runCypher(
|
|
2282
|
+
query.cypher,
|
|
2283
|
+
params,
|
|
2284
|
+
query.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS
|
|
2285
|
+
);
|
|
2286
|
+
return jsonResponse({ data, queryName });
|
|
2287
|
+
} catch (error) {
|
|
2288
|
+
return jsonResponse(
|
|
2289
|
+
{
|
|
2290
|
+
error: error instanceof Error ? error.message : "Neo4j query failed",
|
|
2291
|
+
queryName
|
|
2292
|
+
},
|
|
2293
|
+
{ status: 500 }
|
|
2294
|
+
);
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2187
2297
|
function createNeo4jQueryRouteHandler(options) {
|
|
2188
2298
|
return async function handleNeo4jQuery(request) {
|
|
2189
|
-
const
|
|
2190
|
-
if (
|
|
2191
|
-
return
|
|
2192
|
-
{ error: "Neo4j sync secret not configured" },
|
|
2193
|
-
{ status: 500 }
|
|
2194
|
-
);
|
|
2195
|
-
}
|
|
2196
|
-
if (readBearerSecret(request) !== expectedSecret) {
|
|
2197
|
-
return jsonResponse({ error: "Unauthorized" }, { status: 401 });
|
|
2299
|
+
const authError = validateRouteSecret(request, options);
|
|
2300
|
+
if (authError) {
|
|
2301
|
+
return authError;
|
|
2198
2302
|
}
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
} catch {
|
|
2203
|
-
return jsonResponse({ error: "Invalid JSON body" }, { status: 400 });
|
|
2303
|
+
const bodyResult = await readQueryBody(request);
|
|
2304
|
+
if (!bodyResult.ok) {
|
|
2305
|
+
return bodyResult.response;
|
|
2204
2306
|
}
|
|
2307
|
+
const { body } = bodyResult;
|
|
2205
2308
|
if (typeof body.queryName !== "string" || body.queryName.length === 0) {
|
|
2206
2309
|
return jsonResponse({ error: "Missing queryName" }, { status: 400 });
|
|
2207
2310
|
}
|
|
2208
|
-
const params =
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
{ status: 400 }
|
|
2213
|
-
);
|
|
2311
|
+
const params = normalizeQueryParams(body.params);
|
|
2312
|
+
const tenantContextError = requireTenantContext(options, params);
|
|
2313
|
+
if (tenantContextError) {
|
|
2314
|
+
return tenantContextError;
|
|
2214
2315
|
}
|
|
2215
|
-
const
|
|
2316
|
+
const namedQuery = resolveNamedQuery(
|
|
2216
2317
|
body.queryName,
|
|
2217
2318
|
params,
|
|
2218
2319
|
options.queries
|
|
2219
2320
|
);
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
return jsonResponse(
|
|
2223
|
-
{ error: `Unknown query: ${body.queryName}` },
|
|
2224
|
-
{ status: 400 }
|
|
2225
|
-
);
|
|
2226
|
-
}
|
|
2227
|
-
try {
|
|
2228
|
-
const data = await runCypher(
|
|
2229
|
-
query.cypher,
|
|
2230
|
-
params,
|
|
2231
|
-
query.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS
|
|
2232
|
-
);
|
|
2233
|
-
return jsonResponse({ data, queryName });
|
|
2234
|
-
} catch (error) {
|
|
2235
|
-
return jsonResponse(
|
|
2236
|
-
{
|
|
2237
|
-
error: error instanceof Error ? error.message : "Neo4j query failed",
|
|
2238
|
-
queryName
|
|
2239
|
-
},
|
|
2240
|
-
{ status: 500 }
|
|
2241
|
-
);
|
|
2321
|
+
if (!namedQuery.ok) {
|
|
2322
|
+
return namedQuery.response;
|
|
2242
2323
|
}
|
|
2324
|
+
return executeNamedQuery(namedQuery.queryName, namedQuery.query, params);
|
|
2243
2325
|
};
|
|
2244
2326
|
}
|
|
2245
2327
|
|
|
@@ -2257,6 +2339,9 @@ __export(neo4jSync_exports, {
|
|
|
2257
2339
|
syncEmbeddingToNeo4j: () => syncEmbeddingToNeo4j,
|
|
2258
2340
|
syncNodeToNeo4j: () => syncNodeToNeo4j
|
|
2259
2341
|
});
|
|
2342
|
+
var graphSyncHelpers = internal.neo4jSyncHelpers;
|
|
2343
|
+
var graphSyncActions = internal.neo4jSync;
|
|
2344
|
+
var graphSyncEdgeApi = internal.neo4jEdgeAPI;
|
|
2260
2345
|
function buildSyncResponse(entityType, operation, fields) {
|
|
2261
2346
|
return {
|
|
2262
2347
|
entityType,
|
|
@@ -2277,53 +2362,100 @@ function buildSyncFailure(entityType, operation, error, fields) {
|
|
|
2277
2362
|
...fields
|
|
2278
2363
|
});
|
|
2279
2364
|
}
|
|
2365
|
+
function readRecord(value) {
|
|
2366
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2367
|
+
}
|
|
2368
|
+
function readString(value) {
|
|
2369
|
+
return typeof value === "string" ? value : "";
|
|
2370
|
+
}
|
|
2371
|
+
function readFirstString(...values) {
|
|
2372
|
+
for (const value of values) {
|
|
2373
|
+
const stringValue = readString(value);
|
|
2374
|
+
if (stringValue.length > 0) {
|
|
2375
|
+
return stringValue;
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
return "";
|
|
2379
|
+
}
|
|
2380
|
+
function readArrayLength(value) {
|
|
2381
|
+
return Array.isArray(value) ? value.length : 0;
|
|
2382
|
+
}
|
|
2383
|
+
function nodeSyncEventType(operation) {
|
|
2384
|
+
return operation === "delete" ? "node_deleted" : "node_updated";
|
|
2385
|
+
}
|
|
2386
|
+
function edgeSyncEventType(operation) {
|
|
2387
|
+
return operation === "delete" ? "edge_deleted" : "edge_created";
|
|
2388
|
+
}
|
|
2389
|
+
function readNeo4jEndpointFailure(edge, operation) {
|
|
2390
|
+
if (edge.fromGlobalId && edge.toGlobalId) {
|
|
2391
|
+
return null;
|
|
2392
|
+
}
|
|
2393
|
+
console.warn(
|
|
2394
|
+
"[Neo4j Sync] Edge missing fromGlobalId or toGlobalId, skipping"
|
|
2395
|
+
);
|
|
2396
|
+
return buildSyncFailure(
|
|
2397
|
+
"edge",
|
|
2398
|
+
operation,
|
|
2399
|
+
"Edge missing connected node globalIds",
|
|
2400
|
+
{
|
|
2401
|
+
skippedReason: "edge_endpoint_missing"
|
|
2402
|
+
}
|
|
2403
|
+
);
|
|
2404
|
+
}
|
|
2405
|
+
async function applyEdgeOperation(edge, operation, edgeId) {
|
|
2406
|
+
const relType = EDGE_TYPE_TO_REL[edge.edgeType] || edge.edgeType.toUpperCase();
|
|
2407
|
+
if (operation === "delete") {
|
|
2408
|
+
await deleteEdge(edge.globalId);
|
|
2409
|
+
console.log(`[Neo4j Sync] Deleted edge ${edge.globalId}`);
|
|
2410
|
+
return relType;
|
|
2411
|
+
}
|
|
2412
|
+
await upsertEdge(
|
|
2413
|
+
relType,
|
|
2414
|
+
edge.globalId,
|
|
2415
|
+
edge.fromGlobalId ?? "",
|
|
2416
|
+
edge.toGlobalId ?? "",
|
|
2417
|
+
{
|
|
2418
|
+
...buildEdgeProperties(edge),
|
|
2419
|
+
convexId: edgeId
|
|
2420
|
+
}
|
|
2421
|
+
);
|
|
2422
|
+
console.log(`[Neo4j Sync] Upserted edge ${edge.globalId} as ${relType}`);
|
|
2423
|
+
return relType;
|
|
2424
|
+
}
|
|
2280
2425
|
function buildNodeProperties(node) {
|
|
2281
|
-
const metadata = node.metadata
|
|
2282
|
-
const
|
|
2283
|
-
const stage = metadata.stage || metadata.beliefStage || "";
|
|
2284
|
-
const criticality = metadata.criticality || "";
|
|
2285
|
-
const synthesizedFrom = metadata.synthesizedFrom || [];
|
|
2286
|
-
const epistemicStatus = node.epistemicStatus || "";
|
|
2287
|
-
const methodology = node.methodology || "";
|
|
2288
|
-
const informationAsymmetry = node.informationAsymmetry || "";
|
|
2289
|
-
const questionType = node.questionType || "";
|
|
2290
|
-
const questionPriority = node.questionPriority || "";
|
|
2291
|
-
const answerQuality = node.answerQuality || "";
|
|
2292
|
-
const reversibility = node.reversibility || "";
|
|
2293
|
-
const predictionMeta = node.predictionMeta;
|
|
2426
|
+
const metadata = readRecord(node.metadata);
|
|
2427
|
+
const predictionMeta = readRecord(node.predictionMeta);
|
|
2294
2428
|
return {
|
|
2295
|
-
|
|
2429
|
+
answerQuality: readString(node.answerQuality),
|
|
2296
2430
|
canonicalText: node.canonicalText || "",
|
|
2297
|
-
title: node.title || "",
|
|
2298
|
-
status: node.status || "active",
|
|
2299
|
-
subtype: node.subtype || "",
|
|
2300
|
-
domain: node.domain || "",
|
|
2301
2431
|
confidence: node.confidence || 0,
|
|
2302
|
-
|
|
2303
|
-
sourceType: node.sourceType || "unknown",
|
|
2432
|
+
convexId: node._id,
|
|
2304
2433
|
createdAt: node.createdAt,
|
|
2305
|
-
updatedAt: node.updatedAt || Date.now(),
|
|
2306
2434
|
createdBy: node.createdBy || "",
|
|
2307
|
-
|
|
2435
|
+
criticality: readString(metadata.criticality),
|
|
2436
|
+
domain: node.domain || "",
|
|
2308
2437
|
epistemicLayer: node.epistemicLayer || "",
|
|
2309
|
-
|
|
2438
|
+
epistemicStatus: readString(node.epistemicStatus),
|
|
2439
|
+
expectedBy: predictionMeta?.expectedBy ? String(predictionMeta.expectedBy) : "",
|
|
2440
|
+
informationAsymmetry: readString(node.informationAsymmetry),
|
|
2441
|
+
isPrediction: predictionMeta?.isPrediction ? "true" : "false",
|
|
2442
|
+
methodology: readString(node.methodology),
|
|
2443
|
+
nodeType: node.nodeType,
|
|
2444
|
+
pillar: readFirstString(metadata.pillar, metadata.topic),
|
|
2310
2445
|
projectId: node.projectId || "",
|
|
2446
|
+
questionPriority: readString(node.questionPriority),
|
|
2447
|
+
questionType: readString(node.questionType),
|
|
2448
|
+
reversibility: readString(node.reversibility),
|
|
2449
|
+
sourceType: node.sourceType || "unknown",
|
|
2450
|
+
stage: readFirstString(metadata.stage, metadata.beliefStage),
|
|
2451
|
+
status: node.status || "active",
|
|
2452
|
+
subtype: node.subtype || "",
|
|
2453
|
+
synthesizedFromCount: readArrayLength(metadata.synthesizedFrom),
|
|
2311
2454
|
tenantId: node.tenantId || "",
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
synthesizedFromCount: synthesizedFrom.length,
|
|
2317
|
-
// Classification fields (Logic Machine)
|
|
2318
|
-
epistemicStatus,
|
|
2319
|
-
methodology,
|
|
2320
|
-
informationAsymmetry,
|
|
2321
|
-
questionType,
|
|
2322
|
-
questionPriority,
|
|
2323
|
-
answerQuality,
|
|
2324
|
-
reversibility,
|
|
2325
|
-
isPrediction: predictionMeta?.isPrediction ? "true" : "false",
|
|
2326
|
-
expectedBy: predictionMeta?.expectedBy ? String(predictionMeta.expectedBy) : ""
|
|
2455
|
+
title: node.title || "",
|
|
2456
|
+
updatedAt: node.updatedAt || Date.now(),
|
|
2457
|
+
verificationStatus: node.verificationStatus || "unverified",
|
|
2458
|
+
workspaceId: node.workspaceId || ""
|
|
2327
2459
|
};
|
|
2328
2460
|
}
|
|
2329
2461
|
function buildEdgeProperties(edge) {
|
|
@@ -2363,7 +2495,7 @@ var syncNodeToNeo4j = internalAction({
|
|
|
2363
2495
|
skippedReason: "credentials_missing"
|
|
2364
2496
|
});
|
|
2365
2497
|
}
|
|
2366
|
-
const node = await ctx.runQuery(
|
|
2498
|
+
const node = await ctx.runQuery(graphSyncHelpers.getNodeForSync, {
|
|
2367
2499
|
nodeId: args.nodeId
|
|
2368
2500
|
});
|
|
2369
2501
|
if (!node) {
|
|
@@ -2386,7 +2518,7 @@ var syncNodeToNeo4j = internalAction({
|
|
|
2386
2518
|
} else {
|
|
2387
2519
|
const props = buildNodeProperties(node);
|
|
2388
2520
|
const embedding = await ctx.runQuery(
|
|
2389
|
-
|
|
2521
|
+
graphSyncHelpers.getEmbeddingForSync,
|
|
2390
2522
|
{ nodeId: args.nodeId }
|
|
2391
2523
|
);
|
|
2392
2524
|
if (embedding) {
|
|
@@ -2397,8 +2529,8 @@ var syncNodeToNeo4j = internalAction({
|
|
|
2397
2529
|
`[Neo4j Sync] Upserted node ${node.globalId} as ${label} with projectId=${node.projectId}` + (embedding ? ` (with ${embedding.length}-dim embedding)` : "")
|
|
2398
2530
|
);
|
|
2399
2531
|
}
|
|
2400
|
-
await ctx.runMutation(
|
|
2401
|
-
eventType: args.operation
|
|
2532
|
+
await ctx.runMutation(graphSyncHelpers.logSyncEvent, {
|
|
2533
|
+
eventType: nodeSyncEventType(args.operation),
|
|
2402
2534
|
entityId: args.nodeId,
|
|
2403
2535
|
entityType: node.nodeType,
|
|
2404
2536
|
status: "success"
|
|
@@ -2407,14 +2539,14 @@ var syncNodeToNeo4j = internalAction({
|
|
|
2407
2539
|
} catch (error) {
|
|
2408
2540
|
const errorMsg = error instanceof Error ? error.message : "Unknown error";
|
|
2409
2541
|
console.error("[Neo4j Sync] Node sync error:", errorMsg);
|
|
2410
|
-
await ctx.runMutation(
|
|
2542
|
+
await ctx.runMutation(graphSyncHelpers.logSyncEvent, {
|
|
2411
2543
|
eventType: args.operation === "delete" ? "node_deleted" : "node_updated",
|
|
2412
2544
|
entityId: args.nodeId,
|
|
2413
2545
|
entityType: node.nodeType,
|
|
2414
2546
|
status: "failed",
|
|
2415
2547
|
error: errorMsg
|
|
2416
2548
|
});
|
|
2417
|
-
await ctx.runMutation(
|
|
2549
|
+
await ctx.runMutation(graphSyncHelpers.queueForRetry, {
|
|
2418
2550
|
entityType: "node",
|
|
2419
2551
|
entityId: args.nodeId,
|
|
2420
2552
|
operation: args.operation,
|
|
@@ -2440,7 +2572,7 @@ var syncEdgeToNeo4j = internalAction({
|
|
|
2440
2572
|
skippedReason: "credentials_missing"
|
|
2441
2573
|
});
|
|
2442
2574
|
}
|
|
2443
|
-
const edge = await ctx.runQuery(
|
|
2575
|
+
const edge = await ctx.runQuery(graphSyncHelpers.getEdgeForSync, {
|
|
2444
2576
|
edgeId: args.edgeId
|
|
2445
2577
|
});
|
|
2446
2578
|
if (!edge) {
|
|
@@ -2455,53 +2587,18 @@ var syncEdgeToNeo4j = internalAction({
|
|
|
2455
2587
|
skippedReason: "source_edge_missing"
|
|
2456
2588
|
});
|
|
2457
2589
|
}
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
"Edge missing connected node globalIds",
|
|
2466
|
-
{
|
|
2467
|
-
skippedReason: "edge_endpoint_missing"
|
|
2468
|
-
}
|
|
2469
|
-
);
|
|
2590
|
+
const edgeForSync = edge;
|
|
2591
|
+
const endpointFailure = readNeo4jEndpointFailure(
|
|
2592
|
+
edgeForSync,
|
|
2593
|
+
args.operation
|
|
2594
|
+
);
|
|
2595
|
+
if (endpointFailure) {
|
|
2596
|
+
return endpointFailure;
|
|
2470
2597
|
}
|
|
2471
|
-
const relType = EDGE_TYPE_TO_REL[edge.edgeType] || edge.edgeType.toUpperCase();
|
|
2472
2598
|
try {
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
} else {
|
|
2477
|
-
await upsertEdge(
|
|
2478
|
-
relType,
|
|
2479
|
-
edge.globalId,
|
|
2480
|
-
edge.fromGlobalId,
|
|
2481
|
-
edge.toGlobalId,
|
|
2482
|
-
{
|
|
2483
|
-
convexId: args.edgeId,
|
|
2484
|
-
weight: edge.weight || 1,
|
|
2485
|
-
confidence: edge.confidence || 0,
|
|
2486
|
-
context: edge.context || "",
|
|
2487
|
-
derivationType: edge.derivationType || "",
|
|
2488
|
-
createdAt: edge.createdAt,
|
|
2489
|
-
createdBy: edge.createdBy || "",
|
|
2490
|
-
edgeType: edge.edgeType,
|
|
2491
|
-
fromLayer: edge.fromLayer || "",
|
|
2492
|
-
toLayer: edge.toLayer || "",
|
|
2493
|
-
// Classification fields (Logic Machine)
|
|
2494
|
-
reasoningMethod: edge.reasoningMethod || "",
|
|
2495
|
-
logicalRole: edge.logicalRole || "",
|
|
2496
|
-
temporalClass: edge.temporalClass || ""
|
|
2497
|
-
}
|
|
2498
|
-
);
|
|
2499
|
-
console.log(
|
|
2500
|
-
`[Neo4j Sync] Upserted edge ${edge.globalId} as ${relType}`
|
|
2501
|
-
);
|
|
2502
|
-
}
|
|
2503
|
-
await ctx.runMutation(internal.neo4jSyncHelpers.logSyncEvent, {
|
|
2504
|
-
eventType: args.operation === "delete" ? "edge_deleted" : "edge_created",
|
|
2599
|
+
await applyEdgeOperation(edgeForSync, args.operation, args.edgeId);
|
|
2600
|
+
await ctx.runMutation(graphSyncHelpers.logSyncEvent, {
|
|
2601
|
+
eventType: edgeSyncEventType(args.operation),
|
|
2505
2602
|
entityId: args.edgeId,
|
|
2506
2603
|
entityType: edge.edgeType,
|
|
2507
2604
|
status: "success"
|
|
@@ -2510,14 +2607,14 @@ var syncEdgeToNeo4j = internalAction({
|
|
|
2510
2607
|
} catch (error) {
|
|
2511
2608
|
const errorMsg = error instanceof Error ? error.message : "Unknown error";
|
|
2512
2609
|
console.error("[Neo4j Sync] Edge sync error:", errorMsg);
|
|
2513
|
-
await ctx.runMutation(
|
|
2514
|
-
eventType: args.operation
|
|
2610
|
+
await ctx.runMutation(graphSyncHelpers.logSyncEvent, {
|
|
2611
|
+
eventType: edgeSyncEventType(args.operation),
|
|
2515
2612
|
entityId: args.edgeId,
|
|
2516
2613
|
entityType: edge.edgeType,
|
|
2517
2614
|
status: "failed",
|
|
2518
2615
|
error: errorMsg
|
|
2519
2616
|
});
|
|
2520
|
-
await ctx.runMutation(
|
|
2617
|
+
await ctx.runMutation(graphSyncHelpers.queueForRetry, {
|
|
2521
2618
|
entityType: "edge",
|
|
2522
2619
|
entityId: args.edgeId,
|
|
2523
2620
|
operation: args.operation,
|
|
@@ -2535,13 +2632,10 @@ var syncAllNodesToNeo4j = internalAction({
|
|
|
2535
2632
|
returns: permissiveReturn,
|
|
2536
2633
|
handler: async (ctx, args) => {
|
|
2537
2634
|
const batchSize = args.batchSize ?? 100;
|
|
2538
|
-
const result = await ctx.runQuery(
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
cursor: args.cursor
|
|
2543
|
-
}
|
|
2544
|
-
);
|
|
2635
|
+
const result = await ctx.runQuery(graphSyncHelpers.getNodeBatchForSync, {
|
|
2636
|
+
limit: batchSize,
|
|
2637
|
+
cursor: args.cursor
|
|
2638
|
+
});
|
|
2545
2639
|
if (result.nodes.length === 0) {
|
|
2546
2640
|
return { synced: 0, failed: 0, hasMore: false };
|
|
2547
2641
|
}
|
|
@@ -2586,19 +2680,16 @@ var syncAllEdgesToNeo4j = internalAction({
|
|
|
2586
2680
|
returns: permissiveReturn,
|
|
2587
2681
|
handler: async (ctx, args) => {
|
|
2588
2682
|
const batchSize = args.batchSize ?? 100;
|
|
2589
|
-
const result = await ctx.runQuery(
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
cursor: args.cursor
|
|
2594
|
-
}
|
|
2595
|
-
);
|
|
2683
|
+
const result = await ctx.runQuery(graphSyncHelpers.getEdgeBatchForSync, {
|
|
2684
|
+
limit: batchSize,
|
|
2685
|
+
cursor: args.cursor
|
|
2686
|
+
});
|
|
2596
2687
|
if (result.edges.length === 0) {
|
|
2597
2688
|
return { synced: 0, failed: 0, hasMore: false };
|
|
2598
2689
|
}
|
|
2599
2690
|
const edgesToSync = [];
|
|
2600
2691
|
for (const edge of result.edges) {
|
|
2601
|
-
if (!edge.fromGlobalId
|
|
2692
|
+
if (!(edge.fromGlobalId && edge.toGlobalId)) {
|
|
2602
2693
|
console.warn(
|
|
2603
2694
|
`[Neo4j Sync] Skipping edge ${edge.globalId} - missing globalIds`
|
|
2604
2695
|
);
|
|
@@ -2644,16 +2735,13 @@ var backfillAllToNeo4j = internalAction({
|
|
|
2644
2735
|
console.log("[Neo4j Sync] Starting full backfill...");
|
|
2645
2736
|
let nodeCursor;
|
|
2646
2737
|
do {
|
|
2647
|
-
const result = await ctx.runAction(
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
cursor: nodeCursor
|
|
2652
|
-
}
|
|
2653
|
-
);
|
|
2738
|
+
const result = await ctx.runAction(graphSyncActions.syncAllNodesToNeo4j, {
|
|
2739
|
+
batchSize,
|
|
2740
|
+
cursor: nodeCursor
|
|
2741
|
+
});
|
|
2654
2742
|
totalNodes += result.synced;
|
|
2655
2743
|
totalFailed += result.failed;
|
|
2656
|
-
nodeCursor = result.hasMore ? result.nextCursor : void 0;
|
|
2744
|
+
nodeCursor = result.hasMore ? result.nextCursor ?? void 0 : void 0;
|
|
2657
2745
|
console.log(
|
|
2658
2746
|
`[Neo4j Sync] Nodes progress: ${totalNodes} synced, ${totalFailed} failed`
|
|
2659
2747
|
);
|
|
@@ -2661,16 +2749,13 @@ var backfillAllToNeo4j = internalAction({
|
|
|
2661
2749
|
console.log(`[Neo4j Sync] Finished nodes: ${totalNodes} synced`);
|
|
2662
2750
|
let edgeCursor;
|
|
2663
2751
|
do {
|
|
2664
|
-
const result = await ctx.runAction(
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
cursor: edgeCursor
|
|
2669
|
-
}
|
|
2670
|
-
);
|
|
2752
|
+
const result = await ctx.runAction(graphSyncActions.syncAllEdgesToNeo4j, {
|
|
2753
|
+
batchSize,
|
|
2754
|
+
cursor: edgeCursor
|
|
2755
|
+
});
|
|
2671
2756
|
totalEdges += result.synced;
|
|
2672
2757
|
totalFailed += result.failed;
|
|
2673
|
-
edgeCursor = result.hasMore ? result.nextCursor : void 0;
|
|
2758
|
+
edgeCursor = result.hasMore ? result.nextCursor ?? void 0 : void 0;
|
|
2674
2759
|
console.log(
|
|
2675
2760
|
`[Neo4j Sync] Edges progress: ${totalEdges} synced, ${totalFailed} failed`
|
|
2676
2761
|
);
|
|
@@ -2693,7 +2778,7 @@ var processRetryQueue = internalAction({
|
|
|
2693
2778
|
handler: async (ctx, args) => {
|
|
2694
2779
|
const limit = args.limit ?? 10;
|
|
2695
2780
|
const pendingItems = await ctx.runQuery(
|
|
2696
|
-
|
|
2781
|
+
graphSyncHelpers.getPendingRetries,
|
|
2697
2782
|
{ limit }
|
|
2698
2783
|
);
|
|
2699
2784
|
if (pendingItems.length === 0) {
|
|
@@ -2702,31 +2787,31 @@ var processRetryQueue = internalAction({
|
|
|
2702
2787
|
let succeeded = 0;
|
|
2703
2788
|
let failed = 0;
|
|
2704
2789
|
for (const item of pendingItems) {
|
|
2705
|
-
await ctx.runMutation(
|
|
2790
|
+
await ctx.runMutation(graphSyncHelpers.updateQueueStatus, {
|
|
2706
2791
|
queueId: item._id,
|
|
2707
2792
|
status: "in_progress"
|
|
2708
2793
|
});
|
|
2709
2794
|
let result;
|
|
2710
2795
|
if (item.entityType === "node") {
|
|
2711
|
-
result = await ctx.runAction(
|
|
2796
|
+
result = await ctx.runAction(graphSyncActions.syncNodeToNeo4j, {
|
|
2712
2797
|
nodeId: item.entityId,
|
|
2713
2798
|
operation: item.operation
|
|
2714
2799
|
});
|
|
2715
2800
|
} else {
|
|
2716
2801
|
const resolved = await ctx.runQuery(
|
|
2717
|
-
|
|
2802
|
+
graphSyncHelpers.resolveEdgeRetryTarget,
|
|
2718
2803
|
{
|
|
2719
2804
|
entityId: item.entityId
|
|
2720
2805
|
}
|
|
2721
2806
|
);
|
|
2722
2807
|
if (resolved.mode === "convex_id" || resolved.mode === "global_id_in_convex") {
|
|
2723
|
-
result = await ctx.runAction(
|
|
2808
|
+
result = await ctx.runAction(graphSyncActions.syncEdgeToNeo4j, {
|
|
2724
2809
|
edgeId: resolved.edgeId,
|
|
2725
2810
|
operation: item.operation
|
|
2726
2811
|
});
|
|
2727
2812
|
} else {
|
|
2728
2813
|
result = await ctx.runAction(
|
|
2729
|
-
|
|
2814
|
+
graphSyncEdgeApi.retryProjectionByGlobalId,
|
|
2730
2815
|
{
|
|
2731
2816
|
globalId: resolved.edgeGlobalId
|
|
2732
2817
|
}
|
|
@@ -2734,14 +2819,14 @@ var processRetryQueue = internalAction({
|
|
|
2734
2819
|
}
|
|
2735
2820
|
}
|
|
2736
2821
|
if (result.success) {
|
|
2737
|
-
await ctx.runMutation(
|
|
2822
|
+
await ctx.runMutation(graphSyncHelpers.updateQueueStatus, {
|
|
2738
2823
|
queueId: item._id,
|
|
2739
2824
|
status: "succeeded"
|
|
2740
2825
|
});
|
|
2741
2826
|
succeeded++;
|
|
2742
2827
|
} else {
|
|
2743
2828
|
const updated = await ctx.runMutation(
|
|
2744
|
-
|
|
2829
|
+
graphSyncHelpers.incrementAttempts,
|
|
2745
2830
|
{
|
|
2746
2831
|
queueId: item._id,
|
|
2747
2832
|
error: result.error || "Unknown error"
|
|
@@ -2767,7 +2852,7 @@ var syncEmbeddingToNeo4j = internalAction({
|
|
|
2767
2852
|
skippedReason: "credentials_missing"
|
|
2768
2853
|
});
|
|
2769
2854
|
}
|
|
2770
|
-
const node = await ctx.runQuery(
|
|
2855
|
+
const node = await ctx.runQuery(graphSyncHelpers.getNodeForSync, {
|
|
2771
2856
|
nodeId: args.nodeId
|
|
2772
2857
|
});
|
|
2773
2858
|
if (!node?.globalId) {
|
|
@@ -2775,10 +2860,9 @@ var syncEmbeddingToNeo4j = internalAction({
|
|
|
2775
2860
|
skippedReason: "source_node_missing"
|
|
2776
2861
|
});
|
|
2777
2862
|
}
|
|
2778
|
-
const embedding = await ctx.runQuery(
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
);
|
|
2863
|
+
const embedding = await ctx.runQuery(graphSyncHelpers.getEmbeddingForSync, {
|
|
2864
|
+
nodeId: args.nodeId
|
|
2865
|
+
});
|
|
2782
2866
|
if (!embedding) {
|
|
2783
2867
|
return buildSyncFailure("embedding", "sync", "Embedding not found", {
|
|
2784
2868
|
skippedReason: "embedding_missing"
|
|
@@ -2828,20 +2912,17 @@ var resyncAllNodes = internalAction({
|
|
|
2828
2912
|
returns: permissiveReturn,
|
|
2829
2913
|
handler: async (ctx, args) => {
|
|
2830
2914
|
const batchSize = args.batchSize ?? 50;
|
|
2831
|
-
const result = await ctx.runQuery(
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
cursor: args.cursor
|
|
2837
|
-
}
|
|
2838
|
-
);
|
|
2915
|
+
const result = await ctx.runQuery(graphSyncHelpers.getAllNodesForResync, {
|
|
2916
|
+
nodeType: args.nodeType,
|
|
2917
|
+
limit: batchSize,
|
|
2918
|
+
cursor: args.cursor
|
|
2919
|
+
});
|
|
2839
2920
|
let synced = 0;
|
|
2840
2921
|
let failed = 0;
|
|
2841
2922
|
for (const node of result.nodes) {
|
|
2842
2923
|
try {
|
|
2843
2924
|
const syncResult = await ctx.runAction(
|
|
2844
|
-
|
|
2925
|
+
graphSyncActions.syncNodeToNeo4j,
|
|
2845
2926
|
{
|
|
2846
2927
|
nodeId: node._id,
|
|
2847
2928
|
operation: "upsert"
|
|
@@ -2874,19 +2955,16 @@ var resyncAllEdges = internalAction({
|
|
|
2874
2955
|
returns: permissiveReturn,
|
|
2875
2956
|
handler: async (ctx, args) => {
|
|
2876
2957
|
const batchSize = args.batchSize ?? 50;
|
|
2877
|
-
const result = await ctx.runQuery(
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
cursor: args.cursor
|
|
2882
|
-
}
|
|
2883
|
-
);
|
|
2958
|
+
const result = await ctx.runQuery(graphSyncHelpers.getAllEdgesForResync, {
|
|
2959
|
+
limit: batchSize,
|
|
2960
|
+
cursor: args.cursor
|
|
2961
|
+
});
|
|
2884
2962
|
let synced = 0;
|
|
2885
2963
|
let failed = 0;
|
|
2886
2964
|
for (const edge of result.edges) {
|
|
2887
2965
|
try {
|
|
2888
2966
|
const syncResult = await ctx.runAction(
|
|
2889
|
-
|
|
2967
|
+
graphSyncActions.syncEdgeToNeo4j,
|
|
2890
2968
|
{
|
|
2891
2969
|
edgeId: edge._id,
|
|
2892
2970
|
operation: "upsert"
|
|
@@ -2937,6 +3015,16 @@ function logRetryTargetFallback(context, error) {
|
|
|
2937
3015
|
}
|
|
2938
3016
|
console.debug("[graph-sync][neo4jSyncHelpers]", context, error);
|
|
2939
3017
|
}
|
|
3018
|
+
function readString2(value) {
|
|
3019
|
+
if (typeof value !== "string") {
|
|
3020
|
+
return;
|
|
3021
|
+
}
|
|
3022
|
+
const trimmed = value.trim();
|
|
3023
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
3024
|
+
}
|
|
3025
|
+
function isSyncEdgeDoc(value) {
|
|
3026
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && "edgeType" in value && typeof value.edgeType === "string" && "fromNodeId" in value;
|
|
3027
|
+
}
|
|
2940
3028
|
var logSyncEvent = internalMutation({
|
|
2941
3029
|
args: {
|
|
2942
3030
|
eventType: v.union(
|
|
@@ -2956,7 +3044,7 @@ var logSyncEvent = internalMutation({
|
|
|
2956
3044
|
error: v.optional(v.string())
|
|
2957
3045
|
},
|
|
2958
3046
|
returns: permissiveReturn,
|
|
2959
|
-
handler:
|
|
3047
|
+
handler: (_ctx, args) => {
|
|
2960
3048
|
console.log(
|
|
2961
3049
|
`[Neo4j Sync] ${args.eventType} ${args.entityType}:${args.entityId} - ${args.status}`,
|
|
2962
3050
|
args.error || ""
|
|
@@ -2966,9 +3054,7 @@ var logSyncEvent = internalMutation({
|
|
|
2966
3054
|
var getNodeForSync = internalQuery({
|
|
2967
3055
|
args: { nodeId: v.id("epistemicNodes") },
|
|
2968
3056
|
returns: permissiveReturn,
|
|
2969
|
-
handler: async (ctx, args) =>
|
|
2970
|
-
return await ctx.db.get(args.nodeId);
|
|
2971
|
-
}
|
|
3057
|
+
handler: async (ctx, args) => await ctx.db.get(args.nodeId)
|
|
2972
3058
|
});
|
|
2973
3059
|
var getEmbeddingForSync = internalQuery({
|
|
2974
3060
|
args: { nodeId: v.id("epistemicNodes") },
|
|
@@ -2991,11 +3077,9 @@ var getAllNodesForResync = internalQuery({
|
|
|
2991
3077
|
numItems: limit,
|
|
2992
3078
|
cursor: args.cursor ?? null
|
|
2993
3079
|
};
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
(q) => q.eq("nodeType", args.nodeType)
|
|
2998
|
-
).paginate(paginationOpts);
|
|
3080
|
+
const nodeType = args.nodeType;
|
|
3081
|
+
if (nodeType) {
|
|
3082
|
+
const result2 = await ctx.db.query("epistemicNodes").withIndex("by_nodeType", (q) => q.eq("nodeType", nodeType)).paginate(paginationOpts);
|
|
2999
3083
|
return {
|
|
3000
3084
|
nodes: result2.page,
|
|
3001
3085
|
hasMore: !result2.isDone,
|
|
@@ -3024,8 +3108,8 @@ var getEdgeForSync = internalQuery({
|
|
|
3024
3108
|
...edge,
|
|
3025
3109
|
// Cross-graph edges may not have toNodeId/fromNodeId in Convex mirror.
|
|
3026
3110
|
// Fall back to denormalized global IDs when node lookup is unavailable.
|
|
3027
|
-
fromGlobalId: fromNode?.globalId
|
|
3028
|
-
toGlobalId: toNode?.globalId
|
|
3111
|
+
fromGlobalId: readString2(fromNode?.globalId) ?? edge.sourceGlobalId,
|
|
3112
|
+
toGlobalId: readString2(toNode?.globalId) ?? edge.targetGlobalId
|
|
3029
3113
|
};
|
|
3030
3114
|
}
|
|
3031
3115
|
});
|
|
@@ -3082,12 +3166,12 @@ var getEdgeBatchForSync = internalQuery({
|
|
|
3082
3166
|
const result = await ctx.db.query("epistemicEdges").order("asc").paginate(paginationOpts);
|
|
3083
3167
|
const enrichedEdges = await Promise.all(
|
|
3084
3168
|
result.page.map(async (edge) => {
|
|
3085
|
-
const fromNode = await ctx.db.get(edge.fromNodeId);
|
|
3169
|
+
const fromNode = edge.fromNodeId ? await ctx.db.get(edge.fromNodeId) : null;
|
|
3086
3170
|
const toNode = edge.toNodeId ? await ctx.db.get(edge.toNodeId) : null;
|
|
3087
3171
|
return {
|
|
3088
3172
|
...edge,
|
|
3089
|
-
fromGlobalId: fromNode?.globalId
|
|
3090
|
-
toGlobalId: toNode?.globalId
|
|
3173
|
+
fromGlobalId: readString2(fromNode?.globalId) ?? edge.sourceGlobalId,
|
|
3174
|
+
toGlobalId: readString2(toNode?.globalId) ?? edge.targetGlobalId
|
|
3091
3175
|
};
|
|
3092
3176
|
})
|
|
3093
3177
|
);
|
|
@@ -3105,13 +3189,17 @@ var resolveEdgeRetryTarget = internalQuery({
|
|
|
3105
3189
|
returns: permissiveReturn,
|
|
3106
3190
|
handler: async (ctx, args) => {
|
|
3107
3191
|
try {
|
|
3108
|
-
const
|
|
3109
|
-
if (
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3192
|
+
const directEdgeId = ctx.db.normalizeId("epistemicEdges", args.entityId);
|
|
3193
|
+
if (directEdgeId) {
|
|
3194
|
+
const byId = await ctx.db.get(directEdgeId);
|
|
3195
|
+
if (isSyncEdgeDoc(byId)) {
|
|
3196
|
+
const edgeGlobalId = readString2(byId.globalId) ?? args.entityId;
|
|
3197
|
+
return {
|
|
3198
|
+
mode: "convex_id",
|
|
3199
|
+
edgeId: byId._id,
|
|
3200
|
+
edgeGlobalId
|
|
3201
|
+
};
|
|
3202
|
+
}
|
|
3115
3203
|
}
|
|
3116
3204
|
} catch (error) {
|
|
3117
3205
|
logRetryTargetFallback(
|
|
@@ -3180,9 +3268,7 @@ var getPendingRetries = internalQuery({
|
|
|
3180
3268
|
limit: v.number()
|
|
3181
3269
|
},
|
|
3182
3270
|
returns: permissiveReturn,
|
|
3183
|
-
handler: async (ctx, args) =>
|
|
3184
|
-
return await ctx.db.query("neo4jSyncQueue").withIndex("by_status", (q) => q.eq("status", "pending")).take(args.limit);
|
|
3185
|
-
}
|
|
3271
|
+
handler: async (ctx, args) => await ctx.db.query("neo4jSyncQueue").withIndex("by_status", (q) => q.eq("status", "pending")).take(args.limit)
|
|
3186
3272
|
});
|
|
3187
3273
|
var updateQueueStatus = internalMutation({
|
|
3188
3274
|
args: {
|
|
@@ -3238,12 +3324,18 @@ var checkSyncHealth = internalQuery({
|
|
|
3238
3324
|
const recentEdges = await ctx.db.query("epistemicEdges").filter((q) => q.gte(q.field("createdAt"), oneHourAgo)).collect();
|
|
3239
3325
|
const pendingRetries = await ctx.db.query("neo4jSyncQueue").withIndex("by_status", (q) => q.eq("status", "pending")).collect();
|
|
3240
3326
|
const failedRetries = await ctx.db.query("neo4jSyncQueue").withIndex("by_status", (q) => q.eq("status", "failed")).collect();
|
|
3327
|
+
let healthStatus = "healthy";
|
|
3328
|
+
if (failedRetries.length > 10) {
|
|
3329
|
+
healthStatus = "unhealthy";
|
|
3330
|
+
} else if (pendingRetries.length > 50) {
|
|
3331
|
+
healthStatus = "degraded";
|
|
3332
|
+
}
|
|
3241
3333
|
return {
|
|
3242
3334
|
recentNodesUpdated: recentNodes.length,
|
|
3243
3335
|
recentEdgesUpdated: recentEdges.length,
|
|
3244
3336
|
pendingRetries: pendingRetries.length,
|
|
3245
3337
|
failedRetries: failedRetries.length,
|
|
3246
|
-
healthStatus
|
|
3338
|
+
healthStatus,
|
|
3247
3339
|
checkedAt: now
|
|
3248
3340
|
};
|
|
3249
3341
|
}
|