@utaba/deep-memory-storage-neo4j 0.19.0

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.cjs ADDED
@@ -0,0 +1,3324 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ Neo4jStorageProvider: () => Neo4jStorageProvider,
34
+ SCHEMA_VERSION: () => SCHEMA_VERSION,
35
+ getSchemaCypher: () => getSchemaCypher
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/Neo4jStorageProvider.ts
40
+ var import_deep_memory8 = require("@utaba/deep-memory");
41
+
42
+ // src/Neo4jTraversalExecutor.ts
43
+ var import_deep_memory2 = require("@utaba/deep-memory");
44
+
45
+ // src/mapping.ts
46
+ var import_deep_memory = require("@utaba/deep-memory");
47
+ var MAX_SAFE_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
48
+ var MIN_SAFE_BIGINT = BigInt(Number.MIN_SAFE_INTEGER);
49
+ function bigintToSafeNumber(value) {
50
+ if (typeof value === "number") return value;
51
+ if (typeof value === "bigint") {
52
+ if (value > MAX_SAFE_BIGINT || value < MIN_SAFE_BIGINT) {
53
+ throw new import_deep_memory.ProviderError(
54
+ `Neo4j integer value ${value.toString()} exceeds Number.MAX_SAFE_INTEGER \u2014 refusing to truncate.`
55
+ );
56
+ }
57
+ return Number(value);
58
+ }
59
+ throw new import_deep_memory.ProviderError(
60
+ `bigintToSafeNumber expected bigint | number, received ${typeof value}.`
61
+ );
62
+ }
63
+ function entityFromRecord(record, alias = "n") {
64
+ return entityFromProperties(extractPropertyBag(record, alias));
65
+ }
66
+ function relationshipFromRecord(record, alias = "r") {
67
+ return relationshipFromProperties(extractPropertyBag(record, alias));
68
+ }
69
+ function entityFromProperties(props) {
70
+ const summary = optionalString(props, "summary");
71
+ const data = optionalString(props, "data");
72
+ const dataFormat = optionalString(props, "dataFormat");
73
+ const embedding = parseEmbedding(props);
74
+ const entity = {
75
+ id: requireString(props, "id"),
76
+ slug: requireString(props, "slug"),
77
+ entityType: requireString(props, "entityType"),
78
+ label: requireString(props, "label"),
79
+ properties: parsePropertiesBlob(props),
80
+ provenance: provenanceFromProperties(props)
81
+ };
82
+ if (summary !== void 0) entity.summary = summary;
83
+ if (data !== void 0) entity.data = data;
84
+ if (dataFormat !== void 0) entity.dataFormat = dataFormat;
85
+ if (embedding !== void 0) entity.embedding = embedding;
86
+ return entity;
87
+ }
88
+ function relationshipFromProperties(props) {
89
+ return {
90
+ id: requireString(props, "id"),
91
+ relationshipType: requireString(props, "relationshipType"),
92
+ sourceEntityId: requireString(props, "sourceEntityId"),
93
+ targetEntityId: requireString(props, "targetEntityId"),
94
+ properties: parsePropertiesBlob(props),
95
+ bidirectional: requireBoolean(props, "bidirectional"),
96
+ provenance: provenanceFromProperties(props)
97
+ };
98
+ }
99
+ function repositoryFromRecord(record, alias = "r") {
100
+ return repositoryFromProperties(extractPropertyBag(record, alias));
101
+ }
102
+ function repositorySummaryFromRecord(record, alias = "r") {
103
+ return repositorySummaryFromProperties(extractPropertyBag(record, alias));
104
+ }
105
+ function repositoryFromProperties(props) {
106
+ const type = optionalString(props, "type");
107
+ const description = optionalString(props, "description");
108
+ const legal = optionalString(props, "legal");
109
+ const owner = optionalString(props, "owner");
110
+ const metadata = parseOptionalJsonObject(props, "metadata");
111
+ const repo = {
112
+ repositoryId: requireString(props, "repositoryId"),
113
+ label: requireString(props, "label"),
114
+ governanceConfig: parseRequiredJsonObject(props, "governanceConfig"),
115
+ createdAt: requireString(props, "createdAt"),
116
+ createdBy: requireString(props, "createdBy")
117
+ };
118
+ if (type !== void 0) repo.type = type;
119
+ if (description !== void 0) repo.description = description;
120
+ if (legal !== void 0) repo.legal = legal;
121
+ if (owner !== void 0) repo.owner = owner;
122
+ if (metadata !== void 0) repo.metadata = metadata;
123
+ return repo;
124
+ }
125
+ function repositorySummaryFromProperties(props) {
126
+ const type = optionalString(props, "type");
127
+ const description = optionalString(props, "description");
128
+ const summary = {
129
+ repositoryId: requireString(props, "repositoryId"),
130
+ label: requireString(props, "label"),
131
+ governanceConfig: parseRequiredJsonObject(props, "governanceConfig")
132
+ };
133
+ if (type !== void 0) summary.type = type;
134
+ if (description !== void 0) summary.description = description;
135
+ return summary;
136
+ }
137
+ function changeRecordFromRecord(record, alias = "e") {
138
+ return changeRecordFromProperties(extractPropertyBag(record, alias));
139
+ }
140
+ function changeRecordFromProperties(props) {
141
+ const previousVersion = optionalString(props, "previousVersion");
142
+ const approvedBy = optionalString(props, "approvedBy");
143
+ const approvedAt = optionalString(props, "approvedAt");
144
+ const record = {
145
+ changeId: requireString(props, "changeId"),
146
+ changeType: requireChangeType(props, "changeType"),
147
+ typeName: requireString(props, "typeName"),
148
+ newVersion: requireString(props, "newVersion"),
149
+ proposedBy: requireString(props, "proposedBy"),
150
+ proposedAt: requireString(props, "proposedAt"),
151
+ reason: requireString(props, "reason")
152
+ };
153
+ if (previousVersion !== void 0) record.previousVersion = previousVersion;
154
+ if (approvedBy !== void 0) record.approvedBy = approvedBy;
155
+ if (approvedAt !== void 0) record.approvedAt = approvedAt;
156
+ return record;
157
+ }
158
+ var STORED_ENTITY_FIELDS = [
159
+ "id",
160
+ "entityType",
161
+ "label",
162
+ "slug",
163
+ "summary",
164
+ "properties",
165
+ "data",
166
+ "dataFormat",
167
+ "createdBy",
168
+ "createdByType",
169
+ "createdAt",
170
+ "createdInConversation",
171
+ "createdFromMessage",
172
+ "modifiedBy",
173
+ "modifiedByType",
174
+ "modifiedAt",
175
+ "modifiedInConversation",
176
+ "modifiedFromMessage"
177
+ ];
178
+ function buildEntityProjection(options) {
179
+ const alias = options?.alias ?? "n";
180
+ const parts = STORED_ENTITY_FIELDS.map((field) => `${alias}.${field} AS ${field}`);
181
+ if (options?.loadEmbeddings === true) {
182
+ parts.push(`${alias}.embedding AS embedding`);
183
+ }
184
+ return parts.join(", ");
185
+ }
186
+ var RESERVED_ENTITY_PROPERTY_KEYS = /* @__PURE__ */ new Set([
187
+ "id",
188
+ "repositoryId",
189
+ "entityType",
190
+ "label",
191
+ "slug",
192
+ "summary",
193
+ "properties",
194
+ "data",
195
+ "dataFormat",
196
+ "embedding",
197
+ "createdBy",
198
+ "createdByType",
199
+ "createdAt",
200
+ "createdInConversation",
201
+ "createdFromMessage",
202
+ "modifiedBy",
203
+ "modifiedByType",
204
+ "modifiedAt",
205
+ "modifiedInConversation",
206
+ "modifiedFromMessage"
207
+ ]);
208
+ var USER_PROPERTY_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
209
+ function assertSafeUserPropertyKey(key) {
210
+ if (!USER_PROPERTY_KEY_PATTERN.test(key)) {
211
+ throw new import_deep_memory.ProviderError(
212
+ `Entity property key "${key}" is not a valid Cypher identifier \u2014 must match ${USER_PROPERTY_KEY_PATTERN.source}. User-property keys are interpolated into Cypher REMOVE / predicate slots (the key slot cannot be parameterised), so an unsafe value would widen the injection surface.`
213
+ );
214
+ }
215
+ if (RESERVED_ENTITY_PROPERTY_KEYS.has(key)) {
216
+ throw new import_deep_memory.ProviderError(
217
+ `Entity property key "${key}" collides with a schema-managed field. Reserved names: ${Array.from(RESERVED_ENTITY_PROPERTY_KEYS).join(", ")}.`
218
+ );
219
+ }
220
+ return key;
221
+ }
222
+ function isNativeStorableValue(value) {
223
+ if (typeof value === "string") return true;
224
+ if (typeof value === "boolean") return true;
225
+ if (typeof value === "number") return Number.isFinite(value);
226
+ if (Array.isArray(value)) {
227
+ if (value.length === 0) return true;
228
+ const first = value[0];
229
+ const t = typeof first;
230
+ if (t !== "string" && t !== "boolean" && (t !== "number" || !Number.isFinite(first))) {
231
+ return false;
232
+ }
233
+ for (const v of value) {
234
+ if (typeof v !== t) return false;
235
+ if (t === "number" && !Number.isFinite(v)) return false;
236
+ }
237
+ return true;
238
+ }
239
+ return false;
240
+ }
241
+ function entityToParams(entity) {
242
+ const p = entity.provenance;
243
+ return {
244
+ id: entity.id,
245
+ entityType: entity.entityType,
246
+ label: entity.label,
247
+ slug: entity.slug,
248
+ summary: entity.summary ?? null,
249
+ properties: JSON.stringify(entity.properties),
250
+ data: entity.data ?? null,
251
+ dataFormat: entity.dataFormat ?? null,
252
+ embedding: entity.embedding ?? null,
253
+ createdBy: p.createdBy,
254
+ createdByType: p.createdByType,
255
+ createdAt: p.createdAt,
256
+ createdInConversation: p.createdInConversation ?? null,
257
+ createdFromMessage: p.createdFromMessage ?? null,
258
+ modifiedBy: p.modifiedBy,
259
+ modifiedByType: p.modifiedByType,
260
+ modifiedAt: p.modifiedAt,
261
+ modifiedInConversation: p.modifiedInConversation ?? null,
262
+ modifiedFromMessage: p.modifiedFromMessage ?? null
263
+ };
264
+ }
265
+ function entityUserPropertyParams(properties) {
266
+ const out = {};
267
+ for (const [key, value] of Object.entries(properties)) {
268
+ assertSafeUserPropertyKey(key);
269
+ if (isNativeStorableValue(value)) {
270
+ out[key] = value;
271
+ }
272
+ }
273
+ return out;
274
+ }
275
+ var STORED_RELATIONSHIP_FIELDS = [
276
+ "id",
277
+ "relationshipType",
278
+ "sourceEntityId",
279
+ "targetEntityId",
280
+ "properties",
281
+ "bidirectional",
282
+ "createdBy",
283
+ "createdByType",
284
+ "createdAt",
285
+ "createdInConversation",
286
+ "createdFromMessage",
287
+ "modifiedBy",
288
+ "modifiedByType",
289
+ "modifiedAt",
290
+ "modifiedInConversation",
291
+ "modifiedFromMessage"
292
+ ];
293
+ function buildRelationshipProjection(options) {
294
+ const alias = options?.alias ?? "r";
295
+ return STORED_RELATIONSHIP_FIELDS.map((field) => `${alias}.${field} AS ${field}`).join(", ");
296
+ }
297
+ function relationshipToParams(rel) {
298
+ const p = rel.provenance;
299
+ return {
300
+ id: rel.id,
301
+ relationshipType: rel.relationshipType,
302
+ sourceEntityId: rel.sourceEntityId,
303
+ targetEntityId: rel.targetEntityId,
304
+ properties: JSON.stringify(rel.properties ?? {}),
305
+ bidirectional: rel.bidirectional,
306
+ createdBy: p.createdBy,
307
+ createdByType: p.createdByType,
308
+ createdAt: p.createdAt,
309
+ createdInConversation: p.createdInConversation ?? null,
310
+ createdFromMessage: p.createdFromMessage ?? null,
311
+ modifiedBy: p.modifiedBy,
312
+ modifiedByType: p.modifiedByType,
313
+ modifiedAt: p.modifiedAt,
314
+ modifiedInConversation: p.modifiedInConversation ?? null,
315
+ modifiedFromMessage: p.modifiedFromMessage ?? null
316
+ };
317
+ }
318
+ var RELATIONSHIP_TYPE_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
319
+ function assertSafeRelationshipType(value) {
320
+ if (!RELATIONSHIP_TYPE_PATTERN.test(value)) {
321
+ throw new import_deep_memory.ProviderError(
322
+ `Relationship type "${value}" is not a valid Cypher identifier \u2014 must match ${RELATIONSHIP_TYPE_PATTERN.source}. Cypher 25 does not allow parameterising the relationship-type slot, so the value is interpolated directly into the query string and an unsafe value would otherwise widen the injection surface.`
323
+ );
324
+ }
325
+ return value;
326
+ }
327
+ function repositoryCreateParams(config) {
328
+ return {
329
+ type: config.type ?? null,
330
+ label: config.label,
331
+ description: config.description ?? null,
332
+ legal: config.legal ?? null,
333
+ owner: config.owner ?? null,
334
+ governanceConfig: JSON.stringify(config.governanceConfig),
335
+ metadata: config.metadata !== void 0 ? JSON.stringify(config.metadata) : null,
336
+ createdAt: config.createdAt,
337
+ createdBy: config.createdBy
338
+ };
339
+ }
340
+ function extractPropertyBag(record, alias) {
341
+ if (record.keys.includes(alias)) {
342
+ const value = record.get(alias);
343
+ if (isNodeLike(value)) return value.properties;
344
+ if (isRelationshipLike(value)) return value.properties;
345
+ }
346
+ const bag = {};
347
+ for (const key of record.keys) {
348
+ if (typeof key !== "string") continue;
349
+ bag[key] = record.get(key);
350
+ }
351
+ return bag;
352
+ }
353
+ function isNodeLike(value) {
354
+ if (typeof value !== "object" || value === null) return false;
355
+ const v = value;
356
+ return typeof v.properties === "object" && v.properties !== null && !Array.isArray(v.properties) && Array.isArray(v.labels);
357
+ }
358
+ function isRelationshipLike(value) {
359
+ if (typeof value !== "object" || value === null) return false;
360
+ const v = value;
361
+ return typeof v.properties === "object" && v.properties !== null && !Array.isArray(v.properties) && typeof v.type === "string";
362
+ }
363
+ function provenanceFromProperties(props) {
364
+ const provenance = {
365
+ createdBy: requireString(props, "createdBy"),
366
+ createdByType: requireActorType(props, "createdByType"),
367
+ createdAt: requireString(props, "createdAt"),
368
+ modifiedBy: requireString(props, "modifiedBy"),
369
+ modifiedByType: requireActorType(props, "modifiedByType"),
370
+ modifiedAt: requireString(props, "modifiedAt")
371
+ };
372
+ const createdInConversation = optionalString(props, "createdInConversation");
373
+ const createdFromMessage = optionalString(props, "createdFromMessage");
374
+ const modifiedInConversation = optionalString(props, "modifiedInConversation");
375
+ const modifiedFromMessage = optionalString(props, "modifiedFromMessage");
376
+ if (createdInConversation !== void 0) provenance.createdInConversation = createdInConversation;
377
+ if (createdFromMessage !== void 0) provenance.createdFromMessage = createdFromMessage;
378
+ if (modifiedInConversation !== void 0) provenance.modifiedInConversation = modifiedInConversation;
379
+ if (modifiedFromMessage !== void 0) provenance.modifiedFromMessage = modifiedFromMessage;
380
+ return provenance;
381
+ }
382
+ function requireString(props, key) {
383
+ const value = props[key];
384
+ if (typeof value !== "string") {
385
+ throw new import_deep_memory.ProviderError(
386
+ `Neo4j record is missing required string field "${key}" (got ${describeType(value)}).`
387
+ );
388
+ }
389
+ return value;
390
+ }
391
+ function requireActorType(props, key) {
392
+ const value = requireString(props, key);
393
+ if (value !== "user" && value !== "agent") {
394
+ throw new import_deep_memory.ProviderError(
395
+ `Neo4j record field "${key}" must be "user" or "agent" (got ${JSON.stringify(value)}).`
396
+ );
397
+ }
398
+ return value;
399
+ }
400
+ var CHANGE_TYPES = [
401
+ "entity_type_added",
402
+ "relationship_type_added",
403
+ "entity_type_modified",
404
+ "relationship_type_modified",
405
+ "entity_type_removed",
406
+ "relationship_type_removed"
407
+ ];
408
+ function requireChangeType(props, key) {
409
+ const value = requireString(props, key);
410
+ if (!CHANGE_TYPES.includes(value)) {
411
+ throw new import_deep_memory.ProviderError(
412
+ `Neo4j record field "${key}" must be one of the VocabularyChangeRecord change types (got ${JSON.stringify(value)}).`
413
+ );
414
+ }
415
+ return value;
416
+ }
417
+ function requireBoolean(props, key) {
418
+ const value = props[key];
419
+ if (typeof value !== "boolean") {
420
+ throw new import_deep_memory.ProviderError(
421
+ `Neo4j record is missing required boolean field "${key}" (got ${describeType(value)}).`
422
+ );
423
+ }
424
+ return value;
425
+ }
426
+ function optionalString(props, key) {
427
+ const value = props[key];
428
+ if (value === void 0 || value === null || value === "") return void 0;
429
+ if (typeof value !== "string") {
430
+ throw new import_deep_memory.ProviderError(
431
+ `Neo4j record field "${key}" must be a string or empty (got ${describeType(value)}).`
432
+ );
433
+ }
434
+ return value;
435
+ }
436
+ function parsePropertiesBlob(props) {
437
+ const raw = props["properties"];
438
+ if (raw === void 0 || raw === null || raw === "") return {};
439
+ if (typeof raw !== "string") {
440
+ throw new import_deep_memory.ProviderError(
441
+ `Neo4j record field "properties" must be a JSON string (got ${describeType(raw)}).`
442
+ );
443
+ }
444
+ let parsed;
445
+ try {
446
+ parsed = JSON.parse(raw);
447
+ } catch (err) {
448
+ const detail = err instanceof Error ? err.message : String(err);
449
+ throw new import_deep_memory.ProviderError(`Neo4j record field "properties" is not valid JSON: ${detail}.`);
450
+ }
451
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
452
+ throw new import_deep_memory.ProviderError(
453
+ `Neo4j record field "properties" must decode to a JSON object (got ${describeType(parsed)}).`
454
+ );
455
+ }
456
+ return parsed;
457
+ }
458
+ function parseRequiredJsonObject(props, key) {
459
+ const raw = props[key];
460
+ if (typeof raw !== "string" || raw === "") {
461
+ throw new import_deep_memory.ProviderError(
462
+ `Neo4j record field "${key}" must be a non-empty JSON string (got ${describeType(raw)}).`
463
+ );
464
+ }
465
+ let parsed;
466
+ try {
467
+ parsed = JSON.parse(raw);
468
+ } catch (err) {
469
+ const detail = err instanceof Error ? err.message : String(err);
470
+ throw new import_deep_memory.ProviderError(`Neo4j record field "${key}" is not valid JSON: ${detail}.`);
471
+ }
472
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
473
+ throw new import_deep_memory.ProviderError(
474
+ `Neo4j record field "${key}" must decode to a JSON object (got ${describeType(parsed)}).`
475
+ );
476
+ }
477
+ return parsed;
478
+ }
479
+ function parseOptionalJsonObject(props, key) {
480
+ const raw = props[key];
481
+ if (raw === void 0 || raw === null || raw === "") return void 0;
482
+ if (typeof raw !== "string") {
483
+ throw new import_deep_memory.ProviderError(
484
+ `Neo4j record field "${key}" must be a JSON string (got ${describeType(raw)}).`
485
+ );
486
+ }
487
+ let parsed;
488
+ try {
489
+ parsed = JSON.parse(raw);
490
+ } catch (err) {
491
+ const detail = err instanceof Error ? err.message : String(err);
492
+ throw new import_deep_memory.ProviderError(`Neo4j record field "${key}" is not valid JSON: ${detail}.`);
493
+ }
494
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
495
+ throw new import_deep_memory.ProviderError(
496
+ `Neo4j record field "${key}" must decode to a JSON object (got ${describeType(parsed)}).`
497
+ );
498
+ }
499
+ return parsed;
500
+ }
501
+ function parseEmbedding(props) {
502
+ const raw = props["embedding"];
503
+ if (raw === void 0 || raw === null) return void 0;
504
+ if (!Array.isArray(raw)) {
505
+ throw new import_deep_memory.ProviderError(
506
+ `Neo4j record field "embedding" must be an array of numbers (got ${describeType(raw)}).`
507
+ );
508
+ }
509
+ for (const v of raw) {
510
+ if (typeof v !== "number") {
511
+ throw new import_deep_memory.ProviderError(
512
+ `Neo4j record field "embedding" must contain only numbers (got ${describeType(v)}).`
513
+ );
514
+ }
515
+ }
516
+ return raw;
517
+ }
518
+ function describeType(value) {
519
+ if (value === null) return "null";
520
+ if (Array.isArray(value)) return "array";
521
+ return typeof value;
522
+ }
523
+
524
+ // src/Neo4jTraversalExecutor.ts
525
+ var Neo4jTraversalExecutor = class {
526
+ constructor(connection, config) {
527
+ this.connection = connection;
528
+ this.config = config;
529
+ }
530
+ connection;
531
+ config;
532
+ compiler = new import_deep_memory2.CypherCompiler();
533
+ /**
534
+ * Compile, scope, submit, and parse a traversal spec. The provider's vocab
535
+ * cache is the source of `vocabulary` — `traverse` / `exploreNeighborhood`
536
+ * / `findPaths` look it up once and pass it down.
537
+ */
538
+ async execute(repositoryId, spec, vocabulary) {
539
+ const startTime = Date.now();
540
+ const compiled = this.compiler.compile(spec, vocabulary);
541
+ const scopedQuery = this.scopeQuery(compiled.query);
542
+ const finalQuery = this.config.profileTraversals ? `PROFILE ${scopedQuery}` : scopedQuery;
543
+ const params = coerceSkipLimitToBigInt(finalQuery, compiled.params);
544
+ let result;
545
+ try {
546
+ result = await this.connection.executeQuery(
547
+ finalQuery,
548
+ params,
549
+ { repositoryId, routing: "READ" }
550
+ );
551
+ } catch (err) {
552
+ throw new import_deep_memory2.ProviderError(
553
+ `Neo4j traversal failed: ${err instanceof Error ? err.message : String(err)}`,
554
+ "Inspect the compiled Cypher in queryMetadata.compiledQuery and confirm the spec passed validation."
555
+ );
556
+ }
557
+ const executionTimeMs = Date.now() - startTime;
558
+ const summary = result.summary;
559
+ const serverMs = bigintLike(summary.resultConsumedAfter);
560
+ const raw = {
561
+ terminalEntities: [],
562
+ allEntities: [],
563
+ allRelationships: [],
564
+ pathRows: [],
565
+ entityMap: /* @__PURE__ */ new Map(),
566
+ relationshipMap: /* @__PURE__ */ new Map(),
567
+ pathRelFirstDirection: /* @__PURE__ */ new Map(),
568
+ executionTimeMs,
569
+ serverMs,
570
+ compiledQuery: finalQuery
571
+ };
572
+ if (this.config.profileTraversals && summary.profile !== void 0) {
573
+ raw.profile = summariseProfile(summary.profile);
574
+ }
575
+ const emitsProjection = spec.projection !== void 0 && spec.returnMode !== "path" && spec.returnMode !== "all";
576
+ if (emitsProjection) {
577
+ this.parseProjectionRows(result.records, raw, spec.projection.properties, spec.projection.mode ?? "values");
578
+ } else if (spec.returnMode === "terminal") {
579
+ this.parseTerminalRows(result.records, raw);
580
+ } else if (spec.returnMode === "all") {
581
+ this.parseAllRows(result.records, raw);
582
+ } else {
583
+ this.parsePathRows(result.records, raw);
584
+ }
585
+ return raw;
586
+ }
587
+ /**
588
+ * Parse the server-aggregated projection rows. Each row has one column per
589
+ * projected property and, when mode is 'count', an additional `count` column.
590
+ * Values arrive as native scalars / strings / BigInts; counts arrive as
591
+ * BigInt (per the driver's `useBigInt: true` configuration) and coerce to
592
+ * Number for the JSON wire shape.
593
+ */
594
+ parseProjectionRows(records, raw, propertyNames, mode) {
595
+ const aggregations = [];
596
+ for (const record of records) {
597
+ const values = {};
598
+ for (const prop of propertyNames) {
599
+ values[prop] = normaliseScalar(record.get(prop));
600
+ }
601
+ if (mode === "count") {
602
+ const rawCount = record.get("count");
603
+ const count = typeof rawCount === "bigint" ? Number(rawCount) : typeof rawCount === "number" ? rawCount : 0;
604
+ aggregations.push({ values, count });
605
+ } else {
606
+ aggregations.push({ values });
607
+ }
608
+ }
609
+ raw.aggregations = aggregations;
610
+ }
611
+ /**
612
+ * Inject the repositoryId scope onto the start node so the chokepoint's
613
+ * `$rid` assertion is satisfied structurally and the planner uses the
614
+ * `(repositoryId, id)` unique constraint's backing index for the seek.
615
+ *
616
+ * The compiler always emits `(n0)` as the first match part (it never adds a
617
+ * label or property map there) — we rewrite that single occurrence. The
618
+ * provider's defence-in-depth (D3b layer 3) prevents cross-repository edges
619
+ * from existing in the first place, so scoping just the start node is
620
+ * sufficient — once the planner anchors on a node in this repo, every
621
+ * reachable node is also in this repo.
622
+ */
623
+ scopeQuery(query) {
624
+ const scope = "(n0:_Entity {repositoryId: $rid})";
625
+ const replaced = query.replace("(n0)", scope);
626
+ if (replaced === query) {
627
+ throw new import_deep_memory2.ProviderError(
628
+ "Neo4jTraversalExecutor: compiled query did not contain the expected `(n0)` start-node form; cannot inject repositoryId scope.",
629
+ "This indicates a CypherCompiler emission change that the provider has not been updated for."
630
+ );
631
+ }
632
+ return replaced;
633
+ }
634
+ parseTerminalRows(records, raw) {
635
+ for (const record of records) {
636
+ for (const key of record.keys) {
637
+ if (typeof key !== "string") continue;
638
+ const value = record.get(key);
639
+ if (isNode(value)) {
640
+ const stored = entityFromNode(value);
641
+ raw.terminalEntities.push(stored);
642
+ if (!raw.entityMap.has(stored.id)) raw.entityMap.set(stored.id, stored);
643
+ }
644
+ }
645
+ }
646
+ }
647
+ parseAllRows(records, raw) {
648
+ for (const record of records) {
649
+ for (const key of record.keys) {
650
+ if (typeof key !== "string") continue;
651
+ const value = record.get(key);
652
+ if (isNode(value)) {
653
+ const stored = entityFromNode(value);
654
+ if (!raw.entityMap.has(stored.id)) {
655
+ raw.entityMap.set(stored.id, stored);
656
+ raw.allEntities.push(stored);
657
+ }
658
+ } else if (isRelationship(value)) {
659
+ const stored = relationshipFromRelationship(value);
660
+ if (!raw.relationshipMap.has(stored.id)) {
661
+ raw.relationshipMap.set(stored.id, stored);
662
+ raw.allRelationships.push(stored);
663
+ }
664
+ }
665
+ }
666
+ }
667
+ }
668
+ parsePathRows(records, raw) {
669
+ for (const record of records) {
670
+ const pathNodes = record.get("pathNodes");
671
+ const pathRels = record.get("pathRels");
672
+ if (!Array.isArray(pathNodes) || !Array.isArray(pathRels)) continue;
673
+ const pathEntityIds = [];
674
+ const pathRelIds = [];
675
+ const pathRelDirections = [];
676
+ let previousElementId;
677
+ for (const node of pathNodes) {
678
+ if (!isNode(node)) continue;
679
+ const stored = entityFromNode(node);
680
+ if (!raw.entityMap.has(stored.id)) raw.entityMap.set(stored.id, stored);
681
+ pathEntityIds.push(stored.id);
682
+ }
683
+ for (let i = 0; i < pathRels.length; i++) {
684
+ const rel = pathRels[i];
685
+ if (!isRelationship(rel)) continue;
686
+ const stored = relationshipFromRelationship(rel);
687
+ if (!raw.relationshipMap.has(stored.id)) raw.relationshipMap.set(stored.id, stored);
688
+ pathRelIds.push(stored.id);
689
+ const segmentStartNode = pathNodes[i];
690
+ const segmentStartElementId = isNode(segmentStartNode) ? segmentStartNode.elementId : previousElementId;
691
+ const direction = segmentStartElementId !== void 0 && rel.startNodeElementId === segmentStartElementId ? "out" : "in";
692
+ pathRelDirections.push(direction);
693
+ if (!raw.pathRelFirstDirection.has(stored.id)) {
694
+ raw.pathRelFirstDirection.set(stored.id, direction);
695
+ }
696
+ const segmentEndNode = pathNodes[i + 1];
697
+ if (isNode(segmentEndNode)) previousElementId = segmentEndNode.elementId;
698
+ }
699
+ raw.pathRows.push({
700
+ entityIds: pathEntityIds,
701
+ relationshipIds: pathRelIds,
702
+ relationshipDirections: pathRelDirections
703
+ });
704
+ }
705
+ }
706
+ };
707
+ function isNode(value) {
708
+ if (typeof value !== "object" || value === null) return false;
709
+ const v = value;
710
+ return typeof v.elementId === "string" && Array.isArray(v.labels) && typeof v.properties === "object" && v.properties !== null;
711
+ }
712
+ function isRelationship(value) {
713
+ if (typeof value !== "object" || value === null) return false;
714
+ const v = value;
715
+ return typeof v.elementId === "string" && typeof v.type === "string" && typeof v.startNodeElementId === "string" && typeof v.endNodeElementId === "string" && typeof v.properties === "object" && v.properties !== null;
716
+ }
717
+ function entityFromNode(node) {
718
+ return entityFromProperties(node.properties);
719
+ }
720
+ function relationshipFromRelationship(rel) {
721
+ return relationshipFromProperties(rel.properties);
722
+ }
723
+ function bigintLike(value) {
724
+ if (typeof value === "bigint") return Number(value);
725
+ if (typeof value === "number") return value;
726
+ return 0;
727
+ }
728
+ function normaliseScalar(value) {
729
+ if (typeof value === "bigint") return Number(value);
730
+ return value;
731
+ }
732
+ var SKIP_LIMIT_PARAM_PATTERN = /\b(?:SKIP|LIMIT)\s+\$(\w+)/gi;
733
+ function coerceSkipLimitToBigInt(cypher, params) {
734
+ const targetKeys = /* @__PURE__ */ new Set();
735
+ for (const match of cypher.matchAll(SKIP_LIMIT_PARAM_PATTERN)) {
736
+ if (match[1] !== void 0) targetKeys.add(match[1]);
737
+ }
738
+ if (targetKeys.size === 0) return params;
739
+ const out = { ...params };
740
+ for (const key of targetKeys) {
741
+ const value = out[key];
742
+ if (typeof value === "number" && Number.isFinite(value)) {
743
+ out[key] = BigInt(Math.trunc(value));
744
+ }
745
+ }
746
+ return out;
747
+ }
748
+ function summariseProfile(plan) {
749
+ let total = 0;
750
+ const visit = (node) => {
751
+ if (node === void 0) return;
752
+ const hits = node.dbHits;
753
+ if (typeof hits === "bigint") total += Number(hits);
754
+ else if (typeof hits === "number") total += hits;
755
+ for (const child of node.children ?? []) visit(child);
756
+ };
757
+ visit(plan);
758
+ return { totalDbHits: total, rootOperator: plan.operatorType ?? "unknown" };
759
+ }
760
+
761
+ // src/Neo4jConnection.ts
762
+ var import_neo4j_driver = __toESM(require("neo4j-driver"), 1);
763
+ var import_deep_memory3 = require("@utaba/deep-memory");
764
+
765
+ // src/usageScope.ts
766
+ var import_node_async_hooks = require("async_hooks");
767
+ var COUNTER_KEYS = [
768
+ "nodesCreated",
769
+ "nodesDeleted",
770
+ "relationshipsCreated",
771
+ "relationshipsDeleted",
772
+ "propertiesSet",
773
+ "labelsAdded",
774
+ "labelsRemoved",
775
+ "indexesAdded",
776
+ "indexesRemoved",
777
+ "constraintsAdded",
778
+ "constraintsRemoved"
779
+ ];
780
+ var store = new import_node_async_hooks.AsyncLocalStorage();
781
+ function createUsageScope() {
782
+ return {
783
+ calls: 0,
784
+ recordCount: 0,
785
+ serverMs: 0,
786
+ availableAfterMs: 0,
787
+ counters: {}
788
+ };
789
+ }
790
+ function runInUsageScope(scope, fn) {
791
+ return store.run(scope, fn);
792
+ }
793
+ function recordRoundTrip(summary, recordCount) {
794
+ const scope = store.getStore();
795
+ if (scope === void 0) return;
796
+ scope.calls += 1;
797
+ scope.recordCount += recordCount;
798
+ if (summary.resultConsumedAfter !== void 0) {
799
+ scope.serverMs += bigintToSafeNumber(summary.resultConsumedAfter);
800
+ }
801
+ if (summary.resultAvailableAfter !== void 0) {
802
+ scope.availableAfterMs += bigintToSafeNumber(summary.resultAvailableAfter);
803
+ }
804
+ const stats = summary.counters !== void 0 ? summary.counters.updates() : void 0;
805
+ if (stats !== void 0) {
806
+ for (const key of COUNTER_KEYS) {
807
+ const value = stats[key];
808
+ if (typeof value === "number" && value !== 0) {
809
+ scope.counters[key] = (scope.counters[key] ?? 0) + value;
810
+ }
811
+ }
812
+ }
813
+ }
814
+ function buildUsageDetails(scope) {
815
+ const details = {
816
+ calls: scope.calls,
817
+ recordCount: scope.recordCount,
818
+ availableAfterMs: scope.availableAfterMs
819
+ };
820
+ for (const [key, value] of Object.entries(scope.counters)) {
821
+ if (value !== 0) details[key] = value;
822
+ }
823
+ return details;
824
+ }
825
+
826
+ // src/Neo4jConnection.ts
827
+ var DEFAULT_DATABASE = "neo4j";
828
+ var DEFAULT_USER_AGENT = "@utaba/deep-memory-storage-neo4j";
829
+ var RID_TOKEN_PATTERN = /\$rid\b/;
830
+ var NOTIFICATION_QUERY_TRUNCATION = 200;
831
+ var Neo4jConnection = class {
832
+ driver;
833
+ database;
834
+ constructor(config) {
835
+ this.database = config.database ?? DEFAULT_DATABASE;
836
+ this.driver = import_neo4j_driver.default.driver(
837
+ config.uri,
838
+ import_neo4j_driver.default.auth.basic(config.username, config.password),
839
+ {
840
+ // BigInt for INTEGER avoids silent precision loss on counts that may
841
+ // exceed Number.MAX_SAFE_INTEGER — see D6b. The mapping layer
842
+ // narrows BigInt → Number at the public-API boundary.
843
+ useBigInt: true,
844
+ userAgent: config.userAgent ?? DEFAULT_USER_AGENT,
845
+ ...config.maxTransactionRetryTime !== void 0 ? { maxTransactionRetryTime: config.maxTransactionRetryTime } : {}
846
+ }
847
+ );
848
+ }
849
+ /** Verify the driver can reach the server. Throws on failure. */
850
+ async verifyConnectivity() {
851
+ await this.driver.verifyConnectivity({ database: this.database });
852
+ }
853
+ /** Returns server version / edition metadata for diagnostics. */
854
+ async getServerInfo() {
855
+ return this.driver.getServerInfo({ database: this.database });
856
+ }
857
+ /** Close the driver and release all connections. */
858
+ async close() {
859
+ await this.driver.close();
860
+ }
861
+ /**
862
+ * Run a single-statement query scoped to a repository.
863
+ *
864
+ * Required `repositoryId` binds `$rid` for the caller. The Cypher string
865
+ * MUST reference `$rid` somewhere in a predicate or property map; absence
866
+ * throws `ProviderError` as a programming error (D3b layer 2).
867
+ */
868
+ async executeQuery(cypher, params, options) {
869
+ this.assertRepositoryId(options.repositoryId);
870
+ this.assertScoped(cypher);
871
+ const result = await this.driver.executeQuery(
872
+ cypher,
873
+ { ...params, rid: options.repositoryId },
874
+ {
875
+ database: this.database,
876
+ ...options.routing !== void 0 ? { routing: options.routing } : {}
877
+ }
878
+ );
879
+ recordRoundTrip(result.summary, result.records.length);
880
+ this.surfaceNotifications(cypher, result.summary);
881
+ return result;
882
+ }
883
+ /**
884
+ * Run a managed write transaction scoped to a repository. Use only when
885
+ * more than one Cypher statement must commit atomically — single-statement
886
+ * writes go through `executeQuery` so the driver handles transient-error
887
+ * retry uniformly (D2b).
888
+ *
889
+ * Every `tx.run` call inside `txFn` is wrapped to inject `$rid` and assert
890
+ * scope, so the same isolation enforcement applies as on the default path.
891
+ * The transaction function MUST be idempotent (the driver retries on
892
+ * transient errors) and MUST NOT return the raw `Result` — process records
893
+ * inside the function and return mapped data.
894
+ */
895
+ async executeWrite(repositoryId, txFn) {
896
+ return this.runManaged("write", repositoryId, txFn);
897
+ }
898
+ /**
899
+ * Run a managed read transaction scoped to a repository. Same isolation
900
+ * contract as `executeWrite`.
901
+ */
902
+ async executeRead(repositoryId, txFn) {
903
+ return this.runManaged("read", repositoryId, txFn);
904
+ }
905
+ /**
906
+ * Cross-repository escape hatch. Reserved for the small allowlist of
907
+ * legitimate cross-repository operations: `ensureSchema` (DDL),
908
+ * `listRepositories`, `_Meta` schema-version reads, server-info probes.
909
+ * Does NOT inject `$rid`. Every call site MUST add a one-line comment
910
+ * justifying why cross-repository access is correct (D3b).
911
+ */
912
+ async executeSystemQuery(cypher, params, options) {
913
+ if (options.crossRepository !== true) {
914
+ throw new import_deep_memory3.ProviderError(
915
+ "executeSystemQuery requires crossRepository: true \u2014 use executeQuery for repository-scoped Cypher."
916
+ );
917
+ }
918
+ const result = await this.driver.executeQuery(cypher, params, {
919
+ database: this.database,
920
+ ...options.routing !== void 0 ? { routing: options.routing } : {}
921
+ });
922
+ recordRoundTrip(result.summary, result.records.length);
923
+ this.surfaceNotifications(cypher, result.summary);
924
+ return result;
925
+ }
926
+ /**
927
+ * Run a single DDL statement on a session (auto-commit). Used for schema
928
+ * setup — `CREATE CONSTRAINT` / `CREATE INDEX` statements cannot run inside
929
+ * a managed transaction in Cypher 25 and must be issued one per call.
930
+ * Cross-repository by definition.
931
+ */
932
+ async executeSystemDdl(cypher) {
933
+ const session = this.driver.session({ database: this.database });
934
+ try {
935
+ const result = await session.run(cypher);
936
+ recordRoundTrip(result.summary, result.records.length);
937
+ this.surfaceNotifications(cypher, result.summary);
938
+ return result.summary;
939
+ } finally {
940
+ await session.close();
941
+ }
942
+ }
943
+ /**
944
+ * Run a repository-scoped Cypher statement that uses `CALL ( ) { ... } IN
945
+ * TRANSACTIONS OF $batchSize ROWS` semantics.
946
+ *
947
+ * `IN TRANSACTIONS` is only valid in implicit (auto-commit) transactions —
948
+ * the managed `executeWrite` / `executeRead` helpers open an explicit
949
+ * transaction internally and the server rejects the statement with
950
+ * `Neo.DatabaseError.Transaction.TransactionStartFailed`. Probe P13
951
+ * (local-tests/baseline/neo4j-call-in-transactions-results.md) captured the
952
+ * exact failure mode and the empty-match / partial-batch behaviour the
953
+ * chunked-wipe path relies on.
954
+ *
955
+ * Same `$rid` injection + scope assertion as `executeQuery`, so the chunked
956
+ * delete still flows through the D3b layer 2 lifeline. Returns only the
957
+ * summary because the chunked-wipe subquery yields no rows; counters live on
958
+ * `summary.counters.updates()`.
959
+ */
960
+ async executeImplicitInTransactions(cypher, params, options) {
961
+ this.assertRepositoryId(options.repositoryId);
962
+ this.assertScoped(cypher);
963
+ const session = this.driver.session({ database: this.database });
964
+ try {
965
+ const result = await session.run(cypher, { ...params, rid: options.repositoryId });
966
+ recordRoundTrip(result.summary, result.records.length);
967
+ this.surfaceNotifications(cypher, result.summary);
968
+ return result.summary;
969
+ } finally {
970
+ await session.close();
971
+ }
972
+ }
973
+ async runManaged(mode, repositoryId, txFn) {
974
+ this.assertRepositoryId(repositoryId);
975
+ const session = this.driver.session({ database: this.database });
976
+ try {
977
+ const wrapped = (managed) => txFn(new ScopedTransaction(managed, repositoryId, this.surfaceNotifications.bind(this)));
978
+ return mode === "write" ? await session.executeWrite(wrapped) : await session.executeRead(wrapped);
979
+ } finally {
980
+ await session.close();
981
+ }
982
+ }
983
+ assertRepositoryId(repositoryId) {
984
+ if (typeof repositoryId !== "string" || repositoryId.length === 0) {
985
+ throw new import_deep_memory3.ProviderError(
986
+ "Neo4jConnection: repositoryId is required for scoped queries (D3b isolation guarantee)."
987
+ );
988
+ }
989
+ }
990
+ assertScoped(cypher) {
991
+ if (!RID_TOKEN_PATTERN.test(cypher)) {
992
+ throw new import_deep_memory3.ProviderError(
993
+ "Neo4jConnection: Cypher omits required $rid binding. Repository-scoped queries MUST reference $rid in a predicate or property map. Use executeSystemQuery for cross-repository calls (D3b allowlist)."
994
+ );
995
+ }
996
+ }
997
+ surfaceNotifications(cypher, summary) {
998
+ const items = collectNonInformationNotifications(summary);
999
+ if (items.length === 0) return;
1000
+ const truncated = cypher.length > NOTIFICATION_QUERY_TRUNCATION ? `${cypher.slice(0, NOTIFICATION_QUERY_TRUNCATION)}\u2026` : cypher;
1001
+ console.warn("[neo4j] notifications", { cypher: truncated, notifications: items });
1002
+ }
1003
+ };
1004
+ var ScopedTransaction = class {
1005
+ constructor(tx, repositoryId, notify) {
1006
+ this.tx = tx;
1007
+ this.repositoryId = repositoryId;
1008
+ this.notify = notify;
1009
+ }
1010
+ tx;
1011
+ repositoryId;
1012
+ notify;
1013
+ /**
1014
+ * Run a single statement inside the managed transaction. Injects `$rid`
1015
+ * and asserts the Cypher string references it.
1016
+ */
1017
+ async run(cypher, params) {
1018
+ if (!RID_TOKEN_PATTERN.test(cypher)) {
1019
+ throw new import_deep_memory3.ProviderError(
1020
+ "ScopedTransaction.run: Cypher omits required $rid binding. Multi-statement writes still flow through the isolation chokepoint."
1021
+ );
1022
+ }
1023
+ const queryResult = await this.tx.run(cypher, { ...params, rid: this.repositoryId });
1024
+ const records = queryResult.records;
1025
+ const summary = queryResult.summary;
1026
+ recordRoundTrip(summary, records.length);
1027
+ this.notify(cypher, summary);
1028
+ return { records, summary };
1029
+ }
1030
+ };
1031
+ function collectNonInformationNotifications(summary) {
1032
+ const summaryLike = summary;
1033
+ const out = [];
1034
+ const seen = /* @__PURE__ */ new Set();
1035
+ for (const item of summaryLike.notifications ?? []) {
1036
+ const entry = {
1037
+ severity: item.severity ?? "",
1038
+ category: item.category ?? "",
1039
+ code: item.code ?? "",
1040
+ title: item.title ?? "",
1041
+ description: item.description ?? ""
1042
+ };
1043
+ const key = entry.code || entry.title;
1044
+ if (key && seen.has(key)) continue;
1045
+ if (key) seen.add(key);
1046
+ out.push(entry);
1047
+ }
1048
+ for (const item of summaryLike.gqlStatusObjects ?? []) {
1049
+ const severity = item.severity ?? "";
1050
+ if (severity !== "WARNING" && severity !== "ERROR") continue;
1051
+ const entry = {
1052
+ severity,
1053
+ category: item.classification ?? "",
1054
+ code: item.gqlStatus ?? "",
1055
+ title: item.title ?? "",
1056
+ description: item.description ?? item.statusDescription ?? ""
1057
+ };
1058
+ const key = entry.code || entry.title || entry.description;
1059
+ if (key && seen.has(key)) continue;
1060
+ if (key) seen.add(key);
1061
+ out.push(entry);
1062
+ }
1063
+ return out;
1064
+ }
1065
+
1066
+ // src/errors.ts
1067
+ var import_deep_memory4 = require("@utaba/deep-memory");
1068
+ var CONSTRAINT_VIOLATION_CODE = "Neo.ClientError.Schema.ConstraintValidationFailed";
1069
+ var SYNTAX_ERROR_CODE = "Neo.ClientError.Statement.SyntaxError";
1070
+ function mapDriverError(error, context = {}) {
1071
+ if (error instanceof import_deep_memory4.DuplicateEntityError || error instanceof import_deep_memory4.DuplicateRelationshipError || error instanceof import_deep_memory4.DuplicateRepositoryError || error instanceof import_deep_memory4.ProviderError) {
1072
+ throw error;
1073
+ }
1074
+ const driverError = error ?? {};
1075
+ const code = typeof driverError.code === "string" ? driverError.code : "";
1076
+ const message = typeof driverError.message === "string" ? driverError.message : "";
1077
+ if (code === CONSTRAINT_VIOLATION_CODE) {
1078
+ const kind = inferConstraintKind(context, message);
1079
+ if (kind === "repository" && context.repositoryId !== void 0) {
1080
+ throw new import_deep_memory4.DuplicateRepositoryError(context.repositoryId);
1081
+ }
1082
+ if (kind === "entity" && context.entityId !== void 0) {
1083
+ throw new import_deep_memory4.DuplicateEntityError(context.entityId);
1084
+ }
1085
+ if (kind === "relationship" && context.relationshipId !== void 0) {
1086
+ throw new import_deep_memory4.DuplicateRelationshipError(context.relationshipId);
1087
+ }
1088
+ throw new import_deep_memory4.ProviderError(
1089
+ `Neo4j constraint violation${formatOperation(context)}: ${message || code}`,
1090
+ "Inspect the failing Cypher and the schema constraints \u2014 the affected unique key already exists."
1091
+ );
1092
+ }
1093
+ if (code === SYNTAX_ERROR_CODE) {
1094
+ throw new import_deep_memory4.ProviderError(
1095
+ `Neo4j syntax error${formatOperation(context)}: ${message || code}`,
1096
+ "This is a programming error inside @utaba/deep-memory-storage-neo4j \u2014 please file an issue."
1097
+ );
1098
+ }
1099
+ const prefix = `Neo4j driver error${formatOperation(context)}`;
1100
+ const codeSuffix = code ? ` [${code}]` : "";
1101
+ const messageSuffix = message ? `: ${message}` : "";
1102
+ throw new import_deep_memory4.ProviderError(`${prefix}${codeSuffix}${messageSuffix}`);
1103
+ }
1104
+ function inferConstraintKind(context, message) {
1105
+ if (context.kind !== void 0) return context.kind;
1106
+ if (message.startsWith("Relationship")) return "relationship";
1107
+ if (message.includes("`_Repository`")) return "repository";
1108
+ return "entity";
1109
+ }
1110
+ function formatOperation(context) {
1111
+ return context.operation ? ` in ${context.operation}` : "";
1112
+ }
1113
+
1114
+ // src/queries/bulk.ts
1115
+ var import_deep_memory5 = require("@utaba/deep-memory");
1116
+ var EXPORT_BATCH_SIZE = 100;
1117
+ var DEFAULT_IMPORT_CHUNK_SIZE = 500;
1118
+ var DEFAULT_IMPORT_CONCURRENCY = 8;
1119
+ var EXPORT_ENTITY_PROJECTION = buildEntityProjection({ loadEmbeddings: true });
1120
+ var EXPORT_RELATIONSHIP_PROJECTION = buildRelationshipProjection();
1121
+ var EXPORT_ENTITIES_FIRST_PAGE = `
1122
+ MATCH (n:_Entity {repositoryId: $rid})
1123
+ RETURN ${EXPORT_ENTITY_PROJECTION}
1124
+ ORDER BY n.id
1125
+ LIMIT $batchSize
1126
+ `;
1127
+ var EXPORT_ENTITIES_NEXT_PAGE = `
1128
+ MATCH (n:_Entity {repositoryId: $rid})
1129
+ WHERE n.id > $cursor
1130
+ RETURN ${EXPORT_ENTITY_PROJECTION}
1131
+ ORDER BY n.id
1132
+ LIMIT $batchSize
1133
+ `;
1134
+ var EXPORT_RELATIONSHIPS_FIRST_PAGE = `
1135
+ MATCH (:_Entity {repositoryId: $rid})-[r {repositoryId: $rid}]->(:_Entity {repositoryId: $rid})
1136
+ RETURN ${EXPORT_RELATIONSHIP_PROJECTION}
1137
+ ORDER BY r.id
1138
+ LIMIT $batchSize
1139
+ `;
1140
+ var EXPORT_RELATIONSHIPS_NEXT_PAGE = `
1141
+ MATCH (:_Entity {repositoryId: $rid})-[r {repositoryId: $rid}]->(:_Entity {repositoryId: $rid})
1142
+ WHERE r.id > $cursor
1143
+ RETURN ${EXPORT_RELATIONSHIP_PROJECTION}
1144
+ ORDER BY r.id
1145
+ LIMIT $batchSize
1146
+ `;
1147
+ async function* exportAll(conn, repositoryId) {
1148
+ let sequence = 0;
1149
+ let cursor;
1150
+ while (true) {
1151
+ const query = cursor === void 0 ? EXPORT_ENTITIES_FIRST_PAGE : EXPORT_ENTITIES_NEXT_PAGE;
1152
+ const params = cursor === void 0 ? { batchSize: BigInt(EXPORT_BATCH_SIZE) } : { cursor, batchSize: BigInt(EXPORT_BATCH_SIZE) };
1153
+ const result = await conn.executeQuery(query, params, { repositoryId, routing: "READ" });
1154
+ const entities = result.records.map((record) => entityFromRecord(record));
1155
+ const isLast = entities.length < EXPORT_BATCH_SIZE;
1156
+ if (entities.length > 0) {
1157
+ cursor = entities[entities.length - 1].id;
1158
+ yield {
1159
+ type: "entities",
1160
+ data: entities,
1161
+ sequence: sequence++,
1162
+ isLast
1163
+ };
1164
+ }
1165
+ if (isLast) break;
1166
+ }
1167
+ let relCursor;
1168
+ while (true) {
1169
+ const query = relCursor === void 0 ? EXPORT_RELATIONSHIPS_FIRST_PAGE : EXPORT_RELATIONSHIPS_NEXT_PAGE;
1170
+ const params = relCursor === void 0 ? { batchSize: BigInt(EXPORT_BATCH_SIZE) } : { cursor: relCursor, batchSize: BigInt(EXPORT_BATCH_SIZE) };
1171
+ const result = await conn.executeQuery(query, params, { repositoryId, routing: "READ" });
1172
+ const relationships = result.records.map((record) => relationshipFromRecord(record));
1173
+ const isLast = relationships.length < EXPORT_BATCH_SIZE;
1174
+ if (relationships.length > 0) {
1175
+ relCursor = relationships[relationships.length - 1].id;
1176
+ yield {
1177
+ type: "relationships",
1178
+ data: relationships,
1179
+ sequence: sequence++,
1180
+ isLast
1181
+ };
1182
+ }
1183
+ if (isLast) break;
1184
+ }
1185
+ if (sequence === 0) {
1186
+ yield {
1187
+ type: "entities",
1188
+ data: [],
1189
+ sequence: 0,
1190
+ isLast: true
1191
+ };
1192
+ }
1193
+ }
1194
+ var INSERT_ENTITIES_QUERY = `
1195
+ UNWIND $rows AS row
1196
+ CREATE (n:_Entity)
1197
+ SET
1198
+ n.id = row.id,
1199
+ n.repositoryId = $rid,
1200
+ n.entityType = row.entityType,
1201
+ n.label = row.label,
1202
+ n.slug = row.slug,
1203
+ n.summary = row.summary,
1204
+ n.properties = row.properties,
1205
+ n.data = row.data,
1206
+ n.dataFormat = row.dataFormat,
1207
+ n.embedding = row.embedding,
1208
+ n.createdBy = row.createdBy,
1209
+ n.createdByType = row.createdByType,
1210
+ n.createdAt = row.createdAt,
1211
+ n.createdInConversation = row.createdInConversation,
1212
+ n.createdFromMessage = row.createdFromMessage,
1213
+ n.modifiedBy = row.modifiedBy,
1214
+ n.modifiedByType = row.modifiedByType,
1215
+ n.modifiedAt = row.modifiedAt,
1216
+ n.modifiedInConversation = row.modifiedInConversation,
1217
+ n.modifiedFromMessage = row.modifiedFromMessage
1218
+ SET n += row.userProperties
1219
+ `;
1220
+ var UPSERT_ENTITIES_QUERY = `
1221
+ UNWIND $rows AS row
1222
+ MERGE (n:_Entity {repositoryId: $rid, id: row.id})
1223
+ ON CREATE SET
1224
+ n.entityType = row.entityType,
1225
+ n.label = row.label,
1226
+ n.slug = row.slug,
1227
+ n.summary = row.summary,
1228
+ n.properties = row.properties,
1229
+ n.data = row.data,
1230
+ n.dataFormat = row.dataFormat,
1231
+ n.embedding = row.embedding,
1232
+ n.createdBy = row.createdBy,
1233
+ n.createdByType = row.createdByType,
1234
+ n.createdAt = row.createdAt,
1235
+ n.createdInConversation = row.createdInConversation,
1236
+ n.createdFromMessage = row.createdFromMessage,
1237
+ n.modifiedBy = row.modifiedBy,
1238
+ n.modifiedByType = row.modifiedByType,
1239
+ n.modifiedAt = row.modifiedAt,
1240
+ n.modifiedInConversation = row.modifiedInConversation,
1241
+ n.modifiedFromMessage = row.modifiedFromMessage
1242
+ ON MATCH SET
1243
+ n.entityType = row.entityType,
1244
+ n.label = row.label,
1245
+ n.slug = row.slug,
1246
+ n.summary = row.summary,
1247
+ n.properties = row.properties,
1248
+ n.data = row.data,
1249
+ n.dataFormat = row.dataFormat,
1250
+ n.embedding = row.embedding,
1251
+ n.createdBy = row.createdBy,
1252
+ n.createdByType = row.createdByType,
1253
+ n.createdAt = row.createdAt,
1254
+ n.createdInConversation = row.createdInConversation,
1255
+ n.createdFromMessage = row.createdFromMessage,
1256
+ n.modifiedBy = row.modifiedBy,
1257
+ n.modifiedByType = row.modifiedByType,
1258
+ n.modifiedAt = row.modifiedAt,
1259
+ n.modifiedInConversation = row.modifiedInConversation,
1260
+ n.modifiedFromMessage = row.modifiedFromMessage
1261
+ SET n += row.userProperties
1262
+ `;
1263
+ function buildInsertRelationshipsQuery(relationshipType) {
1264
+ const safe = assertSafeRelationshipType(relationshipType);
1265
+ return `
1266
+ UNWIND $rows AS row
1267
+ MATCH (s:_Entity {repositoryId: $rid, id: row.sourceEntityId})
1268
+ MATCH (t:_Entity {repositoryId: $rid, id: row.targetEntityId})
1269
+ CREATE (s)-[r:${safe} {
1270
+ repositoryId: $rid,
1271
+ id: row.id,
1272
+ relationshipType: row.relationshipType,
1273
+ sourceEntityId: row.sourceEntityId,
1274
+ targetEntityId: row.targetEntityId,
1275
+ properties: row.properties,
1276
+ bidirectional: row.bidirectional,
1277
+ createdBy: row.createdBy,
1278
+ createdByType: row.createdByType,
1279
+ createdAt: row.createdAt,
1280
+ createdInConversation: row.createdInConversation,
1281
+ createdFromMessage: row.createdFromMessage,
1282
+ modifiedBy: row.modifiedBy,
1283
+ modifiedByType: row.modifiedByType,
1284
+ modifiedAt: row.modifiedAt,
1285
+ modifiedInConversation: row.modifiedInConversation,
1286
+ modifiedFromMessage: row.modifiedFromMessage
1287
+ }]->(t)
1288
+ RETURN row.id AS id
1289
+ `;
1290
+ }
1291
+ function buildUpsertRelationshipsQuery(relationshipType) {
1292
+ const safe = assertSafeRelationshipType(relationshipType);
1293
+ return `
1294
+ UNWIND $rows AS row
1295
+ MATCH (s:_Entity {repositoryId: $rid, id: row.sourceEntityId})
1296
+ MATCH (t:_Entity {repositoryId: $rid, id: row.targetEntityId})
1297
+ MERGE (s)-[r:${safe} {repositoryId: $rid, id: row.id}]->(t)
1298
+ ON CREATE SET
1299
+ r.relationshipType = row.relationshipType,
1300
+ r.sourceEntityId = row.sourceEntityId,
1301
+ r.targetEntityId = row.targetEntityId,
1302
+ r.properties = row.properties,
1303
+ r.bidirectional = row.bidirectional,
1304
+ r.createdBy = row.createdBy,
1305
+ r.createdByType = row.createdByType,
1306
+ r.createdAt = row.createdAt,
1307
+ r.createdInConversation = row.createdInConversation,
1308
+ r.createdFromMessage = row.createdFromMessage,
1309
+ r.modifiedBy = row.modifiedBy,
1310
+ r.modifiedByType = row.modifiedByType,
1311
+ r.modifiedAt = row.modifiedAt,
1312
+ r.modifiedInConversation = row.modifiedInConversation,
1313
+ r.modifiedFromMessage = row.modifiedFromMessage
1314
+ ON MATCH SET
1315
+ r.properties = row.properties,
1316
+ r.bidirectional = row.bidirectional,
1317
+ r.modifiedBy = row.modifiedBy,
1318
+ r.modifiedByType = row.modifiedByType,
1319
+ r.modifiedAt = row.modifiedAt,
1320
+ r.modifiedInConversation = row.modifiedInConversation,
1321
+ r.modifiedFromMessage = row.modifiedFromMessage
1322
+ RETURN row.id AS id
1323
+ `;
1324
+ }
1325
+ async function importBulk(conn, repositoryId, data, options) {
1326
+ const skipCheck = options?.skipExistenceCheck === true;
1327
+ const ext = options ?? {};
1328
+ const chunkSize = ext.chunkSize ?? DEFAULT_IMPORT_CHUNK_SIZE;
1329
+ const concurrency = ext.concurrency ?? DEFAULT_IMPORT_CONCURRENCY;
1330
+ const allEntities = [];
1331
+ const allRelationships = [];
1332
+ for (const chunk of data) {
1333
+ if (chunk.entities) allEntities.push(...chunk.entities);
1334
+ if (chunk.relationships) allRelationships.push(...chunk.relationships);
1335
+ }
1336
+ const errors = [];
1337
+ const entityChunks = sliceIntoChunks(allEntities, chunkSize);
1338
+ const entityResults = await runBounded(
1339
+ entityChunks,
1340
+ concurrency,
1341
+ (chunk) => importEntityChunk(conn, repositoryId, chunk, skipCheck)
1342
+ );
1343
+ let entitiesImported = 0;
1344
+ for (const res of entityResults) {
1345
+ entitiesImported += res.imported;
1346
+ errors.push(...res.errors);
1347
+ }
1348
+ const relationshipChunks = groupRelationshipsByTypeIntoChunks(allRelationships, chunkSize);
1349
+ const relationshipResults = await runBounded(
1350
+ relationshipChunks,
1351
+ concurrency,
1352
+ (group) => importRelationshipChunk(conn, repositoryId, group, skipCheck)
1353
+ );
1354
+ let relationshipsImported = 0;
1355
+ for (const res of relationshipResults) {
1356
+ relationshipsImported += res.imported;
1357
+ errors.push(...res.errors);
1358
+ }
1359
+ return { entitiesImported, relationshipsImported, errors };
1360
+ }
1361
+ async function importEntityChunk(conn, repositoryId, entities, skipCheck) {
1362
+ if (entities.length === 0) return { imported: 0, errors: [] };
1363
+ const rows = entities.map((entity) => ({
1364
+ ...entityToParams(entity),
1365
+ userProperties: entityUserPropertyParams(entity.properties)
1366
+ }));
1367
+ const query = skipCheck ? INSERT_ENTITIES_QUERY : UPSERT_ENTITIES_QUERY;
1368
+ try {
1369
+ await conn.executeQuery(query, { rows }, { repositoryId });
1370
+ return { imported: entities.length, errors: [] };
1371
+ } catch (err) {
1372
+ return fallbackPerEntity(conn, repositoryId, entities, skipCheck, err);
1373
+ }
1374
+ }
1375
+ async function fallbackPerEntity(conn, repositoryId, entities, skipCheck, chunkError) {
1376
+ const errors = [];
1377
+ let imported = 0;
1378
+ const query = skipCheck ? INSERT_ENTITIES_QUERY : UPSERT_ENTITIES_QUERY;
1379
+ for (const entity of entities) {
1380
+ try {
1381
+ const rows = [
1382
+ {
1383
+ ...entityToParams(entity),
1384
+ userProperties: entityUserPropertyParams(entity.properties)
1385
+ }
1386
+ ];
1387
+ await conn.executeQuery(query, { rows }, { repositoryId });
1388
+ imported++;
1389
+ } catch (rowErr) {
1390
+ errors.push({
1391
+ item: `entity:${entity.id}`,
1392
+ error: rowErr instanceof Error ? rowErr.message : String(rowErr)
1393
+ });
1394
+ }
1395
+ }
1396
+ if (errors.length === 0 && imported < entities.length) {
1397
+ errors.push({
1398
+ item: `entity-chunk`,
1399
+ error: chunkError instanceof Error ? chunkError.message : String(chunkError)
1400
+ });
1401
+ }
1402
+ return { imported, errors };
1403
+ }
1404
+ async function importRelationshipChunk(conn, repositoryId, group, skipCheck) {
1405
+ if (group.rows.length === 0) return { imported: 0, errors: [] };
1406
+ const rows = group.rows.map((rel) => relationshipToParams(rel));
1407
+ const query = skipCheck ? buildInsertRelationshipsQuery(group.relationshipType) : buildUpsertRelationshipsQuery(group.relationshipType);
1408
+ try {
1409
+ const result = await conn.executeQuery(query, { rows }, { repositoryId });
1410
+ const importedIds = /* @__PURE__ */ new Set();
1411
+ for (const record of result.records) {
1412
+ const id = record.get("id");
1413
+ if (typeof id === "string") importedIds.add(id);
1414
+ }
1415
+ const errors = [];
1416
+ for (const rel of group.rows) {
1417
+ if (!importedIds.has(rel.id)) {
1418
+ errors.push({
1419
+ item: `relationship:${rel.id}`,
1420
+ error: `endpoint not found in repository (source=${rel.sourceEntityId}, target=${rel.targetEntityId})`
1421
+ });
1422
+ }
1423
+ }
1424
+ return { imported: importedIds.size, errors };
1425
+ } catch (err) {
1426
+ return fallbackPerRelationship(conn, repositoryId, group, skipCheck, err);
1427
+ }
1428
+ }
1429
+ async function fallbackPerRelationship(conn, repositoryId, group, skipCheck, chunkError) {
1430
+ const errors = [];
1431
+ let imported = 0;
1432
+ const query = skipCheck ? buildInsertRelationshipsQuery(group.relationshipType) : buildUpsertRelationshipsQuery(group.relationshipType);
1433
+ for (const rel of group.rows) {
1434
+ try {
1435
+ const rows = [relationshipToParams(rel)];
1436
+ const result = await conn.executeQuery(query, { rows }, { repositoryId });
1437
+ if (result.records.length === 1) {
1438
+ imported++;
1439
+ } else {
1440
+ errors.push({
1441
+ item: `relationship:${rel.id}`,
1442
+ error: `endpoint not found in repository (source=${rel.sourceEntityId}, target=${rel.targetEntityId})`
1443
+ });
1444
+ }
1445
+ } catch (rowErr) {
1446
+ errors.push({
1447
+ item: `relationship:${rel.id}`,
1448
+ error: rowErr instanceof Error ? rowErr.message : String(rowErr)
1449
+ });
1450
+ }
1451
+ }
1452
+ if (errors.length === 0 && imported < group.rows.length) {
1453
+ errors.push({
1454
+ item: `relationship-chunk:${group.relationshipType}`,
1455
+ error: chunkError instanceof Error ? chunkError.message : String(chunkError)
1456
+ });
1457
+ }
1458
+ return { imported, errors };
1459
+ }
1460
+ function sliceIntoChunks(items, chunkSize) {
1461
+ if (items.length === 0) return [];
1462
+ const out = [];
1463
+ for (let i = 0; i < items.length; i += chunkSize) {
1464
+ out.push(items.slice(i, i + chunkSize));
1465
+ }
1466
+ return out;
1467
+ }
1468
+ function groupRelationshipsByTypeIntoChunks(relationships, chunkSize) {
1469
+ const grouped = /* @__PURE__ */ new Map();
1470
+ for (const rel of relationships) {
1471
+ const list = grouped.get(rel.relationshipType);
1472
+ if (list) list.push(rel);
1473
+ else grouped.set(rel.relationshipType, [rel]);
1474
+ }
1475
+ const out = [];
1476
+ for (const [relationshipType, rows] of grouped) {
1477
+ for (let i = 0; i < rows.length; i += chunkSize) {
1478
+ out.push({ relationshipType, rows: rows.slice(i, i + chunkSize) });
1479
+ }
1480
+ }
1481
+ return out;
1482
+ }
1483
+ async function runBounded(items, concurrency, fn) {
1484
+ if (items.length === 0) return [];
1485
+ if (concurrency < 1) {
1486
+ throw new import_deep_memory5.ProviderError(`runBounded: concurrency must be >= 1 (got ${concurrency}).`);
1487
+ }
1488
+ const cap = Math.min(concurrency, items.length);
1489
+ const results = new Array(items.length);
1490
+ let nextIndex = 0;
1491
+ const workers = [];
1492
+ for (let w = 0; w < cap; w++) {
1493
+ workers.push(
1494
+ (async () => {
1495
+ while (true) {
1496
+ const i = nextIndex++;
1497
+ if (i >= items.length) return;
1498
+ results[i] = await fn(items[i]);
1499
+ }
1500
+ })()
1501
+ );
1502
+ }
1503
+ await Promise.all(workers);
1504
+ return results;
1505
+ }
1506
+
1507
+ // src/queries/entity.ts
1508
+ var import_deep_memory6 = require("@utaba/deep-memory");
1509
+ var ENTITY_CREATE_QUERY = `
1510
+ CREATE (n:_Entity {
1511
+ repositoryId: $rid,
1512
+ id: $id,
1513
+ entityType: $entityType,
1514
+ label: $label,
1515
+ slug: $slug,
1516
+ summary: $summary,
1517
+ properties: $properties,
1518
+ data: $data,
1519
+ dataFormat: $dataFormat,
1520
+ embedding: $embedding,
1521
+ createdBy: $createdBy,
1522
+ createdByType: $createdByType,
1523
+ createdAt: $createdAt,
1524
+ createdInConversation: $createdInConversation,
1525
+ createdFromMessage: $createdFromMessage,
1526
+ modifiedBy: $modifiedBy,
1527
+ modifiedByType: $modifiedByType,
1528
+ modifiedAt: $modifiedAt,
1529
+ modifiedInConversation: $modifiedInConversation,
1530
+ modifiedFromMessage: $modifiedFromMessage
1531
+ })
1532
+ SET n += $userProperties
1533
+ RETURN n.id AS id
1534
+ `;
1535
+ var ENTITY_PROJECTION_LIGHT = buildEntityProjection();
1536
+ var ENTITY_PROJECTION_FULL = buildEntityProjection({ loadEmbeddings: true });
1537
+ var ENTITY_GET_QUERY_LIGHT = `MATCH (n:_Entity {repositoryId: $rid, id: $id}) RETURN ${ENTITY_PROJECTION_LIGHT}`;
1538
+ var ENTITY_GET_QUERY_FULL = `MATCH (n:_Entity {repositoryId: $rid, id: $id}) RETURN ${ENTITY_PROJECTION_FULL}`;
1539
+ var ENTITY_GET_BY_SLUG_QUERY_LIGHT = `MATCH (n:_Entity {repositoryId: $rid, slug: $slug}) RETURN ${ENTITY_PROJECTION_LIGHT}`;
1540
+ var ENTITY_GET_BY_SLUG_QUERY_FULL = `MATCH (n:_Entity {repositoryId: $rid, slug: $slug}) RETURN ${ENTITY_PROJECTION_FULL}`;
1541
+ var ENTITY_GET_MANY_QUERY_LIGHT = `MATCH (n:_Entity {repositoryId: $rid}) WHERE n.id IN $ids RETURN ${ENTITY_PROJECTION_LIGHT}`;
1542
+ var ENTITY_GET_MANY_QUERY_FULL = `MATCH (n:_Entity {repositoryId: $rid}) WHERE n.id IN $ids RETURN ${ENTITY_PROJECTION_FULL}`;
1543
+ async function createEntity(conn, repositoryId, entity) {
1544
+ const userProperties = entityUserPropertyParams(entity.properties);
1545
+ try {
1546
+ await conn.executeQuery(
1547
+ ENTITY_CREATE_QUERY,
1548
+ { ...entityToParams(entity), userProperties },
1549
+ { repositoryId }
1550
+ );
1551
+ } catch (err) {
1552
+ mapDriverError(err, {
1553
+ kind: "entity",
1554
+ entityId: entity.id,
1555
+ operation: "createEntity"
1556
+ });
1557
+ }
1558
+ return entity;
1559
+ }
1560
+ async function getEntity(conn, repositoryId, entityId, options) {
1561
+ const query = options?.loadEmbeddings === true ? ENTITY_GET_QUERY_FULL : ENTITY_GET_QUERY_LIGHT;
1562
+ const result = await conn.executeQuery(
1563
+ query,
1564
+ { id: entityId },
1565
+ { repositoryId, routing: "READ" }
1566
+ );
1567
+ const record = result.records[0];
1568
+ if (record === void 0) return null;
1569
+ return entityFromRecord(record);
1570
+ }
1571
+ async function getEntityBySlug(conn, repositoryId, slug, options) {
1572
+ const query = options?.loadEmbeddings === true ? ENTITY_GET_BY_SLUG_QUERY_FULL : ENTITY_GET_BY_SLUG_QUERY_LIGHT;
1573
+ const result = await conn.executeQuery(
1574
+ query,
1575
+ { slug },
1576
+ { repositoryId, routing: "READ" }
1577
+ );
1578
+ const record = result.records[0];
1579
+ if (record === void 0) return null;
1580
+ return entityFromRecord(record);
1581
+ }
1582
+ async function getEntities(conn, repositoryId, entityIds, options) {
1583
+ const map = /* @__PURE__ */ new Map();
1584
+ if (entityIds.length === 0) return map;
1585
+ const query = options?.loadEmbeddings === true ? ENTITY_GET_MANY_QUERY_FULL : ENTITY_GET_MANY_QUERY_LIGHT;
1586
+ const result = await conn.executeQuery(
1587
+ query,
1588
+ { ids: entityIds },
1589
+ { repositoryId, routing: "READ" }
1590
+ );
1591
+ for (const record of result.records) {
1592
+ const entity = entityFromRecord(record);
1593
+ map.set(entity.id, entity);
1594
+ }
1595
+ return map;
1596
+ }
1597
+ async function updateEntity(conn, repositoryId, entityId, updates) {
1598
+ let userProperties = null;
1599
+ if (updates.properties !== void 0) {
1600
+ userProperties = entityUserPropertyParams(updates.properties);
1601
+ }
1602
+ let keysToRemove = [];
1603
+ if (userProperties !== null) {
1604
+ const readResult = await conn.executeQuery(
1605
+ "MATCH (n:_Entity {repositoryId: $rid, id: $id}) RETURN properties(n) AS props",
1606
+ { id: entityId },
1607
+ { repositoryId, routing: "READ" }
1608
+ );
1609
+ const readRecord = readResult.records[0];
1610
+ if (readRecord === void 0) throw new import_deep_memory6.EntityNotFoundError(entityId);
1611
+ const props = readRecord.get("props");
1612
+ const existingProps = typeof props === "object" && props !== null && !Array.isArray(props) ? props : {};
1613
+ const existingUserKeys = Object.keys(existingProps).filter(
1614
+ (k) => !RESERVED_ENTITY_PROPERTY_KEYS.has(k)
1615
+ );
1616
+ keysToRemove = existingUserKeys.filter((k) => !(k in userProperties));
1617
+ }
1618
+ const setParts = [];
1619
+ const params = { id: entityId };
1620
+ const setField = (key, paramName, value) => {
1621
+ setParts.push(`n.${key} = $${paramName}`);
1622
+ params[paramName] = value;
1623
+ };
1624
+ if (updates.entityType !== void 0) setField("entityType", "entityType", updates.entityType);
1625
+ if (updates.label !== void 0) setField("label", "label", updates.label);
1626
+ if (updates.slug !== void 0) setField("slug", "slug", updates.slug);
1627
+ if (updates.summary !== void 0) setField("summary", "summary", updates.summary);
1628
+ if (updates.properties !== void 0) {
1629
+ setField("properties", "properties", JSON.stringify(updates.properties));
1630
+ }
1631
+ if (updates.data !== void 0) setField("data", "data", updates.data);
1632
+ if (updates.dataFormat !== void 0) setField("dataFormat", "dataFormat", updates.dataFormat);
1633
+ if (updates.embedding !== void 0) setField("embedding", "embedding", updates.embedding);
1634
+ const p = updates.provenance;
1635
+ setField("modifiedBy", "modifiedBy", p.modifiedBy);
1636
+ setField("modifiedByType", "modifiedByType", p.modifiedByType);
1637
+ setField("modifiedAt", "modifiedAt", p.modifiedAt);
1638
+ if (p.modifiedInConversation !== void 0) {
1639
+ setField("modifiedInConversation", "modifiedInConversation", p.modifiedInConversation);
1640
+ }
1641
+ if (p.modifiedFromMessage !== void 0) {
1642
+ setField("modifiedFromMessage", "modifiedFromMessage", p.modifiedFromMessage);
1643
+ }
1644
+ let userPropsClause = "";
1645
+ if (userProperties !== null) {
1646
+ params["userProperties"] = userProperties;
1647
+ userPropsClause = " SET n += $userProperties";
1648
+ }
1649
+ let removeClause = "";
1650
+ if (keysToRemove.length > 0) {
1651
+ const safeKeys = keysToRemove.map((k) => `n.${assertSafeUserPropertyKey(k)}`);
1652
+ removeClause = ` REMOVE ${safeKeys.join(", ")}`;
1653
+ }
1654
+ const cypher = `MATCH (n:_Entity {repositoryId: $rid, id: $id}) SET ${setParts.join(", ")}${userPropsClause}${removeClause} RETURN ${ENTITY_PROJECTION_LIGHT}`;
1655
+ const result = await conn.executeQuery(cypher, params, { repositoryId });
1656
+ const record = result.records[0];
1657
+ if (record === void 0) throw new import_deep_memory6.EntityNotFoundError(entityId);
1658
+ return entityFromRecord(record);
1659
+ }
1660
+ async function deleteEntity(conn, repositoryId, entityId) {
1661
+ const result = await conn.executeQuery(
1662
+ "MATCH (n:_Entity {repositoryId: $rid, id: $id}) DETACH DELETE n RETURN count(n) AS deleted",
1663
+ { id: entityId },
1664
+ { repositoryId }
1665
+ );
1666
+ const deleted = bigintToSafeNumber(result.records[0]?.get("deleted") ?? 0);
1667
+ if (deleted !== 1) throw new import_deep_memory6.EntityNotFoundError(entityId);
1668
+ }
1669
+ async function deleteEntities(conn, repositoryId, ids) {
1670
+ if (ids.length === 0) return { deleted: [], notFound: [] };
1671
+ const result = await conn.executeQuery(
1672
+ "MATCH (n:_Entity {repositoryId: $rid}) WHERE n.id IN $ids WITH n, n.id AS id DETACH DELETE n RETURN id AS deleted",
1673
+ { ids },
1674
+ { repositoryId }
1675
+ );
1676
+ const deleted = [];
1677
+ for (const record of result.records) {
1678
+ const id = record.get("deleted");
1679
+ if (typeof id === "string") deleted.push(id);
1680
+ }
1681
+ const deletedSet = new Set(deleted);
1682
+ const notFound = ids.filter((id) => !deletedSet.has(id));
1683
+ return { deleted, notFound };
1684
+ }
1685
+ var FIND_PROJECTION_LIGHT = buildEntityProjection({ alias: "n" });
1686
+ var FIND_PROJECTION_FULL = buildEntityProjection({ alias: "n", loadEmbeddings: true });
1687
+ var FIND_PROJECTION_LIGHT_FT = buildEntityProjection({ alias: "node" });
1688
+ var FIND_PROJECTION_FULL_FT = buildEntityProjection({ alias: "node", loadEmbeddings: true });
1689
+ function buildFindEntitiesWhere(query, options) {
1690
+ const { alias, includeRepositoryPredicate } = options;
1691
+ const params = {};
1692
+ const predicates = [];
1693
+ if (includeRepositoryPredicate) {
1694
+ predicates.push(`${alias}.repositoryId = $rid`);
1695
+ }
1696
+ if (query.entityTypes && query.entityTypes.length > 0) {
1697
+ predicates.push(`${alias}.entityType IN $entityTypes`);
1698
+ params["entityTypes"] = query.entityTypes;
1699
+ }
1700
+ if (query.properties) {
1701
+ let i = 0;
1702
+ for (const [key, value] of Object.entries(query.properties)) {
1703
+ assertSafeUserPropertyKey(key);
1704
+ if (!isNativeStorableValue(value)) {
1705
+ throw new import_deep_memory6.ProviderError(
1706
+ `findEntities property filter "${key}" has a value that Neo4j cannot store as a native scalar \u2014 filters must be strings, finite numbers, booleans, or homogeneous arrays of those. Nested objects and null values live only inside the JSON properties blob and are not predicate-queryable.`
1707
+ );
1708
+ }
1709
+ const paramName = `prop${i}`;
1710
+ predicates.push(`${alias}.${key} = $${paramName}`);
1711
+ params[paramName] = value;
1712
+ i++;
1713
+ }
1714
+ }
1715
+ if (query.provenance) {
1716
+ if (query.provenance.conversationIds && query.provenance.conversationIds.length > 0) {
1717
+ predicates.push(
1718
+ `(${alias}.createdInConversation IN $convIds OR ${alias}.modifiedInConversation IN $convIds)`
1719
+ );
1720
+ params["convIds"] = query.provenance.conversationIds;
1721
+ }
1722
+ if (query.provenance.actors && query.provenance.actors.length > 0) {
1723
+ predicates.push(
1724
+ `(${alias}.createdBy IN $actors OR ${alias}.modifiedBy IN $actors)`
1725
+ );
1726
+ params["actors"] = query.provenance.actors;
1727
+ }
1728
+ if (query.provenance.dateRange) {
1729
+ predicates.push(
1730
+ `((${alias}.createdAt >= $dateFrom AND ${alias}.createdAt <= $dateTo) OR (${alias}.modifiedAt >= $dateFrom AND ${alias}.modifiedAt <= $dateTo))`
1731
+ );
1732
+ params["dateFrom"] = query.provenance.dateRange.from;
1733
+ params["dateTo"] = query.provenance.dateRange.to;
1734
+ }
1735
+ }
1736
+ const cypherWhere = predicates.length > 0 ? `WHERE ${predicates.join(" AND ")}` : "";
1737
+ return { cypherWhere, params };
1738
+ }
1739
+ async function findEntities(conn, repositoryId, query, options) {
1740
+ const loadEmbeddings = options?.loadEmbeddings === true;
1741
+ const skipLimitParams = {
1742
+ skip: BigInt(query.offset),
1743
+ limit: BigInt(query.limit)
1744
+ };
1745
+ let records;
1746
+ let countResult;
1747
+ if (query.searchTerm !== void 0 && query.searchTerm !== "") {
1748
+ const projection = loadEmbeddings ? FIND_PROJECTION_FULL_FT : FIND_PROJECTION_LIGHT_FT;
1749
+ const where = buildFindEntitiesWhere(query, {
1750
+ alias: "node",
1751
+ includeRepositoryPredicate: false
1752
+ });
1753
+ const repoPredicate = "node.repositoryId = $rid";
1754
+ const combinedWhere = where.cypherWhere.length > 0 ? `WHERE ${repoPredicate} AND ${where.cypherWhere.slice("WHERE ".length)}` : `WHERE ${repoPredicate}`;
1755
+ const dataCypher = `CALL db.index.fulltext.queryNodes('dm_entity_text', $term) YIELD node, score ${combinedWhere} RETURN ${projection} ORDER BY score DESC SKIP $skip LIMIT $limit`;
1756
+ const countCypher = `CALL db.index.fulltext.queryNodes('dm_entity_text', $term) YIELD node ${combinedWhere} RETURN count(node) AS total`;
1757
+ const termParam = { term: query.searchTerm };
1758
+ const [dataResult, count] = await Promise.all([
1759
+ conn.executeQuery(
1760
+ dataCypher,
1761
+ { ...where.params, ...termParam, ...skipLimitParams },
1762
+ { repositoryId, routing: "READ" }
1763
+ ),
1764
+ conn.executeQuery(
1765
+ countCypher,
1766
+ { ...where.params, ...termParam },
1767
+ { repositoryId, routing: "READ" }
1768
+ )
1769
+ ]);
1770
+ records = dataResult.records;
1771
+ countResult = count;
1772
+ } else {
1773
+ const projection = loadEmbeddings ? FIND_PROJECTION_FULL : FIND_PROJECTION_LIGHT;
1774
+ const where = buildFindEntitiesWhere(query, {
1775
+ alias: "n",
1776
+ includeRepositoryPredicate: true
1777
+ });
1778
+ const dataCypher = `MATCH (n:_Entity) ${where.cypherWhere} RETURN ${projection} ORDER BY n.id SKIP $skip LIMIT $limit`;
1779
+ const countCypher = `MATCH (n:_Entity) ${where.cypherWhere} RETURN count(n) AS total`;
1780
+ const [dataResult, count] = await Promise.all([
1781
+ conn.executeQuery(
1782
+ dataCypher,
1783
+ { ...where.params, ...skipLimitParams },
1784
+ { repositoryId, routing: "READ" }
1785
+ ),
1786
+ conn.executeQuery(countCypher, where.params, { repositoryId, routing: "READ" })
1787
+ ]);
1788
+ records = dataResult.records;
1789
+ countResult = count;
1790
+ }
1791
+ const items = records.map((record) => entityFromRecord(record));
1792
+ const totalRaw = countResult.records[0]?.get("total");
1793
+ const total = totalRaw === void 0 ? 0 : bigintToSafeNumber(totalRaw);
1794
+ const hasMore = query.offset + items.length < total;
1795
+ return {
1796
+ items,
1797
+ total,
1798
+ hasMore,
1799
+ limit: query.limit,
1800
+ offset: query.offset
1801
+ };
1802
+ }
1803
+ async function deleteEntitiesByType(conn, repositoryId, entityType) {
1804
+ const result = await conn.executeQuery(
1805
+ `MATCH (n:_Entity {repositoryId: $rid, entityType: $entityType})
1806
+ OPTIONAL MATCH (n)-[r]-()
1807
+ WITH collect(DISTINCT n) AS nodes,
1808
+ count(DISTINCT n) AS entities,
1809
+ count(DISTINCT r) AS rels
1810
+ CALL (nodes) {
1811
+ UNWIND nodes AS node
1812
+ DETACH DELETE node
1813
+ }
1814
+ RETURN entities, rels`,
1815
+ { entityType },
1816
+ { repositoryId }
1817
+ );
1818
+ const record = result.records[0];
1819
+ if (record === void 0) {
1820
+ return { deletedEntities: 0, deletedRelationships: 0 };
1821
+ }
1822
+ const deletedEntities = bigintToSafeNumber(record.get("entities") ?? 0);
1823
+ const deletedRelationships = bigintToSafeNumber(record.get("rels") ?? 0);
1824
+ return { deletedEntities, deletedRelationships };
1825
+ }
1826
+
1827
+ // src/queries/relationship.ts
1828
+ var import_deep_memory7 = require("@utaba/deep-memory");
1829
+ var RELATIONSHIP_PROJECTION = buildRelationshipProjection();
1830
+ async function createRelationship(conn, repositoryId, relationship) {
1831
+ const relType = assertSafeRelationshipType(relationship.relationshipType);
1832
+ const cypher = `
1833
+ OPTIONAL MATCH (s:_Entity {repositoryId: $rid, id: $sourceEntityId})
1834
+ OPTIONAL MATCH (t:_Entity {repositoryId: $rid, id: $targetEntityId})
1835
+ WITH s, t
1836
+ FOREACH (_ IN CASE WHEN s IS NOT NULL AND t IS NOT NULL THEN [1] ELSE [] END |
1837
+ CREATE (s)-[r:${relType} {
1838
+ repositoryId: $rid,
1839
+ id: $id,
1840
+ relationshipType: $relationshipType,
1841
+ sourceEntityId: $sourceEntityId,
1842
+ targetEntityId: $targetEntityId,
1843
+ properties: $properties,
1844
+ bidirectional: $bidirectional,
1845
+ createdBy: $createdBy,
1846
+ createdByType: $createdByType,
1847
+ createdAt: $createdAt,
1848
+ createdInConversation: $createdInConversation,
1849
+ createdFromMessage: $createdFromMessage,
1850
+ modifiedBy: $modifiedBy,
1851
+ modifiedByType: $modifiedByType,
1852
+ modifiedAt: $modifiedAt,
1853
+ modifiedInConversation: $modifiedInConversation,
1854
+ modifiedFromMessage: $modifiedFromMessage
1855
+ }]->(t)
1856
+ )
1857
+ RETURN s IS NULL AS sMissing, t IS NULL AS tMissing
1858
+ `;
1859
+ const result = await conn.executeQuery(
1860
+ cypher,
1861
+ relationshipToParams(relationship),
1862
+ { repositoryId }
1863
+ );
1864
+ const record = result.records[0];
1865
+ if (record === void 0) {
1866
+ throw new import_deep_memory7.EntityNotFoundError(relationship.sourceEntityId);
1867
+ }
1868
+ const sMissing = record.get("sMissing") === true;
1869
+ const tMissing = record.get("tMissing") === true;
1870
+ if (sMissing) throw new import_deep_memory7.EntityNotFoundError(relationship.sourceEntityId);
1871
+ if (tMissing) throw new import_deep_memory7.EntityNotFoundError(relationship.targetEntityId);
1872
+ return relationship;
1873
+ }
1874
+ async function getRelationship(conn, repositoryId, relationshipId) {
1875
+ const result = await conn.executeQuery(
1876
+ `MATCH ()-[r {repositoryId: $rid, id: $relId}]->() RETURN ${RELATIONSHIP_PROJECTION}`,
1877
+ { relId: relationshipId },
1878
+ { repositoryId, routing: "READ" }
1879
+ );
1880
+ const record = result.records[0];
1881
+ if (record === void 0) return null;
1882
+ return relationshipFromRecord(record);
1883
+ }
1884
+ async function getEntityRelationships(conn, repositoryId, entityId, options) {
1885
+ const limit = options?.limit ?? 50;
1886
+ const offset = options?.offset ?? 0;
1887
+ const direction = options?.direction ?? "both";
1888
+ const propertyFilters = options?.propertyFilters;
1889
+ const hasPropertyFilters = propertyFilters != null && propertyFilters.length > 0;
1890
+ const params = {
1891
+ eid: entityId,
1892
+ offset: BigInt(offset),
1893
+ limit: BigInt(limit)
1894
+ };
1895
+ const typeFilter = options?.relationshipTypes != null && options.relationshipTypes.length > 0 ? buildTypeFilter(options.relationshipTypes, params) : "";
1896
+ const { dataCypher, countCypher } = buildEntityRelationshipQueries(direction, typeFilter);
1897
+ const [dataResult, countResult] = await Promise.all([
1898
+ conn.executeQuery(dataCypher, params, { repositoryId, routing: "READ" }),
1899
+ hasPropertyFilters ? Promise.resolve(null) : conn.executeQuery(countCypher, params, { repositoryId, routing: "READ" })
1900
+ ]);
1901
+ let items = dataResult.records.map((record) => relationshipFromRecord(record));
1902
+ if (hasPropertyFilters) {
1903
+ items = items.filter((rel) => (0, import_deep_memory7.matchesPropertyFilters)(rel.properties, propertyFilters));
1904
+ }
1905
+ let total;
1906
+ if (countResult !== null) {
1907
+ const totalRecord = countResult.records[0];
1908
+ total = totalRecord !== void 0 ? bigintToSafeNumber(totalRecord.get("total") ?? 0) : 0;
1909
+ }
1910
+ const hasMore = total !== void 0 ? offset + dataResult.records.length < total : dataResult.records.length === limit;
1911
+ return { items, total, hasMore, limit, offset };
1912
+ }
1913
+ async function deleteRelationship(conn, repositoryId, relationshipId) {
1914
+ await conn.executeQuery(
1915
+ "MATCH ()-[r {repositoryId: $rid, id: $relId}]->() DELETE r",
1916
+ { relId: relationshipId },
1917
+ { repositoryId }
1918
+ );
1919
+ }
1920
+ async function deleteRelationships(conn, repositoryId, ids) {
1921
+ if (ids.length === 0) return { deleted: [], notFound: [] };
1922
+ const result = await conn.executeQuery(
1923
+ "MATCH ()-[r {repositoryId: $rid}]->() WHERE r.id IN $ids WITH r, r.id AS id DELETE r RETURN id AS deleted",
1924
+ { ids },
1925
+ { repositoryId }
1926
+ );
1927
+ const deleted = [];
1928
+ for (const record of result.records) {
1929
+ const id = record.get("deleted");
1930
+ if (typeof id === "string") deleted.push(id);
1931
+ }
1932
+ const deletedSet = new Set(deleted);
1933
+ const notFound = ids.filter((id) => !deletedSet.has(id));
1934
+ return { deleted, notFound };
1935
+ }
1936
+ async function deleteRelationshipsByType(conn, repositoryId, relationshipType) {
1937
+ const relType = assertSafeRelationshipType(relationshipType);
1938
+ const result = await conn.executeQuery(
1939
+ `MATCH ()-[r:${relType} {repositoryId: $rid}]->() WITH r, r.id AS id DELETE r RETURN id`,
1940
+ {},
1941
+ { repositoryId }
1942
+ );
1943
+ return { deletedRelationships: result.records.length };
1944
+ }
1945
+ function buildTypeFilter(relationshipTypes, params) {
1946
+ params["relTypes"] = relationshipTypes;
1947
+ return " WHERE type(r) IN $relTypes";
1948
+ }
1949
+ function buildEntityRelationshipQueries(direction, typeFilter) {
1950
+ if (direction === "both") {
1951
+ const dataCypher2 = `MATCH (e:_Entity {repositoryId: $rid, id: $eid})-[r {repositoryId: $rid}]-()${typeFilter} RETURN ${RELATIONSHIP_PROJECTION} ORDER BY id SKIP $offset LIMIT $limit`;
1952
+ const countCypher2 = `MATCH (e:_Entity {repositoryId: $rid, id: $eid})-[r {repositoryId: $rid}]-()${typeFilter} RETURN count(r) AS total`;
1953
+ return { dataCypher: dataCypher2, countCypher: countCypher2 };
1954
+ }
1955
+ const naturalPattern = direction === "out" ? "(e:_Entity {repositoryId: $rid, id: $eid})-[r {repositoryId: $rid}]->()" : "()-[r {repositoryId: $rid}]->(e:_Entity {repositoryId: $rid, id: $eid})";
1956
+ const bidirPattern = direction === "out" ? "()-[r {repositoryId: $rid, bidirectional: true}]->(e:_Entity {repositoryId: $rid, id: $eid})" : "(e:_Entity {repositoryId: $rid, id: $eid})-[r {repositoryId: $rid, bidirectional: true}]->()";
1957
+ const dataCypher = `CALL () {
1958
+ MATCH ${naturalPattern}${typeFilter} RETURN ${RELATIONSHIP_PROJECTION}
1959
+ UNION ALL
1960
+ MATCH ${bidirPattern}${typeFilter} RETURN ${RELATIONSHIP_PROJECTION}
1961
+ }
1962
+ RETURN ${reprojectFromCallScope()} ORDER BY id SKIP $offset LIMIT $limit`;
1963
+ const countCypher = `CALL () {
1964
+ MATCH ${naturalPattern}${typeFilter} RETURN r.id AS rid
1965
+ UNION ALL
1966
+ MATCH ${bidirPattern}${typeFilter} RETURN r.id AS rid
1967
+ }
1968
+ RETURN count(rid) AS total`;
1969
+ return { dataCypher, countCypher };
1970
+ }
1971
+ function reprojectFromCallScope() {
1972
+ return RELATIONSHIP_PROJECTION.split(",").map((part) => {
1973
+ const segments = part.trim().split(" AS ");
1974
+ const fieldName = (segments[1] ?? segments[0] ?? "").trim();
1975
+ return `${fieldName} AS ${fieldName}`;
1976
+ }).join(", ");
1977
+ }
1978
+
1979
+ // src/queries/repository.ts
1980
+ var ENTITY_STATS_QUERY = `
1981
+ MATCH (n:_Entity {repositoryId: $rid})
1982
+ RETURN n.entityType AS type, count(n) AS count
1983
+ `;
1984
+ var RELATIONSHIP_STATS_QUERY = `
1985
+ MATCH (:_Entity {repositoryId: $rid})-[r {repositoryId: $rid}]->(:_Entity {repositoryId: $rid})
1986
+ RETURN type(r) AS type, count(r) AS count
1987
+ `;
1988
+ async function getRepositoryStats(conn, repositoryId, vocabulary) {
1989
+ const [entityResult, relationshipResult] = await Promise.all([
1990
+ conn.executeQuery(ENTITY_STATS_QUERY, {}, { repositoryId, routing: "READ" }),
1991
+ conn.executeQuery(RELATIONSHIP_STATS_QUERY, {}, { repositoryId, routing: "READ" })
1992
+ ]);
1993
+ const entityTypeBreakdown = {};
1994
+ let entityCount = 0;
1995
+ for (const record of entityResult.records) {
1996
+ const type = record.get("type");
1997
+ if (typeof type !== "string") continue;
1998
+ const count = bigintToSafeNumber(record.get("count") ?? 0);
1999
+ entityTypeBreakdown[type] = count;
2000
+ entityCount += count;
2001
+ }
2002
+ const relationshipTypeBreakdown = {};
2003
+ let relationshipCount = 0;
2004
+ for (const record of relationshipResult.records) {
2005
+ const type = record.get("type");
2006
+ if (typeof type !== "string") continue;
2007
+ const count = bigintToSafeNumber(record.get("count") ?? 0);
2008
+ relationshipTypeBreakdown[type] = count;
2009
+ relationshipCount += count;
2010
+ }
2011
+ return {
2012
+ entityCount,
2013
+ relationshipCount,
2014
+ vocabularyVersion: vocabulary.version,
2015
+ entityTypeBreakdown,
2016
+ relationshipTypeBreakdown
2017
+ };
2018
+ }
2019
+
2020
+ // src/queries/timeline.ts
2021
+ var ENTITY_CREATED = "entity:created";
2022
+ var ENTITY_UPDATED = "entity:updated";
2023
+ var RELATIONSHIP_CREATED = "relationship:created";
2024
+ var TIMELINE_QUERY = `
2025
+ MATCH (n:_Entity {repositoryId: $rid, id: $id})
2026
+ OPTIONAL MATCH (n)-[r {repositoryId: $rid}]-()
2027
+ WITH n, collect(DISTINCT r) AS rels
2028
+ RETURN
2029
+ n.createdAt AS createdAt,
2030
+ n.modifiedAt AS modifiedAt,
2031
+ [x IN rels WHERE x IS NOT NULL | {id: x.id, createdAt: x.createdAt}] AS rels
2032
+ `;
2033
+ async function getTimeline(conn, repositoryId, entityId, options) {
2034
+ const result = await conn.executeQuery(
2035
+ TIMELINE_QUERY,
2036
+ { id: entityId },
2037
+ { repositoryId, routing: "READ" }
2038
+ );
2039
+ const record = result.records[0];
2040
+ if (record === void 0) {
2041
+ return { events: [], total: 0 };
2042
+ }
2043
+ const createdAt = record.get("createdAt");
2044
+ const modifiedAt = record.get("modifiedAt");
2045
+ const relsRaw = record.get("rels");
2046
+ const events = [];
2047
+ if (typeof createdAt === "string" && createdAt.length > 0) {
2048
+ events.push({ timestamp: createdAt, eventType: ENTITY_CREATED, entityId });
2049
+ }
2050
+ if (typeof modifiedAt === "string" && modifiedAt.length > 0 && modifiedAt !== createdAt) {
2051
+ events.push({ timestamp: modifiedAt, eventType: ENTITY_UPDATED, entityId });
2052
+ }
2053
+ if (Array.isArray(relsRaw)) {
2054
+ for (const item of relsRaw) {
2055
+ if (item === null || typeof item !== "object") continue;
2056
+ const rel = item;
2057
+ const relId = rel.id;
2058
+ const relCreatedAt = rel.createdAt;
2059
+ if (typeof relId !== "string" || typeof relCreatedAt !== "string") continue;
2060
+ events.push({
2061
+ timestamp: relCreatedAt,
2062
+ eventType: RELATIONSHIP_CREATED,
2063
+ entityId,
2064
+ relationshipId: relId
2065
+ });
2066
+ }
2067
+ }
2068
+ let filtered = events;
2069
+ if (options.timeRange) {
2070
+ const from = options.timeRange.from;
2071
+ const to = options.timeRange.to;
2072
+ filtered = filtered.filter((e) => e.timestamp >= from && e.timestamp <= to);
2073
+ }
2074
+ if (options.eventTypes && options.eventTypes.length > 0) {
2075
+ const allow = new Set(options.eventTypes);
2076
+ filtered = filtered.filter((e) => allow.has(e.eventType));
2077
+ }
2078
+ filtered.sort((a, b) => a.timestamp < b.timestamp ? 1 : a.timestamp > b.timestamp ? -1 : 0);
2079
+ const total = filtered.length;
2080
+ const paged = filtered.slice(options.offset, options.offset + options.limit);
2081
+ return { events: paged, total };
2082
+ }
2083
+
2084
+ // src/queries/vocabulary.ts
2085
+ function emptyVocabulary() {
2086
+ return {
2087
+ version: "0.0.0",
2088
+ lastModified: (/* @__PURE__ */ new Date()).toISOString(),
2089
+ modifiedBy: "system",
2090
+ entityTypes: [],
2091
+ relationshipTypes: []
2092
+ };
2093
+ }
2094
+ async function getVocabulary(conn, repositoryId) {
2095
+ const result = await conn.executeQuery(
2096
+ "MATCH (v:_Vocabulary {repositoryId: $rid}) RETURN v.vocabulary AS json",
2097
+ {},
2098
+ { repositoryId, routing: "READ" }
2099
+ );
2100
+ const record = result.records[0];
2101
+ if (record === void 0) return emptyVocabulary();
2102
+ const raw = record.get("json");
2103
+ if (typeof raw !== "string" || raw === "") return emptyVocabulary();
2104
+ try {
2105
+ return JSON.parse(raw);
2106
+ } catch {
2107
+ return emptyVocabulary();
2108
+ }
2109
+ }
2110
+ async function saveVocabulary(conn, repositoryId, vocabulary) {
2111
+ await conn.executeQuery(
2112
+ "MERGE (v:_Vocabulary {repositoryId: $rid}) SET v.vocabulary = $json",
2113
+ { json: JSON.stringify(vocabulary) },
2114
+ { repositoryId }
2115
+ );
2116
+ }
2117
+ async function getVocabularyChangeLog(conn, repositoryId, options) {
2118
+ const limit = options?.limit ?? 10;
2119
+ const offset = options?.offset ?? 0;
2120
+ const [dataResult, countResult] = await Promise.all([
2121
+ conn.executeQuery(
2122
+ `MATCH (e:_VocabularyChangeLog {repositoryId: $rid})
2123
+ RETURN e
2124
+ ORDER BY e.proposedAt DESC
2125
+ SKIP $offset LIMIT $limit`,
2126
+ { offset: BigInt(offset), limit: BigInt(limit) },
2127
+ { repositoryId, routing: "READ" }
2128
+ ),
2129
+ conn.executeQuery(
2130
+ "MATCH (e:_VocabularyChangeLog {repositoryId: $rid}) RETURN count(e) AS total",
2131
+ {},
2132
+ { repositoryId, routing: "READ" }
2133
+ )
2134
+ ]);
2135
+ const items = dataResult.records.map((record) => changeRecordFromRecord(record, "e"));
2136
+ const totalRaw = countResult.records[0]?.get("total");
2137
+ const total = totalRaw === void 0 ? 0 : bigintToSafeNumber(totalRaw);
2138
+ return {
2139
+ items,
2140
+ total,
2141
+ hasMore: offset + items.length < total,
2142
+ limit,
2143
+ offset
2144
+ };
2145
+ }
2146
+
2147
+ // src/schema.ts
2148
+ var SCHEMA_VERSION = 1;
2149
+ function getSchemaCypher() {
2150
+ return [
2151
+ // Composite node-property uniqueness on (repositoryId, id) — the load-bearing
2152
+ // primary discriminator for every entity-scoped query.
2153
+ `CREATE CONSTRAINT dm_entity_unique IF NOT EXISTS
2154
+ FOR (n:_Entity) REQUIRE (n.repositoryId, n.id) IS UNIQUE`,
2155
+ // Composite uniqueness on (repositoryId, slug) — backs getEntityBySlug
2156
+ // and guarantees stable URL-style addressing within a repository.
2157
+ `CREATE CONSTRAINT dm_entity_slug_unique IF NOT EXISTS
2158
+ FOR (n:_Entity) REQUIRE (n.repositoryId, n.slug) IS UNIQUE`,
2159
+ // Repository uniqueness — backs getRepository and listRepositories.
2160
+ `CREATE CONSTRAINT dm_repository_unique IF NOT EXISTS
2161
+ FOR (n:_Repository) REQUIRE n.repositoryId IS UNIQUE`,
2162
+ // Range index (default for `CREATE INDEX` without a type keyword) covering
2163
+ // (repositoryId, entityType) — backs findEntities type-filter and
2164
+ // deleteEntitiesByType.
2165
+ `CREATE INDEX dm_entity_type_lookup IF NOT EXISTS
2166
+ FOR (n:_Entity) ON (n.repositoryId, n.entityType)`,
2167
+ // Range index on (repositoryId, modifiedAt) — backs timeline and recency
2168
+ // ordering. Date is stored as ISO-8601 string (D6), lexicographic order
2169
+ // matches chronological order.
2170
+ `CREATE INDEX dm_entity_modified IF NOT EXISTS
2171
+ FOR (n:_Entity) ON (n.repositoryId, n.modifiedAt)`,
2172
+ // Fulltext index on entity label + summary — backs the findEntities
2173
+ // searchTerm branch via CALL db.index.fulltext.queryNodes(...). Unfiltered
2174
+ // by design; query callers must post-filter by repositoryId (see D3b).
2175
+ `CREATE FULLTEXT INDEX dm_entity_text IF NOT EXISTS
2176
+ FOR (n:_Entity) ON EACH [n.label, n.summary]`
2177
+ ];
2178
+ }
2179
+
2180
+ // src/Neo4jStorageProvider.ts
2181
+ var PROVIDER_NAME = "neo4j";
2182
+ var DELETE_BATCH_SIZE = 500;
2183
+ var VOCABULARY_CACHE_TTL_MS = 6e4;
2184
+ var TRACKED_METHODS = {
2185
+ ensureSchema: () => void 0,
2186
+ createRepository: (args) => {
2187
+ const cfg = args[0];
2188
+ return cfg?.repositoryId;
2189
+ },
2190
+ getRepository: (args) => args[0],
2191
+ listRepositories: () => void 0,
2192
+ updateRepository: (args) => args[0],
2193
+ deleteRepository: (args) => args[0],
2194
+ deleteAllContents: (args) => args[0],
2195
+ getVocabulary: (args) => args[0],
2196
+ saveVocabulary: (args) => args[0],
2197
+ getVocabularyChangeLog: (args) => args[0],
2198
+ createEntity: (args) => args[0],
2199
+ getEntity: (args) => args[0],
2200
+ getEntityBySlug: (args) => args[0],
2201
+ getEntities: (args) => args[0],
2202
+ updateEntity: (args) => args[0],
2203
+ deleteEntity: (args) => args[0],
2204
+ deleteEntities: (args) => args[0],
2205
+ deleteEntitiesByType: (args) => args[0],
2206
+ findEntities: (args) => args[0],
2207
+ createRelationship: (args) => args[0],
2208
+ getRelationship: (args) => args[0],
2209
+ getEntityRelationships: (args) => args[0],
2210
+ deleteRelationship: (args) => args[0],
2211
+ deleteRelationships: (args) => args[0],
2212
+ deleteRelationshipsByType: (args) => args[0],
2213
+ traverse: (args) => args[0],
2214
+ exploreNeighborhood: (args) => args[0],
2215
+ findPaths: (args) => args[0],
2216
+ getTimeline: (args) => args[0],
2217
+ getRepositoryStats: (args) => args[0],
2218
+ importBulk: (args) => args[0],
2219
+ // `executeNativeQuery` is cross-repository by design. The first positional
2220
+ // argument is `repositoryId` for interface symmetry but is intentionally
2221
+ // not stamped on the sink record — the call is not scoped to one repository.
2222
+ executeNativeQuery: () => void 0
2223
+ // `exportAll` is intentionally omitted from this map. The Proxy emits one
2224
+ // sink record at promise resolution; an `AsyncIterable` returns
2225
+ // synchronously and is consumed across an arbitrary number of awaits, so
2226
+ // a single emit at method-return would fire BEFORE any chunk had streamed
2227
+ // and the sink would carry zero round-trips. `exportAll` opens its own
2228
+ // usage scope via `trackIterable` and emits when the iterator drains.
2229
+ };
2230
+ var META_KEY = "schema";
2231
+ var Neo4jStorageProvider = class {
2232
+ connection;
2233
+ traversalExecutor;
2234
+ initialized = false;
2235
+ /**
2236
+ * In-process vocabulary cache. Reads hit this map first; writes inside this
2237
+ * process invalidate the entry so cache hits stay coherent with the local
2238
+ * write. Cross-process staleness is bounded by `VOCABULARY_CACHE_TTL_MS`.
2239
+ */
2240
+ vocabularyCache = /* @__PURE__ */ new Map();
2241
+ /**
2242
+ * Captured at construction so `exportAll`'s `trackIterable` can emit
2243
+ * outside the Proxy's promise-resolution path. The Proxy itself relies on
2244
+ * the same `safeSink` captured by closure; this field exists for the
2245
+ * streaming-iterator case only.
2246
+ */
2247
+ reportUsage;
2248
+ constructor(config) {
2249
+ this.connection = new Neo4jConnection(config);
2250
+ this.traversalExecutor = new Neo4jTraversalExecutor(this.connection, {
2251
+ profileTraversals: config.profileTraversals === true
2252
+ });
2253
+ const safeSink = (0, import_deep_memory8.createSafeSink)(config.reportUsage);
2254
+ this.reportUsage = safeSink;
2255
+ if (safeSink) {
2256
+ return new Proxy(this, {
2257
+ get(target, prop, receiver) {
2258
+ const value = Reflect.get(target, prop, receiver);
2259
+ if (typeof prop !== "string" || typeof value !== "function") return value;
2260
+ const extractRepoId = TRACKED_METHODS[prop];
2261
+ if (!extractRepoId) return value;
2262
+ const method = value;
2263
+ return (...args) => {
2264
+ const scope = createUsageScope();
2265
+ const repositoryId = extractRepoId(args);
2266
+ const emit = () => {
2267
+ safeSink({
2268
+ provider: PROVIDER_NAME,
2269
+ operation: prop,
2270
+ unit: "server_ms",
2271
+ value: scope.serverMs,
2272
+ ...repositoryId !== void 0 ? { repositoryId } : {},
2273
+ timestamp: /* @__PURE__ */ new Date(),
2274
+ details: buildUsageDetails(scope)
2275
+ });
2276
+ };
2277
+ return runInUsageScope(scope, () => {
2278
+ let result;
2279
+ try {
2280
+ result = method.apply(target, args);
2281
+ } catch (err) {
2282
+ emit();
2283
+ throw err;
2284
+ }
2285
+ if (result && typeof result.then === "function") {
2286
+ return result.then(
2287
+ (v) => {
2288
+ emit();
2289
+ return v;
2290
+ },
2291
+ (err) => {
2292
+ emit();
2293
+ throw err;
2294
+ }
2295
+ );
2296
+ }
2297
+ emit();
2298
+ return result;
2299
+ });
2300
+ };
2301
+ }
2302
+ });
2303
+ }
2304
+ }
2305
+ // ─── Lifecycle ─────────────────────────────────────────────────────
2306
+ async initialize() {
2307
+ if (this.initialized) return;
2308
+ await this.connection.verifyConnectivity();
2309
+ this.initialized = true;
2310
+ }
2311
+ async dispose() {
2312
+ await this.connection.close();
2313
+ this.initialized = false;
2314
+ }
2315
+ // ─── Schema ────────────────────────────────────────────────────────
2316
+ /**
2317
+ * Idempotent constraint / index DDL plus a single `_Meta` schema-version
2318
+ * handshake. Safe to call repeatedly — `CREATE ... IF NOT EXISTS` makes
2319
+ * each statement a no-op on subsequent runs.
2320
+ *
2321
+ * Neo4j Community has no per-tenant database concept — `databaseCreated`
2322
+ * is always `false`. Operators are responsible for the target database
2323
+ * existing before the provider connects.
2324
+ */
2325
+ async ensureSchema() {
2326
+ const currentVersion = await this.readSchemaVersion();
2327
+ if (currentVersion !== null && currentVersion > SCHEMA_VERSION) {
2328
+ throw new import_deep_memory8.ProviderError(
2329
+ `Database schema version ${currentVersion} is newer than provider version ${SCHEMA_VERSION}. Update the @utaba/deep-memory-storage-neo4j package.`
2330
+ );
2331
+ }
2332
+ if (currentVersion === SCHEMA_VERSION) {
2333
+ return {
2334
+ databaseCreated: false,
2335
+ schemaCreated: false,
2336
+ alreadyUpToDate: true,
2337
+ schemaVersion: SCHEMA_VERSION
2338
+ };
2339
+ }
2340
+ for (const statement of getSchemaCypher()) {
2341
+ await this.connection.executeSystemDdl(statement);
2342
+ }
2343
+ await this.writeSchemaVersion(SCHEMA_VERSION);
2344
+ return {
2345
+ databaseCreated: false,
2346
+ schemaCreated: true,
2347
+ alreadyUpToDate: false,
2348
+ schemaVersion: SCHEMA_VERSION
2349
+ };
2350
+ }
2351
+ async readSchemaVersion() {
2352
+ const result = await this.connection.executeSystemQuery(
2353
+ "MATCH (m:_Meta {key: $key}) RETURN m.schemaVersion AS schemaVersion",
2354
+ { key: META_KEY },
2355
+ { crossRepository: true, routing: "READ" }
2356
+ );
2357
+ const record = result.records[0];
2358
+ if (record === void 0) return null;
2359
+ const raw = record.get("schemaVersion");
2360
+ if (typeof raw === "bigint") {
2361
+ if (raw > BigInt(Number.MAX_SAFE_INTEGER)) {
2362
+ throw new import_deep_memory8.ProviderError(
2363
+ `_Meta.schemaVersion (${raw.toString()}) exceeds Number.MAX_SAFE_INTEGER.`
2364
+ );
2365
+ }
2366
+ return Number(raw);
2367
+ }
2368
+ if (typeof raw === "number") return raw;
2369
+ if (raw === null) return null;
2370
+ throw new import_deep_memory8.ProviderError(
2371
+ `_Meta.schemaVersion has unexpected type ${typeof raw}; expected bigint or number.`
2372
+ );
2373
+ }
2374
+ async writeSchemaVersion(version) {
2375
+ await this.connection.executeSystemQuery(
2376
+ "MERGE (m:_Meta {key: $key}) SET m.schemaVersion = $version",
2377
+ { key: META_KEY, version },
2378
+ { crossRepository: true }
2379
+ );
2380
+ }
2381
+ // ─── Repository ────────────────────────────────────────────────────
2382
+ /**
2383
+ * Create a new repository. Fixed-shape `CREATE` template — every optional
2384
+ * field is bound on every call so the server plan-caches one entry across
2385
+ * all repository creates. Optional fields bound as `null` are not persisted
2386
+ * (Cypher drops null property values on write — symmetric with read).
2387
+ *
2388
+ * The `(:_Repository) REQUIRE n.repositoryId IS UNIQUE` constraint surfaces
2389
+ * duplicates as `Neo.ClientError.Schema.ConstraintValidationFailed`, which
2390
+ * `mapDriverError({ kind: 'repository', ... })` routes to
2391
+ * `DuplicateRepositoryError`.
2392
+ */
2393
+ async createRepository(config) {
2394
+ try {
2395
+ await this.connection.executeQuery(
2396
+ `CREATE (r:_Repository {
2397
+ repositoryId: $rid,
2398
+ type: $type,
2399
+ label: $label,
2400
+ description: $description,
2401
+ legal: $legal,
2402
+ owner: $owner,
2403
+ governanceConfig: $governanceConfig,
2404
+ metadata: $metadata,
2405
+ createdAt: $createdAt,
2406
+ createdBy: $createdBy
2407
+ })`,
2408
+ repositoryCreateParams(config),
2409
+ { repositoryId: config.repositoryId }
2410
+ );
2411
+ } catch (err) {
2412
+ mapDriverError(err, {
2413
+ kind: "repository",
2414
+ repositoryId: config.repositoryId,
2415
+ operation: "createRepository"
2416
+ });
2417
+ }
2418
+ const result = {
2419
+ repositoryId: config.repositoryId,
2420
+ label: config.label,
2421
+ governanceConfig: config.governanceConfig,
2422
+ createdAt: config.createdAt,
2423
+ createdBy: config.createdBy
2424
+ };
2425
+ if (config.type !== void 0) result.type = config.type;
2426
+ if (config.description !== void 0) result.description = config.description;
2427
+ if (config.legal !== void 0) result.legal = config.legal;
2428
+ if (config.owner !== void 0) result.owner = config.owner;
2429
+ if (config.metadata !== void 0) result.metadata = config.metadata;
2430
+ return result;
2431
+ }
2432
+ async getRepository(repositoryId) {
2433
+ const result = await this.connection.executeQuery(
2434
+ "MATCH (r:_Repository {repositoryId: $rid}) RETURN r",
2435
+ {},
2436
+ { repositoryId, routing: "READ" }
2437
+ );
2438
+ const record = result.records[0];
2439
+ if (record === void 0) return null;
2440
+ return repositoryFromRecord(record);
2441
+ }
2442
+ async listRepositories(filter) {
2443
+ const limit = filter?.limit ?? 20;
2444
+ const offset = filter?.offset ?? 0;
2445
+ const typeFilter = filter?.type;
2446
+ const wherePredicates = [];
2447
+ const params = { offset: BigInt(offset), limit: BigInt(limit) };
2448
+ if (typeFilter !== void 0) {
2449
+ wherePredicates.push("r.type = $filterType");
2450
+ params["filterType"] = typeFilter;
2451
+ }
2452
+ const whereClause = wherePredicates.length > 0 ? `WHERE ${wherePredicates.join(" AND ")}` : "";
2453
+ const [dataResult, countResult] = await Promise.all([
2454
+ this.connection.executeSystemQuery(
2455
+ `MATCH (r:_Repository) ${whereClause} RETURN r ORDER BY r.repositoryId SKIP $offset LIMIT $limit`,
2456
+ params,
2457
+ { crossRepository: true, routing: "READ" }
2458
+ ),
2459
+ this.connection.executeSystemQuery(
2460
+ `MATCH (r:_Repository) ${whereClause} RETURN count(r) AS total`,
2461
+ typeFilter !== void 0 ? { filterType: typeFilter } : {},
2462
+ { crossRepository: true, routing: "READ" }
2463
+ )
2464
+ ]);
2465
+ const items = dataResult.records.map((record) => repositorySummaryFromRecord(record));
2466
+ const totalRaw = countResult.records[0]?.get("total");
2467
+ const total = totalRaw === void 0 ? 0 : bigintToSafeNumber(totalRaw);
2468
+ return {
2469
+ items,
2470
+ total,
2471
+ hasMore: offset + items.length < total,
2472
+ limit,
2473
+ offset
2474
+ };
2475
+ }
2476
+ /**
2477
+ * Variable-shape Cypher (per D23 trade-off — repository writes are rare so
2478
+ * the plan-cache cost is negligible). Projection-on-write returns the
2479
+ * updated row in one round-trip; empty-rowset → `RepositoryNotFoundError`.
2480
+ */
2481
+ async updateRepository(repositoryId, updates) {
2482
+ const setClauses = [];
2483
+ const params = {};
2484
+ if (updates.label !== void 0) {
2485
+ setClauses.push("r.label = $label");
2486
+ params["label"] = updates.label;
2487
+ }
2488
+ if (updates.description !== void 0) {
2489
+ setClauses.push("r.description = $description");
2490
+ params["description"] = updates.description;
2491
+ }
2492
+ if (updates.type !== void 0) {
2493
+ setClauses.push("r.type = $type");
2494
+ params["type"] = updates.type;
2495
+ }
2496
+ if (updates.legal !== void 0) {
2497
+ setClauses.push("r.legal = $legal");
2498
+ params["legal"] = updates.legal;
2499
+ }
2500
+ if (updates.owner !== void 0) {
2501
+ setClauses.push("r.owner = $owner");
2502
+ params["owner"] = updates.owner;
2503
+ }
2504
+ if (updates.governanceConfig !== void 0) {
2505
+ setClauses.push("r.governanceConfig = $governanceConfig");
2506
+ params["governanceConfig"] = JSON.stringify(updates.governanceConfig);
2507
+ }
2508
+ if (updates.metadata !== void 0) {
2509
+ const existing = await this.getRepository(repositoryId);
2510
+ if (existing === null) throw new import_deep_memory8.RepositoryNotFoundError(repositoryId);
2511
+ const merged = { ...existing.metadata, ...updates.metadata };
2512
+ setClauses.push("r.metadata = $metadata");
2513
+ params["metadata"] = JSON.stringify(merged);
2514
+ }
2515
+ if (setClauses.length === 0) {
2516
+ const existing = await this.getRepository(repositoryId);
2517
+ if (existing === null) throw new import_deep_memory8.RepositoryNotFoundError(repositoryId);
2518
+ return existing;
2519
+ }
2520
+ const cypher = `MATCH (r:_Repository {repositoryId: $rid}) SET ${setClauses.join(", ")} RETURN r`;
2521
+ const result = await this.connection.executeQuery(cypher, params, { repositoryId });
2522
+ const record = result.records[0];
2523
+ if (record === void 0) throw new import_deep_memory8.RepositoryNotFoundError(repositoryId);
2524
+ return repositoryFromRecord(record);
2525
+ }
2526
+ /**
2527
+ * Drop every node and relationship scoped to `repositoryId`, including the
2528
+ * `_Repository` node itself. Two-stage chunked wipe driven by app-side loops
2529
+ * so the progress callback fires at a useful cadence:
2530
+ *
2531
+ * 1. Drain relationships in batches via `CALL ( ) { ... } IN TRANSACTIONS`.
2532
+ * 2. Drain nodes (entities + system) in batches via the same form with
2533
+ * `DETACH DELETE` (catches any straggler edges).
2534
+ *
2535
+ * `IN TRANSACTIONS` can only run on auto-commit sessions — `executeWrite`
2536
+ * fails with `Neo.DatabaseError.Transaction.TransactionStartFailed` per
2537
+ * probe P13. The chokepoint's `executeImplicitInTransactions` is the only
2538
+ * legitimate entry point for this pattern.
2539
+ */
2540
+ async deleteRepository(repositoryId, onProgress) {
2541
+ const { totalEntities, totalRelationships } = await this.countRepositoryContents(repositoryId);
2542
+ let relationshipsDeleted = 0;
2543
+ let entitiesDeleted = 0;
2544
+ while (true) {
2545
+ const summary = await this.connection.executeImplicitInTransactions(
2546
+ `CALL () {
2547
+ MATCH ()-[r {repositoryId: $rid}]-()
2548
+ WITH r LIMIT $batchSize
2549
+ DELETE r
2550
+ } IN TRANSACTIONS OF $batchSize ROWS`,
2551
+ // BigInt so the Cypher LIMIT clause sees a Cypher INTEGER, not FLOAT.
2552
+ { batchSize: BigInt(DELETE_BATCH_SIZE) },
2553
+ { repositoryId }
2554
+ );
2555
+ const stats = summary.counters.updates();
2556
+ const deletedThisBatch = stats["relationshipsDeleted"] ?? 0;
2557
+ if (deletedThisBatch === 0) break;
2558
+ relationshipsDeleted = Math.min(relationshipsDeleted + deletedThisBatch, totalRelationships);
2559
+ await onProgress?.({ entitiesDeleted, relationshipsDeleted, totalEntities, totalRelationships });
2560
+ }
2561
+ while (true) {
2562
+ const summary = await this.connection.executeImplicitInTransactions(
2563
+ `CALL () {
2564
+ MATCH (n {repositoryId: $rid})
2565
+ WITH n LIMIT $batchSize
2566
+ DETACH DELETE n
2567
+ } IN TRANSACTIONS OF $batchSize ROWS`,
2568
+ // BigInt so the Cypher LIMIT clause sees a Cypher INTEGER, not FLOAT.
2569
+ { batchSize: BigInt(DELETE_BATCH_SIZE) },
2570
+ { repositoryId }
2571
+ );
2572
+ const stats = summary.counters.updates();
2573
+ const deletedThisBatch = stats["nodesDeleted"] ?? 0;
2574
+ if (deletedThisBatch === 0) break;
2575
+ entitiesDeleted = Math.min(entitiesDeleted + deletedThisBatch, totalEntities);
2576
+ await onProgress?.({ entitiesDeleted, relationshipsDeleted, totalEntities, totalRelationships });
2577
+ }
2578
+ }
2579
+ /**
2580
+ * Drop every entity and relationship scoped to `repositoryId` but preserve
2581
+ * the `_Repository` and `_Vocabulary` / `_VocabularyChangeLog` system nodes.
2582
+ * Same chunked-wipe contract as `deleteRepository`, restricted to the
2583
+ * `:_Entity` umbrella label for nodes.
2584
+ */
2585
+ async deleteAllContents(repositoryId, onProgress) {
2586
+ const { totalEntities, totalRelationships } = await this.countRepositoryContents(repositoryId);
2587
+ let relationshipsDeleted = 0;
2588
+ let entitiesDeleted = 0;
2589
+ while (true) {
2590
+ const summary = await this.connection.executeImplicitInTransactions(
2591
+ `CALL () {
2592
+ MATCH (:_Entity {repositoryId: $rid})-[r {repositoryId: $rid}]-(:_Entity {repositoryId: $rid})
2593
+ WITH r LIMIT $batchSize
2594
+ DELETE r
2595
+ } IN TRANSACTIONS OF $batchSize ROWS`,
2596
+ // BigInt so the Cypher LIMIT clause sees a Cypher INTEGER, not FLOAT.
2597
+ { batchSize: BigInt(DELETE_BATCH_SIZE) },
2598
+ { repositoryId }
2599
+ );
2600
+ const stats = summary.counters.updates();
2601
+ const deletedThisBatch = stats["relationshipsDeleted"] ?? 0;
2602
+ if (deletedThisBatch === 0) break;
2603
+ relationshipsDeleted = Math.min(relationshipsDeleted + deletedThisBatch, totalRelationships);
2604
+ await onProgress?.({ entitiesDeleted, relationshipsDeleted, totalEntities, totalRelationships });
2605
+ }
2606
+ while (true) {
2607
+ const summary = await this.connection.executeImplicitInTransactions(
2608
+ `CALL () {
2609
+ MATCH (n:_Entity {repositoryId: $rid})
2610
+ WITH n LIMIT $batchSize
2611
+ DETACH DELETE n
2612
+ } IN TRANSACTIONS OF $batchSize ROWS`,
2613
+ // BigInt so the Cypher LIMIT clause sees a Cypher INTEGER, not FLOAT.
2614
+ { batchSize: BigInt(DELETE_BATCH_SIZE) },
2615
+ { repositoryId }
2616
+ );
2617
+ const stats = summary.counters.updates();
2618
+ const deletedThisBatch = stats["nodesDeleted"] ?? 0;
2619
+ if (deletedThisBatch === 0) break;
2620
+ entitiesDeleted = Math.min(entitiesDeleted + deletedThisBatch, totalEntities);
2621
+ await onProgress?.({ entitiesDeleted, relationshipsDeleted, totalEntities, totalRelationships });
2622
+ }
2623
+ return { deletedEntities: totalEntities, deletedRelationships: totalRelationships };
2624
+ }
2625
+ /**
2626
+ * One-shot pre-count used by `deleteRepository` / `deleteAllContents`. The
2627
+ * entity count uses the umbrella `:_Entity` label so system nodes
2628
+ * (`_Repository` / `_Vocabulary`) are excluded — that matches the
2629
+ * user-facing semantics of the progress callback.
2630
+ *
2631
+ * The relationship count uses the **directed** pattern `()-[r]->()`: every
2632
+ * edge is written in its stored direction (D5), so the directed pattern
2633
+ * binds each edge to exactly one row. An undirected `-` pattern would
2634
+ * double-count every edge whose endpoints are distinct vertices, because
2635
+ * Cypher enumerates the pattern from both directions.
2636
+ */
2637
+ async countRepositoryContents(repositoryId) {
2638
+ const [entitiesResult, relationshipsResult] = await Promise.all([
2639
+ this.connection.executeQuery(
2640
+ "MATCH (n:_Entity {repositoryId: $rid}) RETURN count(n) AS total",
2641
+ {},
2642
+ { repositoryId, routing: "READ" }
2643
+ ),
2644
+ this.connection.executeQuery(
2645
+ "MATCH ()-[r {repositoryId: $rid}]->() RETURN count(r) AS total",
2646
+ {},
2647
+ { repositoryId, routing: "READ" }
2648
+ )
2649
+ ]);
2650
+ const totalEntities = bigintToSafeNumber(entitiesResult.records[0]?.get("total") ?? 0);
2651
+ const totalRelationships = bigintToSafeNumber(relationshipsResult.records[0]?.get("total") ?? 0);
2652
+ return { totalEntities, totalRelationships };
2653
+ }
2654
+ // ─── Vocabulary ────────────────────────────────────────────────────
2655
+ /**
2656
+ * Read the vocabulary for a repository. Cache-aware: cache hits return
2657
+ * synchronously with zero Bolt round-trips. The TRACKED_METHODS proxy still
2658
+ * fires for every call — the sink record on a cache hit carries
2659
+ * `details.calls === 0` and `value === 0`, which is the contract the sink
2660
+ * expects to express "this operation ran but did no server work".
2661
+ */
2662
+ async getVocabulary(repositoryId) {
2663
+ return this.getVocabularyCached(repositoryId);
2664
+ }
2665
+ /**
2666
+ * Cached vocabulary read used by `getVocabulary` and (in later phases) by
2667
+ * traversal compilation. The vocabulary is compile-time context for the
2668
+ * Cypher compiler — it changes on the order of once per session, but the
2669
+ * traversal hot path would otherwise pay one round-trip per call. The cache
2670
+ * flips that to one round-trip per TTL window.
2671
+ *
2672
+ * Reads inside an active usage scope still record a round-trip when a fetch
2673
+ * actually happens (cache miss); cache hits emit no round-trip and therefore
2674
+ * contribute nothing to the scope.
2675
+ */
2676
+ async getVocabularyCached(repositoryId) {
2677
+ const now = Date.now();
2678
+ const cached = this.vocabularyCache.get(repositoryId);
2679
+ if (cached && cached.expiresAt > now) {
2680
+ return cached.vocab;
2681
+ }
2682
+ const vocab = await getVocabulary(this.connection, repositoryId);
2683
+ this.vocabularyCache.set(repositoryId, {
2684
+ vocab,
2685
+ expiresAt: now + VOCABULARY_CACHE_TTL_MS
2686
+ });
2687
+ return vocab;
2688
+ }
2689
+ /** Drop the cache entry for a repository — call after every vocabulary write. */
2690
+ invalidateVocabularyCache(repositoryId) {
2691
+ this.vocabularyCache.delete(repositoryId);
2692
+ }
2693
+ /**
2694
+ * Upsert the vocabulary for a repository. Invalidates the in-process cache
2695
+ * on success so subsequent reads observe the new state immediately within
2696
+ * this process (cross-process staleness is bounded by the 60 s TTL).
2697
+ */
2698
+ async saveVocabulary(repositoryId, vocabulary) {
2699
+ await saveVocabulary(this.connection, repositoryId, vocabulary);
2700
+ this.invalidateVocabularyCache(repositoryId);
2701
+ }
2702
+ /**
2703
+ * Page the vocabulary change-log newest first. Writes land in
2704
+ * `proposeVocabularyExtension` (out of scope here) — this method only reads
2705
+ * the `_VocabularyChangeLog` nodes back, ordered by `proposedAt` to match
2706
+ * the audit semantic on `VocabularyChangeRecord`.
2707
+ */
2708
+ async getVocabularyChangeLog(repositoryId, options) {
2709
+ return getVocabularyChangeLog(this.connection, repositoryId, options);
2710
+ }
2711
+ // ─── Entities ──────────────────────────────────────────────────────
2712
+ /**
2713
+ * Create a new entity via fixed-shape `CREATE` + catch on the uniqueness
2714
+ * constraint. A `MERGE`-with-discriminator alternative is marginally faster
2715
+ * on the happy path but mutates the existing node on collisions, writing
2716
+ * a discriminator property onto durable graph state the caller never
2717
+ * requested — correctness wins over the marginal perf delta.
2718
+ */
2719
+ async createEntity(repositoryId, entity) {
2720
+ return createEntity(this.connection, repositoryId, entity);
2721
+ }
2722
+ /** Read a single entity by id; `null` when not found. */
2723
+ async getEntity(repositoryId, entityId, options) {
2724
+ return getEntity(this.connection, repositoryId, entityId, options);
2725
+ }
2726
+ /** Read a single entity by slug; `null` when not found. */
2727
+ async getEntityBySlug(repositoryId, slug, options) {
2728
+ return getEntityBySlug(this.connection, repositoryId, slug, options);
2729
+ }
2730
+ /**
2731
+ * Batch read by ids. Absent ids do not appear in the returned `Map`; empty
2732
+ * input returns an empty map without a round-trip.
2733
+ */
2734
+ async getEntities(repositoryId, entityIds, options) {
2735
+ return getEntities(this.connection, repositoryId, entityIds, options);
2736
+ }
2737
+ /**
2738
+ * Variable-shape projection-on-write update (D23) — single round-trip
2739
+ * MATCH+SET+RETURN. Empty record array → `EntityNotFoundError`.
2740
+ */
2741
+ async updateEntity(repositoryId, entityId, updates) {
2742
+ return updateEntity(this.connection, repositoryId, entityId, updates);
2743
+ }
2744
+ /**
2745
+ * Delete a single entity (and its incident relationships via `DETACH
2746
+ * DELETE`). Throws `EntityNotFoundError` when the match returns zero rows.
2747
+ */
2748
+ async deleteEntity(repositoryId, entityId) {
2749
+ return deleteEntity(this.connection, repositoryId, entityId);
2750
+ }
2751
+ /**
2752
+ * Bulk delete by ids — single round-trip. Returns the ids actually
2753
+ * deleted; missing ids land in `notFound`.
2754
+ */
2755
+ async deleteEntities(repositoryId, ids) {
2756
+ return deleteEntities(this.connection, repositoryId, ids);
2757
+ }
2758
+ /**
2759
+ * Delete every entity of a type plus their incident relationships, with
2760
+ * exact counts (entity + relationship) returned in one round-trip — a
2761
+ * strict improvement over Cosmos's `deletedRelationships: undefined` path
2762
+ * (Gremlin would fan out across every partition the type touches).
2763
+ */
2764
+ async deleteEntitiesByType(repositoryId, entityType) {
2765
+ return deleteEntitiesByType(this.connection, repositoryId, entityType);
2766
+ }
2767
+ /**
2768
+ * Page entities matching a `StorageFindQuery`. Parallel data + count Cypher
2769
+ * pair; `total` is always exact because every filter (entity-type, property
2770
+ * equality, search term, provenance) is server-side via either a typed
2771
+ * predicate or the `dm_entity_text` fulltext index. Search-term queries
2772
+ * order by Lucene score descending; non-search queries order by `n.id` to
2773
+ * pin pagination determinism across slices.
2774
+ */
2775
+ async findEntities(repositoryId, query, options) {
2776
+ return findEntities(this.connection, repositoryId, query, options);
2777
+ }
2778
+ // ─── Relationships ─────────────────────────────────────────────────
2779
+ /**
2780
+ * Create a relationship. Both endpoint entities are matched under the
2781
+ * repository scope before the edge is created, so cross-repository edges
2782
+ * are structurally impossible to write (D3b layer 3). A missing endpoint
2783
+ * surfaces as `EntityNotFoundError` carrying the absent id.
2784
+ */
2785
+ async createRelationship(repositoryId, relationship) {
2786
+ return createRelationship(this.connection, repositoryId, relationship);
2787
+ }
2788
+ /** Read a single relationship by id; `null` when not found. */
2789
+ async getRelationship(repositoryId, relationshipId) {
2790
+ return getRelationship(this.connection, repositoryId, relationshipId);
2791
+ }
2792
+ /**
2793
+ * Page an entity's incident relationships. `direction: 'out' | 'in'`
2794
+ * additionally surfaces edges flagged `bidirectional: true` from the
2795
+ * opposite endpoint, mirroring the Cosmos read-time duplication of bidir
2796
+ * edges. `propertyFilters` is applied client-side and reports
2797
+ * `total: undefined` in that branch — same trade-off as the Cosmos
2798
+ * provider, because relationship `properties` is a JSON blob with no
2799
+ * per-key index.
2800
+ */
2801
+ async getEntityRelationships(repositoryId, entityId, options) {
2802
+ return getEntityRelationships(
2803
+ this.connection,
2804
+ repositoryId,
2805
+ entityId,
2806
+ options
2807
+ );
2808
+ }
2809
+ /** Drop a single relationship by id. No-op when the id does not match. */
2810
+ async deleteRelationship(repositoryId, relationshipId) {
2811
+ return deleteRelationship(this.connection, repositoryId, relationshipId);
2812
+ }
2813
+ /**
2814
+ * Bulk drop by ids — single round-trip. Returns the ids actually deleted;
2815
+ * missing ids land in `notFound`.
2816
+ */
2817
+ async deleteRelationships(repositoryId, ids) {
2818
+ return deleteRelationships(this.connection, repositoryId, ids);
2819
+ }
2820
+ /**
2821
+ * Drop every relationship of a type in the repository. Returns an exact
2822
+ * delete count in a single round-trip.
2823
+ */
2824
+ async deleteRelationshipsByType(repositoryId, relationshipType) {
2825
+ return deleteRelationshipsByType(
2826
+ this.connection,
2827
+ repositoryId,
2828
+ relationshipType
2829
+ );
2830
+ }
2831
+ // ─── Graph Traversal ───────────────────────────────────────────────
2832
+ /**
2833
+ * Capabilities surface used by the dispatcher to decide whether a given
2834
+ * `TraversalSpec` shape is supported natively. Strict improvement over the
2835
+ * Cosmos provider in two cells: `supportsAggregation` is `true` (Cypher's
2836
+ * native aggregation makes `count` / per-key projection a one-statement
2837
+ * shape) and the runtime supports every other traversal lever.
2838
+ */
2839
+ getCapabilities() {
2840
+ return {
2841
+ supportsNativeQuery: true,
2842
+ nativeQueryLanguage: "cypher",
2843
+ maxTraversalDepth: 10,
2844
+ supportsRelationshipPropertyFilters: true,
2845
+ supportsEntityPropertyFilters: true,
2846
+ supportsAggregation: true,
2847
+ supportsRepeat: true,
2848
+ supportsDedup: true,
2849
+ supportsRelationshipSummary: false
2850
+ };
2851
+ }
2852
+ /**
2853
+ * Execute a `TraversalSpec` against this repository's subgraph. The
2854
+ * provider owns Cypher compilation; the spec stays language-agnostic.
2855
+ * `track('traverse')` opens the per-operation usage scope so every
2856
+ * `executeQuery` round-trip the executor performs aggregates into a single
2857
+ * `OperationUsage` record.
2858
+ */
2859
+ async traverse(repositoryId, spec) {
2860
+ return this.traverseInternal(repositoryId, spec);
2861
+ }
2862
+ /**
2863
+ * Internal compile → submit → project pipeline. Shared by the public
2864
+ * `traverse` method and by the compiler-model rewrites of
2865
+ * `exploreNeighborhood` / `findPaths`, which both consume the raw stored
2866
+ * shape to rebuild their storage-level outputs.
2867
+ */
2868
+ async traverseInternal(repositoryId, spec) {
2869
+ const raw = await this.executeRawTraversal(repositoryId, spec);
2870
+ const detailLevel = spec.detailLevel ?? "summary";
2871
+ const projectStoredEntity = (stored) => {
2872
+ const projected = (0, import_deep_memory8.projectEntity)(stored, detailLevel);
2873
+ if (!spec.includeProvenance) {
2874
+ delete projected["provenance"];
2875
+ }
2876
+ return projected;
2877
+ };
2878
+ const projectStoredRelationship = (rel, direction = "out") => ({
2879
+ id: rel.id,
2880
+ type: rel.relationshipType,
2881
+ sourceEntityId: rel.sourceEntityId,
2882
+ targetEntityId: rel.targetEntityId,
2883
+ direction,
2884
+ properties: rel.properties
2885
+ });
2886
+ let entities = [];
2887
+ let relationships;
2888
+ let paths;
2889
+ const aggregations = raw.aggregations;
2890
+ const projectionEmitted = aggregations !== void 0;
2891
+ if (projectionEmitted) {
2892
+ } else if (spec.returnMode === "terminal") {
2893
+ entities = raw.terminalEntities.map(projectStoredEntity);
2894
+ relationships = void 0;
2895
+ } else if (spec.returnMode === "all") {
2896
+ entities = raw.allEntities.map(projectStoredEntity);
2897
+ relationships = raw.allRelationships.map((r) => projectStoredRelationship(r));
2898
+ } else {
2899
+ paths = raw.pathRows.map((row) => ({
2900
+ length: Math.max(row.entityIds.length - 1, 0),
2901
+ entities: row.entityIds.map((id) => {
2902
+ const stored = raw.entityMap.get(id);
2903
+ if (!stored) {
2904
+ throw new import_deep_memory8.ProviderError(
2905
+ "Unpacking Cypher path: entity referenced by path is missing from the result.",
2906
+ "Inspect compiledQuery \u2014 this indicates a path emission shape mismatch."
2907
+ );
2908
+ }
2909
+ return projectStoredEntity(stored);
2910
+ }),
2911
+ relationships: row.relationshipIds.map((id, i) => {
2912
+ const stored = raw.relationshipMap.get(id);
2913
+ if (!stored) {
2914
+ throw new import_deep_memory8.ProviderError(
2915
+ "Unpacking Cypher path: relationship referenced by path is missing from the result.",
2916
+ "Inspect compiledQuery \u2014 this indicates a path emission shape mismatch."
2917
+ );
2918
+ }
2919
+ return projectStoredRelationship(stored, row.relationshipDirections[i] ?? "out");
2920
+ })
2921
+ }));
2922
+ relationships = Array.from(raw.relationshipMap.values()).map(
2923
+ (rel) => projectStoredRelationship(rel, raw.pathRelFirstDirection.get(rel.id) ?? "out")
2924
+ );
2925
+ }
2926
+ const limit = spec.limit ?? 50;
2927
+ let total;
2928
+ if (projectionEmitted) {
2929
+ total = aggregations.length;
2930
+ } else if (spec.returnMode === "path") {
2931
+ total = paths?.length ?? 0;
2932
+ } else if (spec.returnMode === "all") {
2933
+ total = entities.length + (relationships?.length ?? 0);
2934
+ } else {
2935
+ total = entities.length;
2936
+ }
2937
+ const truncated = total >= limit;
2938
+ const queryMetadata = {
2939
+ executionTimeMs: raw.executionTimeMs,
2940
+ resourceCost: { units: "server_ms", value: raw.serverMs },
2941
+ compiledQuery: raw.compiledQuery,
2942
+ compiledQueryLanguage: "cypher",
2943
+ appliedLimits: {
2944
+ maxResults: limit,
2945
+ ...spec.steps !== void 0 ? { maxDepth: spec.steps.length } : {}
2946
+ },
2947
+ truncated,
2948
+ ...truncated ? { truncationReason: "result_limit" } : {}
2949
+ };
2950
+ return {
2951
+ entities,
2952
+ ...relationships !== void 0 ? { relationships } : {},
2953
+ ...paths !== void 0 ? { paths } : {},
2954
+ ...aggregations !== void 0 ? { aggregations } : {},
2955
+ total,
2956
+ returned: total,
2957
+ hasMore: truncated,
2958
+ queryMetadata
2959
+ };
2960
+ }
2961
+ /**
2962
+ * Lower-level compile + submit + parse helper. Fetches the cached
2963
+ * vocabulary once (D16) and hands it to the executor; the executor handles
2964
+ * the repositoryId-scope rewrite, optional PROFILE prefix, and Path-object
2965
+ * parsing.
2966
+ */
2967
+ async executeRawTraversal(repositoryId, spec) {
2968
+ const vocabulary = await this.getVocabularyCached(repositoryId);
2969
+ return this.traversalExecutor.execute(repositoryId, spec, vocabulary);
2970
+ }
2971
+ /**
2972
+ * BFS-like neighbourhood exploration. For each depth `d` from 1 to
2973
+ * `options.depth`, compile a cumulative `'all'`-mode spec with `d` discrete
2974
+ * `'both'`-direction steps, run it through the executor, and walk one BFS
2975
+ * layer client-side from the previous frontier using the returned edges.
2976
+ *
2977
+ * Round-trips per call: `options.depth`. Server-side step direction is
2978
+ * fixed to `'both'` (catches every edge in either direction); the
2979
+ * directional + bidirectional filter and entity-type filter run client-side
2980
+ * during layer reconstruction — both to preserve the observable contract
2981
+ * shared with the Cosmos provider and because the compiler's prefix walk at
2982
+ * each depth is intentionally unfiltered so deeper layers stay reachable
2983
+ * through any intermediate.
2984
+ */
2985
+ async exploreNeighborhood(repositoryId, entityId, options) {
2986
+ const layers = [];
2987
+ const visited = /* @__PURE__ */ new Set([entityId]);
2988
+ let frontier = /* @__PURE__ */ new Set([entityId]);
2989
+ for (let d = 1; d <= options.depth; d++) {
2990
+ if (frontier.size === 0) break;
2991
+ const spec = {
2992
+ start: { entityId },
2993
+ steps: buildExploreSteps(d, options),
2994
+ returnMode: "all",
2995
+ // The cumulative-d query fetches every node and edge reachable in ≤d
2996
+ // hops in either direction. Size the limit generously so a single
2997
+ // round-trip can hold the layer's full graph regardless of fan-out.
2998
+ limit: 1e4,
2999
+ detailLevel: "full",
3000
+ includeProvenance: true
3001
+ };
3002
+ const raw = await this.executeRawTraversal(repositoryId, spec);
3003
+ const edgesByVertex = /* @__PURE__ */ new Map();
3004
+ for (const rel of raw.allRelationships) {
3005
+ const a = edgesByVertex.get(rel.sourceEntityId);
3006
+ if (a) a.push(rel);
3007
+ else edgesByVertex.set(rel.sourceEntityId, [rel]);
3008
+ const b = edgesByVertex.get(rel.targetEntityId);
3009
+ if (b) b.push(rel);
3010
+ else edgesByVertex.set(rel.targetEntityId, [rel]);
3011
+ }
3012
+ const layer = {};
3013
+ const nextFrontier = /* @__PURE__ */ new Set();
3014
+ const layerBucketSeen = /* @__PURE__ */ new Map();
3015
+ for (const fv of frontier) {
3016
+ const incident = edgesByVertex.get(fv) ?? [];
3017
+ for (const rel of incident) {
3018
+ const isSource = rel.sourceEntityId === fv;
3019
+ const isTarget = rel.targetEntityId === fv;
3020
+ let matchesDirection = false;
3021
+ let connectedId;
3022
+ if (isSource && (options.direction === "out" || options.direction === "both")) {
3023
+ matchesDirection = true;
3024
+ connectedId = rel.targetEntityId;
3025
+ } else if (isTarget && (options.direction === "in" || options.direction === "both")) {
3026
+ matchesDirection = true;
3027
+ connectedId = rel.sourceEntityId;
3028
+ } else if (rel.bidirectional) {
3029
+ if (isSource && options.direction === "in") {
3030
+ matchesDirection = true;
3031
+ connectedId = rel.targetEntityId;
3032
+ } else if (isTarget && options.direction === "out") {
3033
+ matchesDirection = true;
3034
+ connectedId = rel.sourceEntityId;
3035
+ }
3036
+ }
3037
+ if (!matchesDirection || !connectedId) continue;
3038
+ if (visited.has(connectedId)) continue;
3039
+ if (options.relationshipPropertyFilters && options.relationshipPropertyFilters.length > 0) {
3040
+ if (!(0, import_deep_memory8.matchesPropertyFilters)(rel.properties, options.relationshipPropertyFilters))
3041
+ continue;
3042
+ }
3043
+ const connectedEntity = raw.entityMap.get(connectedId);
3044
+ if (!connectedEntity) continue;
3045
+ if (options.entityTypes && options.entityTypes.length > 0 && !options.entityTypes.includes(connectedEntity.entityType)) {
3046
+ continue;
3047
+ }
3048
+ const relType = rel.relationshipType;
3049
+ let bucketSeen = layerBucketSeen.get(relType);
3050
+ if (!bucketSeen) {
3051
+ bucketSeen = /* @__PURE__ */ new Set();
3052
+ layerBucketSeen.set(relType, bucketSeen);
3053
+ }
3054
+ if (bucketSeen.has(connectedId)) continue;
3055
+ bucketSeen.add(connectedId);
3056
+ if (!layer[relType]) {
3057
+ layer[relType] = { total: 0, entities: [], relationships: [] };
3058
+ }
3059
+ layer[relType].entities.push(connectedEntity);
3060
+ layer[relType].relationships.push(rel);
3061
+ layer[relType].total = layer[relType].entities.length;
3062
+ nextFrontier.add(connectedId);
3063
+ }
3064
+ }
3065
+ for (const relType of Object.keys(layer)) {
3066
+ const group = layer[relType];
3067
+ const start = options.offsetPerType;
3068
+ const end = start + options.limitPerType;
3069
+ group.entities = group.entities.slice(start, end);
3070
+ group.relationships = group.relationships.slice(start, end);
3071
+ }
3072
+ if (Object.keys(layer).length > 0) {
3073
+ layers.push(layer);
3074
+ }
3075
+ for (const id of nextFrontier) visited.add(id);
3076
+ frontier = nextFrontier;
3077
+ }
3078
+ return { centerId: entityId, layers };
3079
+ }
3080
+ /**
3081
+ * Path finding between two entities. Single round-trip via a variable-length
3082
+ * `MATCH p = (s)-[*1..N]-(t)` pattern; the compiler's path-binding emission
3083
+ * lets the executor recover ordered nodes and relationships via `nodes(p)`
3084
+ * / `relationships(p)`. The default `DIFFERENT RELATIONSHIPS` match mode in
3085
+ * Cypher 25 prevents edge reuse within a single path — no explicit dedup
3086
+ * filter is needed.
3087
+ *
3088
+ * The traversal walks the graph topologically regardless of relationship
3089
+ * directionality (a path is defined by reachability, not semantic
3090
+ * direction); entity-type and relationship-property filters apply during
3091
+ * compilation, target filtering happens post-fetch.
3092
+ */
3093
+ async findPaths(repositoryId, sourceId, targetId, options) {
3094
+ if (sourceId === targetId) {
3095
+ return { paths: [{ entityIds: [sourceId], relationshipIds: [] }], totalPaths: 1 };
3096
+ }
3097
+ const step = {
3098
+ direction: "both",
3099
+ repeat: { maxDepth: options.maxDepth, emitIntermediates: true }
3100
+ };
3101
+ if (options.relationshipTypes && options.relationshipTypes.length > 0) {
3102
+ step.relationshipTypes = options.relationshipTypes;
3103
+ }
3104
+ if (options.relationshipPropertyFilters && options.relationshipPropertyFilters.length > 0) {
3105
+ step.relationshipFilter = options.relationshipPropertyFilters;
3106
+ }
3107
+ const spec = {
3108
+ start: { entityId: sourceId },
3109
+ steps: [step],
3110
+ returnMode: "path",
3111
+ // Pull a generous candidate pool so the post-fetch filter (paths ending
3112
+ // at targetId) has enough rows to paginate from. The variable-length
3113
+ // pattern returns every walk of length ≤ maxDepth in one round-trip.
3114
+ limit: Math.max(options.limit + options.offset, options.limit) * 10,
3115
+ detailLevel: "full",
3116
+ includeProvenance: true
3117
+ };
3118
+ const raw = await this.executeRawTraversal(repositoryId, spec);
3119
+ const matchingPaths = [];
3120
+ for (const row of raw.pathRows) {
3121
+ const last = row.entityIds[row.entityIds.length - 1];
3122
+ if (last !== targetId) continue;
3123
+ if (new Set(row.entityIds).size !== row.entityIds.length) continue;
3124
+ if (options.entityTypes && options.entityTypes.length > 0) {
3125
+ let rejected = false;
3126
+ for (let i = 1; i < row.entityIds.length - 1; i++) {
3127
+ const intermediate = raw.entityMap.get(row.entityIds[i]);
3128
+ if (!intermediate) {
3129
+ rejected = true;
3130
+ break;
3131
+ }
3132
+ if (!options.entityTypes.includes(intermediate.entityType)) {
3133
+ rejected = true;
3134
+ break;
3135
+ }
3136
+ }
3137
+ if (rejected) continue;
3138
+ }
3139
+ matchingPaths.push({
3140
+ entityIds: [...row.entityIds],
3141
+ relationshipIds: [...row.relationshipIds]
3142
+ });
3143
+ }
3144
+ const paginated = matchingPaths.slice(options.offset, options.offset + options.limit);
3145
+ return {
3146
+ paths: paginated,
3147
+ totalPaths: matchingPaths.length
3148
+ };
3149
+ }
3150
+ // ─── Timeline ──────────────────────────────────────────────────────
3151
+ /**
3152
+ * Reconstruct the timeline event stream for an entity. One server
3153
+ * round-trip: the centre entity's provenance scalars plus every incident
3154
+ * edge's id + createdAt arrive in a single tuple via `OPTIONAL MATCH +
3155
+ * collect()`. The provider walks the row client-side to emit
3156
+ * `entity:created` / `entity:updated` / `relationship:created` events.
3157
+ *
3158
+ * Cosmos pays two round-trips for the same information because Gremlin
3159
+ * cannot bind an aggregated edge list to a vertex projection in one shot —
3160
+ * a platform divergence, not an inherent trade-off.
3161
+ */
3162
+ async getTimeline(repositoryId, entityId, options) {
3163
+ return getTimeline(this.connection, repositoryId, entityId, options);
3164
+ }
3165
+ // ─── Bulk Operations ───────────────────────────────────────────────
3166
+ /**
3167
+ * Stream every entity and every relationship in the repository as
3168
+ * cursor-paginated chunks. Entities come first, then relationships; each
3169
+ * chunk carries a monotonic `sequence` and an `isLast` flag.
3170
+ *
3171
+ * The Proxy-based tracking flow does not apply here. Tracked methods emit
3172
+ * one sink record at promise resolution; an `AsyncIterable` returns
3173
+ * synchronously, before any chunk has streamed. Instead, `trackIterable`
3174
+ * wraps the underlying generator in its own `UsageScope` that opens at
3175
+ * iterator creation, records each round-trip as the consumer pulls the
3176
+ * next chunk, and emits one sink record when the iterator drains — so the
3177
+ * resulting record aggregates server time across every chunk fetch.
3178
+ */
3179
+ exportAll(repositoryId) {
3180
+ return this.trackIterable(
3181
+ "exportAll",
3182
+ repositoryId,
3183
+ exportAll(this.connection, repositoryId)
3184
+ );
3185
+ }
3186
+ /**
3187
+ * Run a bulk import: every entity then every relationship from the input
3188
+ * chunks lands in the repository. `skipExistenceCheck: true` uses CREATE
3189
+ * for the absolute peak throughput; `false` (default) uses MERGE for
3190
+ * idempotent re-imports.
3191
+ *
3192
+ * Returns a single aggregate `BulkImportResult` spanning every chunk.
3193
+ * Per-row failures land in `result.errors`; surviving rows still count
3194
+ * toward `entitiesImported` / `relationshipsImported`.
3195
+ */
3196
+ async importBulk(repositoryId, data, options) {
3197
+ return importBulk(this.connection, repositoryId, data, options);
3198
+ }
3199
+ /**
3200
+ * Wrap an `AsyncIterable` so every chunk it produces is generated inside a
3201
+ * single shared `UsageScope`. The scope aggregates `summary.resultConsumedAfter`
3202
+ * across every round-trip the iterator performs; one sink record fires when
3203
+ * the iterator drains (or is closed / thrown into).
3204
+ *
3205
+ * Implementation mirrors the Cosmos `trackIterable` precedent: each
3206
+ * `iter.next()` call re-enters the scope via `runInUsageScope`. The
3207
+ * `AsyncLocalStorage` chain links async work performed inside the
3208
+ * generator body to the scope for the duration of that step. Between
3209
+ * `next()` calls the scope is dormant — the consumer's awaits do not
3210
+ * accumulate into it, which is exactly the desired semantics.
3211
+ *
3212
+ * When the sink is absent the wrap degenerates to a pass-through; the
3213
+ * Proxy's `createSafeSink` short-circuit covers the no-sink case at
3214
+ * construction time, so this method is only reached when `reportUsage` is
3215
+ * present.
3216
+ */
3217
+ trackIterable(operation, repositoryId, source) {
3218
+ const sink = this.reportUsage;
3219
+ if (sink === void 0) return source;
3220
+ return {
3221
+ [Symbol.asyncIterator]: () => {
3222
+ const iter = source[Symbol.asyncIterator]();
3223
+ const scope = createUsageScope();
3224
+ let emitted = false;
3225
+ const emit = () => {
3226
+ if (emitted) return;
3227
+ emitted = true;
3228
+ sink({
3229
+ provider: PROVIDER_NAME,
3230
+ operation,
3231
+ unit: "server_ms",
3232
+ value: scope.serverMs,
3233
+ repositoryId,
3234
+ timestamp: /* @__PURE__ */ new Date(),
3235
+ details: buildUsageDetails(scope)
3236
+ });
3237
+ };
3238
+ return {
3239
+ async next() {
3240
+ const step = await runInUsageScope(scope, () => iter.next());
3241
+ if (step.done) emit();
3242
+ return step;
3243
+ },
3244
+ async return(value) {
3245
+ emit();
3246
+ if (iter.return) return iter.return(value);
3247
+ return { done: true, value };
3248
+ },
3249
+ async throw(err) {
3250
+ emit();
3251
+ if (iter.throw) return iter.throw(err);
3252
+ throw err;
3253
+ }
3254
+ };
3255
+ }
3256
+ };
3257
+ }
3258
+ // ─── Stats ─────────────────────────────────────────────────────────
3259
+ /**
3260
+ * Aggregate repository statistics — entity / relationship totals, per-type
3261
+ * breakdowns, vocabulary version. Two parallel native-aggregation round-
3262
+ * trips (`count(n)` per `entityType`, `count(r)` per `type(r)`); the
3263
+ * vocabulary version comes from the cached `_Vocabulary` node so a warm
3264
+ * cache costs exactly two round-trips total.
3265
+ *
3266
+ * Strict improvement over Cosmos's Gremlin `.group().by().by(count())`
3267
+ * shape — Cypher's native aggregation collapses each metric to a one-
3268
+ * statement plan that hits the `(repositoryId, entityType)` and
3269
+ * relationship-property indexes directly.
3270
+ */
3271
+ async getRepositoryStats(repositoryId) {
3272
+ const vocabulary = await this.getVocabularyCached(repositoryId);
3273
+ return getRepositoryStats(this.connection, repositoryId, vocabulary);
3274
+ }
3275
+ // ─── Native Query ──────────────────────────────────────────────────
3276
+ /**
3277
+ * Execute a raw Cypher statement with caller-supplied bindings.
3278
+ *
3279
+ * ⚠️ ELEVATED PRIVILEGE — SYSTEM-LEVEL OPERATION ⚠️
3280
+ *
3281
+ * This method is an unscoped pass-through: it does not filter by
3282
+ * repository, does not inject the `$rid` binding, and performs no
3283
+ * validation on the Cypher string. A single call can read or mutate any
3284
+ * node or relationship in the database regardless of which repository it
3285
+ * belongs to.
3286
+ *
3287
+ * DO NOT expose this method to AI agents, end users, or any untrusted
3288
+ * caller. It is intended for:
3289
+ * - administrative tooling (migrations, diagnostics, repairs)
3290
+ * - internal library operations that need cross-repository reach
3291
+ *
3292
+ * `repositoryId` is accepted for interface symmetry but is intentionally
3293
+ * ignored here — the caller is trusted to scope the query themselves.
3294
+ * Because the call is cross-repository by design, the emitted usage
3295
+ * record carries no `repositoryId` (see `TRACKED_METHODS`).
3296
+ *
3297
+ * For agent-facing graph queries use {@link traverse}, which enforces the
3298
+ * repositoryId scope predicate.
3299
+ */
3300
+ async executeNativeQuery(_repositoryId, query, params) {
3301
+ const result = await this.connection.executeSystemQuery(query, params ?? {}, {
3302
+ crossRepository: true
3303
+ });
3304
+ return result.records.map((record) => record.toObject());
3305
+ }
3306
+ };
3307
+ function buildExploreSteps(depth, options) {
3308
+ const base = { direction: "both" };
3309
+ if (options.relationshipTypes && options.relationshipTypes.length > 0) {
3310
+ base.relationshipTypes = options.relationshipTypes;
3311
+ }
3312
+ const steps = [];
3313
+ for (let i = 0; i < depth; i++) {
3314
+ steps.push({ ...base });
3315
+ }
3316
+ return steps;
3317
+ }
3318
+ // Annotate the CommonJS export names for ESM import in node:
3319
+ 0 && (module.exports = {
3320
+ Neo4jStorageProvider,
3321
+ SCHEMA_VERSION,
3322
+ getSchemaCypher
3323
+ });
3324
+ //# sourceMappingURL=index.cjs.map