@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/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 { anyApi, internalActionGeneric, actionGeneric, internalMutationGeneric, internalQueryGeneric } from 'convex/server';
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 || !user || !password) {
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 = anyApi;
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 (!/^[a-z0-9_]+$/u.test(normalized)) {
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 (!/^[A-Z0-9_]+$/u.test(relType)) {
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 void 0;
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 properties = {
492
- globalId: args.globalId,
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
- await queueEdgeRetry(ctx, {
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) updates.weight = args.weight;
650
- if (args.confidence !== void 0) updates.confidence = args.confidence;
651
- if (args.context !== void 0) updates.context = args.context;
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[0];
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(/\/+$/u, "") : null;
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(/\/+$/u, "")}/api/neo4j-query`;
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 explicitTenant = typeof params.tenantId === "string" ? params.tenantId.trim() : "";
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
- const headers = {
920
- "Content-Type": "application/json",
921
- Authorization: `Bearer ${syncSecret}`
922
- };
923
- const bypassSecret = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
924
- if (bypassSecret && apiBaseUrl) {
925
- headers["x-vercel-protection-bypass"] = bypassSecret;
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(/^Bearer\s+(.+)$/iu);
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 expectedSecret = options.syncSecret ?? process.env.NEO4J_SYNC_SECRET?.trim();
2190
- if (!expectedSecret) {
2191
- return jsonResponse(
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
- let body;
2200
- try {
2201
- body = await request.json();
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 = body.params && typeof body.params === "object" && !Array.isArray(body.params) ? body.params : {};
2209
- if ((options.requireTenantContext ?? true) && !hasTenantContext(params)) {
2210
- return jsonResponse(
2211
- { error: "Missing required tenant context" },
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 queryName = normalizeConnectedNodesQuery(
2316
+ const namedQuery = resolveNamedQuery(
2216
2317
  body.queryName,
2217
2318
  params,
2218
2319
  options.queries
2219
2320
  );
2220
- const query = options.queries[queryName];
2221
- if (!query) {
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 pillar = metadata.pillar || metadata.topic || "";
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
- convexId: node._id,
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
- verificationStatus: node.verificationStatus || "unverified",
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
- nodeType: node.nodeType,
2435
+ criticality: readString(metadata.criticality),
2436
+ domain: node.domain || "",
2308
2437
  epistemicLayer: node.epistemicLayer || "",
2309
- // Project and metadata fields
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
- workspaceId: node.workspaceId || "",
2313
- pillar,
2314
- stage,
2315
- criticality,
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(internal.neo4jSyncHelpers.getNodeForSync, {
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
- internal.neo4jSyncHelpers.getEmbeddingForSync,
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(internal.neo4jSyncHelpers.logSyncEvent, {
2401
- eventType: args.operation === "delete" ? "node_deleted" : node ? "node_updated" : "node_created",
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(internal.neo4jSyncHelpers.logSyncEvent, {
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(internal.neo4jSyncHelpers.queueForRetry, {
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(internal.neo4jSyncHelpers.getEdgeForSync, {
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
- if (!edge.fromGlobalId || !edge.toGlobalId) {
2459
- console.warn(
2460
- "[Neo4j Sync] Edge missing fromGlobalId or toGlobalId, skipping"
2461
- );
2462
- return buildSyncFailure(
2463
- "edge",
2464
- args.operation,
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
- if (args.operation === "delete") {
2474
- await deleteEdge(edge.globalId);
2475
- console.log(`[Neo4j Sync] Deleted edge ${edge.globalId}`);
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(internal.neo4jSyncHelpers.logSyncEvent, {
2514
- eventType: args.operation === "delete" ? "edge_deleted" : "edge_created",
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(internal.neo4jSyncHelpers.queueForRetry, {
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
- internal.neo4jSyncHelpers.getNodeBatchForSync,
2540
- {
2541
- limit: batchSize,
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
- internal.neo4jSyncHelpers.getEdgeBatchForSync,
2591
- {
2592
- limit: batchSize,
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 || !edge.toGlobalId) {
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
- internal.neo4jSync.syncAllNodesToNeo4j,
2649
- {
2650
- batchSize,
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
- internal.neo4jSync.syncAllEdgesToNeo4j,
2666
- {
2667
- batchSize,
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
- internal.neo4jSyncHelpers.getPendingRetries,
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(internal.neo4jSyncHelpers.updateQueueStatus, {
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(internal.neo4jSync.syncNodeToNeo4j, {
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
- internal.neo4jSyncHelpers.resolveEdgeRetryTarget,
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(internal.neo4jSync.syncEdgeToNeo4j, {
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
- internal.neo4jEdgeAPI.retryProjectionByGlobalId,
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(internal.neo4jSyncHelpers.updateQueueStatus, {
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
- internal.neo4jSyncHelpers.incrementAttempts,
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(internal.neo4jSyncHelpers.getNodeForSync, {
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
- internal.neo4jSyncHelpers.getEmbeddingForSync,
2780
- { nodeId: args.nodeId }
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
- internal.neo4jSyncHelpers.getAllNodesForResync,
2833
- {
2834
- nodeType: args.nodeType,
2835
- limit: batchSize,
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
- internal.neo4jSync.syncNodeToNeo4j,
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
- internal.neo4jSyncHelpers.getAllEdgesForResync,
2879
- {
2880
- limit: batchSize,
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
- internal.neo4jSync.syncEdgeToNeo4j,
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: async (_ctx, args) => {
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
- if (args.nodeType) {
2995
- const result2 = await ctx.db.query("epistemicNodes").withIndex(
2996
- "by_nodeType",
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 || edge.sourceGlobalId,
3028
- toGlobalId: toNode?.globalId || edge.targetGlobalId
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 || edge.sourceGlobalId,
3090
- toGlobalId: toNode?.globalId || edge.targetGlobalId
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 byId = await ctx.db.get(args.entityId);
3109
- if (byId && "edgeType" in byId && "fromNodeId" in byId) {
3110
- return {
3111
- mode: "convex_id",
3112
- edgeId: byId._id,
3113
- edgeGlobalId: byId.globalId
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: failedRetries.length > 10 ? "unhealthy" : pendingRetries.length > 50 ? "degraded" : "healthy",
3338
+ healthStatus,
3247
3339
  checkedAt: now
3248
3340
  };
3249
3341
  }