@utaba/deep-memory-storage-cosmosdb 0.5.1 → 0.7.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.d.cts CHANGED
@@ -32,6 +32,16 @@ declare class CosmosDbProvider implements StorageProvider, GraphTraversalProvide
32
32
  private readonly config;
33
33
  private readonly compiler;
34
34
  constructor(config: CosmosDbProviderConfig);
35
+ /**
36
+ * Reject any repositoryId that is not a valid v4 UUID.
37
+ *
38
+ * Every partition-scoped query filters on repositoryId. Since repositoryId is
39
+ * the CosmosDB partition key, a predicate with a different value would target
40
+ * a different partition. Strict format validation at the provider boundary
41
+ * prevents injection (e.g. strings containing quotes or Gremlin syntax) from
42
+ * ever reaching query construction.
43
+ */
44
+ private assertValidRepositoryId;
35
45
  initialize(): Promise<void>;
36
46
  dispose(): Promise<void>;
37
47
  ensureSchema(): Promise<EnsureSchemaResult>;
@@ -56,6 +66,10 @@ declare class CosmosDbProvider implements StorageProvider, GraphTraversalProvide
56
66
  getEntities(repositoryId: string, entityIds: string[]): Promise<Map<string, StoredEntity>>;
57
67
  updateEntity(repositoryId: string, entityId: string, updates: StoredEntityUpdate): Promise<StoredEntity>;
58
68
  deleteEntity(repositoryId: string, entityId: string): Promise<void>;
69
+ deleteEntities(repositoryId: string, ids: string[]): Promise<{
70
+ deleted: string[];
71
+ notFound: string[];
72
+ }>;
59
73
  deleteEntitiesByType(repositoryId: string, entityType: string): Promise<{
60
74
  deletedEntities: number;
61
75
  deletedRelationships: number;
@@ -65,6 +79,10 @@ declare class CosmosDbProvider implements StorageProvider, GraphTraversalProvide
65
79
  getRelationship(repositoryId: string, relationshipId: string): Promise<StoredRelationship | null>;
66
80
  getEntityRelationships(repositoryId: string, entityId: string, options?: RelationshipQueryOptions): Promise<PaginatedResult<StoredRelationship>>;
67
81
  deleteRelationship(repositoryId: string, relationshipId: string): Promise<void>;
82
+ deleteRelationships(repositoryId: string, ids: string[]): Promise<{
83
+ deleted: string[];
84
+ notFound: string[];
85
+ }>;
68
86
  deleteRelationshipsByType(repositoryId: string, relationshipType: string): Promise<{
69
87
  deletedRelationships: number;
70
88
  }>;
@@ -74,7 +92,28 @@ declare class CosmosDbProvider implements StorageProvider, GraphTraversalProvide
74
92
  exportAll(repositoryId: string): AsyncIterable<ExportChunk>;
75
93
  importBulk(repositoryId: string, data: ImportChunk[], options?: BulkImportOptions): Promise<BulkImportResult>;
76
94
  getCapabilities(): GraphTraversalCapabilities;
77
- traverse(repositoryId: string, spec: TraversalSpec, compiledQuery?: string): Promise<TraversalResult>;
95
+ traverse(repositoryId: string, spec: TraversalSpec): Promise<TraversalResult>;
96
+ /**
97
+ * Execute a raw Gremlin query with caller-supplied bindings.
98
+ *
99
+ * ⚠️ ELEVATED PRIVILEGE — SYSTEM-LEVEL OPERATION ⚠️
100
+ *
101
+ * This method is an unscoped pass-through: it does not filter by repository,
102
+ * does not inject the partition key, and performs no validation on the query
103
+ * string. A single call can read or mutate any vertex or edge in the
104
+ * container regardless of which repository (partition) it belongs to.
105
+ *
106
+ * DO NOT expose this method to AI agents, end users, or any untrusted caller.
107
+ * It is intended for:
108
+ * - administrative tooling (migrations, diagnostics, repairs)
109
+ * - internal library operations that need cross-partition reach
110
+ *
111
+ * `repositoryId` is accepted for interface symmetry but is intentionally
112
+ * ignored here — the caller is trusted to scope the query themselves.
113
+ *
114
+ * For agent-facing graph queries use {@link traverse}, which enforces the
115
+ * repositoryId partition predicate.
116
+ */
78
117
  executeNativeQuery(_repositoryId: string, query: string, params?: Record<string, unknown>): Promise<TraversalResult>;
79
118
  }
80
119
 
package/dist/index.d.ts CHANGED
@@ -32,6 +32,16 @@ declare class CosmosDbProvider implements StorageProvider, GraphTraversalProvide
32
32
  private readonly config;
33
33
  private readonly compiler;
34
34
  constructor(config: CosmosDbProviderConfig);
35
+ /**
36
+ * Reject any repositoryId that is not a valid v4 UUID.
37
+ *
38
+ * Every partition-scoped query filters on repositoryId. Since repositoryId is
39
+ * the CosmosDB partition key, a predicate with a different value would target
40
+ * a different partition. Strict format validation at the provider boundary
41
+ * prevents injection (e.g. strings containing quotes or Gremlin syntax) from
42
+ * ever reaching query construction.
43
+ */
44
+ private assertValidRepositoryId;
35
45
  initialize(): Promise<void>;
36
46
  dispose(): Promise<void>;
37
47
  ensureSchema(): Promise<EnsureSchemaResult>;
@@ -56,6 +66,10 @@ declare class CosmosDbProvider implements StorageProvider, GraphTraversalProvide
56
66
  getEntities(repositoryId: string, entityIds: string[]): Promise<Map<string, StoredEntity>>;
57
67
  updateEntity(repositoryId: string, entityId: string, updates: StoredEntityUpdate): Promise<StoredEntity>;
58
68
  deleteEntity(repositoryId: string, entityId: string): Promise<void>;
69
+ deleteEntities(repositoryId: string, ids: string[]): Promise<{
70
+ deleted: string[];
71
+ notFound: string[];
72
+ }>;
59
73
  deleteEntitiesByType(repositoryId: string, entityType: string): Promise<{
60
74
  deletedEntities: number;
61
75
  deletedRelationships: number;
@@ -65,6 +79,10 @@ declare class CosmosDbProvider implements StorageProvider, GraphTraversalProvide
65
79
  getRelationship(repositoryId: string, relationshipId: string): Promise<StoredRelationship | null>;
66
80
  getEntityRelationships(repositoryId: string, entityId: string, options?: RelationshipQueryOptions): Promise<PaginatedResult<StoredRelationship>>;
67
81
  deleteRelationship(repositoryId: string, relationshipId: string): Promise<void>;
82
+ deleteRelationships(repositoryId: string, ids: string[]): Promise<{
83
+ deleted: string[];
84
+ notFound: string[];
85
+ }>;
68
86
  deleteRelationshipsByType(repositoryId: string, relationshipType: string): Promise<{
69
87
  deletedRelationships: number;
70
88
  }>;
@@ -74,7 +92,28 @@ declare class CosmosDbProvider implements StorageProvider, GraphTraversalProvide
74
92
  exportAll(repositoryId: string): AsyncIterable<ExportChunk>;
75
93
  importBulk(repositoryId: string, data: ImportChunk[], options?: BulkImportOptions): Promise<BulkImportResult>;
76
94
  getCapabilities(): GraphTraversalCapabilities;
77
- traverse(repositoryId: string, spec: TraversalSpec, compiledQuery?: string): Promise<TraversalResult>;
95
+ traverse(repositoryId: string, spec: TraversalSpec): Promise<TraversalResult>;
96
+ /**
97
+ * Execute a raw Gremlin query with caller-supplied bindings.
98
+ *
99
+ * ⚠️ ELEVATED PRIVILEGE — SYSTEM-LEVEL OPERATION ⚠️
100
+ *
101
+ * This method is an unscoped pass-through: it does not filter by repository,
102
+ * does not inject the partition key, and performs no validation on the query
103
+ * string. A single call can read or mutate any vertex or edge in the
104
+ * container regardless of which repository (partition) it belongs to.
105
+ *
106
+ * DO NOT expose this method to AI agents, end users, or any untrusted caller.
107
+ * It is intended for:
108
+ * - administrative tooling (migrations, diagnostics, repairs)
109
+ * - internal library operations that need cross-partition reach
110
+ *
111
+ * `repositoryId` is accepted for interface symmetry but is intentionally
112
+ * ignored here — the caller is trusted to scope the query themselves.
113
+ *
114
+ * For agent-facing graph queries use {@link traverse}, which enforces the
115
+ * repositoryId partition predicate.
116
+ */
78
117
  executeNativeQuery(_repositoryId: string, query: string, params?: Record<string, unknown>): Promise<TraversalResult>;
79
118
  }
80
119
 
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/CosmosDbProvider.ts
2
2
  import crypto from "crypto";
3
- import { GremlinCompiler, ProviderError } from "@utaba/deep-memory";
3
+ import { GremlinCompiler, ProviderError, InvalidInputError, isValidUuid, projectEntity } from "@utaba/deep-memory";
4
4
 
5
5
  // src/CosmosDbConnection.ts
6
6
  import gremlin from "gremlin";
@@ -635,12 +635,19 @@ async function updateEntity(conn, repositoryId, entityId, updates) {
635
635
  bindings[paramName] = value;
636
636
  propParts.push(`.property('${key}', ${paramName})`);
637
637
  };
638
+ const dropProp = (key) => {
639
+ propParts.push(`.sideEffect(properties('${key}').drop())`);
640
+ };
641
+ if (updates.entityType !== void 0) addProp("entityType", updates.entityType);
638
642
  if (updates.label !== void 0) addProp("entityLabel", updates.label);
639
643
  if (updates.slug !== void 0) addProp("slug", updates.slug);
640
- if (updates.summary !== void 0) addProp("summary", updates.summary);
644
+ if (updates.summary === null) dropProp("summary");
645
+ else if (updates.summary !== void 0) addProp("summary", updates.summary);
641
646
  if (updates.properties !== void 0) addProp("properties", JSON.stringify(updates.properties));
642
- if (updates.data !== void 0) addProp("data", updates.data);
643
- if (updates.dataFormat !== void 0) addProp("dataFormat", updates.dataFormat);
647
+ if (updates.data === null) dropProp("data");
648
+ else if (updates.data !== void 0) addProp("data", updates.data);
649
+ if (updates.dataFormat === null) dropProp("dataFormat");
650
+ else if (updates.dataFormat !== void 0) addProp("dataFormat", updates.dataFormat);
644
651
  if (updates.embedding !== void 0) addProp("embedding", JSON.stringify(updates.embedding));
645
652
  addProp("modifiedBy", updates.provenance.modifiedBy);
646
653
  addProp("modifiedByType", updates.provenance.modifiedByType);
@@ -688,9 +695,37 @@ async function findEntities(conn, repositoryId, query) {
688
695
  });
689
696
  filterClause += `.has('entityType', within(${typeParams.join(", ")}))`;
690
697
  }
691
- if (query.searchTerm) {
692
- bindings["searchTerm"] = query.searchTerm.toLowerCase();
693
- filterClause += ".has('slug', containing(searchTerm))";
698
+ const hasPropertyFilter = query.properties != null && Object.keys(query.properties).length > 0;
699
+ const needsClientFilter = Boolean(query.searchTerm) || hasPropertyFilter;
700
+ if (needsClientFilter) {
701
+ const dataResult2 = await conn.submit(
702
+ `g.V()${filterClause}.valueMap(true)`,
703
+ bindings
704
+ );
705
+ let items2 = dataResult2.items.map(entityFromGremlin);
706
+ if (query.searchTerm) {
707
+ const term = query.searchTerm.toLowerCase();
708
+ items2 = items2.filter(
709
+ (e) => e.label.toLowerCase().includes(term) || e.slug.toLowerCase().includes(term) || e.summary != null && e.summary.toLowerCase().includes(term)
710
+ );
711
+ }
712
+ if (hasPropertyFilter) {
713
+ items2 = items2.filter((entity) => {
714
+ for (const [key, value] of Object.entries(query.properties)) {
715
+ if (entity.properties[key] !== value) return false;
716
+ }
717
+ return true;
718
+ });
719
+ }
720
+ const total2 = items2.length;
721
+ const paged = items2.slice(query.offset, query.offset + query.limit);
722
+ return {
723
+ items: paged,
724
+ total: total2,
725
+ hasMore: query.offset + query.limit < total2,
726
+ limit: query.limit,
727
+ offset: query.offset
728
+ };
694
729
  }
695
730
  const countResult = await conn.submit(
696
731
  `g.V()${filterClause}.count()`,
@@ -703,15 +738,7 @@ async function findEntities(conn, repositoryId, query) {
703
738
  `g.V()${filterClause}.range(rangeStart, rangeEnd).valueMap(true)`,
704
739
  bindings
705
740
  );
706
- let items = dataResult.items.map(entityFromGremlin);
707
- if (query.properties && Object.keys(query.properties).length > 0) {
708
- items = items.filter((entity) => {
709
- for (const [key, value] of Object.entries(query.properties)) {
710
- if (entity.properties[key] !== value) return false;
711
- }
712
- return true;
713
- });
714
- }
741
+ const items = dataResult.items.map(entityFromGremlin);
715
742
  return {
716
743
  items,
717
744
  total,
@@ -1282,6 +1309,23 @@ var CosmosDbProvider = class {
1282
1309
  rejectUnauthorized: config.rejectUnauthorized
1283
1310
  });
1284
1311
  }
1312
+ /**
1313
+ * Reject any repositoryId that is not a valid v4 UUID.
1314
+ *
1315
+ * Every partition-scoped query filters on repositoryId. Since repositoryId is
1316
+ * the CosmosDB partition key, a predicate with a different value would target
1317
+ * a different partition. Strict format validation at the provider boundary
1318
+ * prevents injection (e.g. strings containing quotes or Gremlin syntax) from
1319
+ * ever reaching query construction.
1320
+ */
1321
+ assertValidRepositoryId(repositoryId) {
1322
+ if (!isValidUuid(repositoryId)) {
1323
+ throw new InvalidInputError(
1324
+ "repositoryId",
1325
+ `repositoryId must be a valid v4 UUID; got '${repositoryId}'`
1326
+ );
1327
+ }
1328
+ }
1285
1329
  // ─── Lifecycle ─────────────────────────────────────────────────────
1286
1330
  async initialize() {
1287
1331
  await this.conn.connect();
@@ -1367,93 +1411,195 @@ var CosmosDbProvider = class {
1367
1411
  }
1368
1412
  // ─── Repository ────────────────────────────────────────────────────
1369
1413
  async createRepository(config) {
1414
+ this.assertValidRepositoryId(config.repositoryId);
1370
1415
  return createRepository(this.conn, config);
1371
1416
  }
1372
1417
  async getRepository(repositoryId) {
1418
+ this.assertValidRepositoryId(repositoryId);
1373
1419
  return getRepository(this.conn, repositoryId);
1374
1420
  }
1375
1421
  async listRepositories(filter) {
1376
1422
  return listRepositories(this.conn, filter);
1377
1423
  }
1378
1424
  async updateRepository(repositoryId, updates) {
1425
+ this.assertValidRepositoryId(repositoryId);
1379
1426
  return updateRepository(this.conn, repositoryId, updates);
1380
1427
  }
1381
1428
  async deleteRepository(repositoryId, onProgress) {
1429
+ this.assertValidRepositoryId(repositoryId);
1382
1430
  return deleteRepository(this.conn, repositoryId, onProgress);
1383
1431
  }
1384
1432
  async deleteAllContents(repositoryId, onProgress) {
1433
+ this.assertValidRepositoryId(repositoryId);
1385
1434
  return deleteAllContents(this.conn, repositoryId, onProgress);
1386
1435
  }
1387
1436
  async getRepositoryStats(repositoryId) {
1437
+ this.assertValidRepositoryId(repositoryId);
1388
1438
  return getRepositoryStats(this.conn, repositoryId);
1389
1439
  }
1390
1440
  // ─── Vocabulary ────────────────────────────────────────────────────
1391
1441
  async getVocabulary(repositoryId) {
1442
+ this.assertValidRepositoryId(repositoryId);
1392
1443
  return getVocabulary(this.conn, repositoryId);
1393
1444
  }
1394
1445
  async saveVocabulary(repositoryId, vocabulary) {
1446
+ this.assertValidRepositoryId(repositoryId);
1395
1447
  return saveVocabulary(this.conn, repositoryId, vocabulary);
1396
1448
  }
1397
1449
  async getVocabularyChangeLog(repositoryId, options) {
1450
+ this.assertValidRepositoryId(repositoryId);
1398
1451
  return getVocabularyChangeLog(this.conn, repositoryId, options);
1399
1452
  }
1400
1453
  // ─── Entities ──────────────────────────────────────────────────────
1401
1454
  async createEntity(repositoryId, entity) {
1455
+ this.assertValidRepositoryId(repositoryId);
1402
1456
  return createEntity(this.conn, repositoryId, entity);
1403
1457
  }
1404
1458
  async getEntity(repositoryId, entityId) {
1459
+ this.assertValidRepositoryId(repositoryId);
1405
1460
  return getEntity(this.conn, repositoryId, entityId);
1406
1461
  }
1407
1462
  async getEntityBySlug(repositoryId, slug) {
1463
+ this.assertValidRepositoryId(repositoryId);
1408
1464
  return getEntityBySlug(this.conn, repositoryId, slug);
1409
1465
  }
1410
1466
  async getEntities(repositoryId, entityIds) {
1467
+ this.assertValidRepositoryId(repositoryId);
1411
1468
  return getEntities(this.conn, repositoryId, entityIds);
1412
1469
  }
1413
1470
  async updateEntity(repositoryId, entityId, updates) {
1471
+ this.assertValidRepositoryId(repositoryId);
1414
1472
  return updateEntity(this.conn, repositoryId, entityId, updates);
1415
1473
  }
1416
1474
  async deleteEntity(repositoryId, entityId) {
1475
+ this.assertValidRepositoryId(repositoryId);
1417
1476
  return deleteEntity(this.conn, repositoryId, entityId);
1418
1477
  }
1478
+ async deleteEntities(repositoryId, ids) {
1479
+ if (ids.length === 0) return { deleted: [], notFound: [] };
1480
+ this.assertValidRepositoryId(repositoryId);
1481
+ const deleted = [];
1482
+ const CHUNK = 100;
1483
+ for (let i = 0; i < ids.length; i += CHUNK) {
1484
+ const chunk = ids.slice(i, i + CHUNK);
1485
+ const bindings = { rid: repositoryId };
1486
+ const idParams = [];
1487
+ for (let j = 0; j < chunk.length; j++) {
1488
+ const p = `id${j}`;
1489
+ bindings[p] = chunk[j];
1490
+ idParams.push(p);
1491
+ }
1492
+ const withinExpr = `P.within(${idParams.join(", ")})`;
1493
+ const existResult = await this.conn.submit(
1494
+ `g.V().has('repositoryId', rid).has('id', ${withinExpr}).has('entityType').values('id')`,
1495
+ bindings
1496
+ );
1497
+ const found = existResult.items;
1498
+ if (found.length > 0) {
1499
+ const dropBindings = { rid: repositoryId };
1500
+ const dropIdParams = [];
1501
+ for (let j = 0; j < found.length; j++) {
1502
+ const p = `did${j}`;
1503
+ dropBindings[p] = found[j];
1504
+ dropIdParams.push(p);
1505
+ }
1506
+ const dropWithinExpr = `P.within(${dropIdParams.join(", ")})`;
1507
+ await this.conn.submit(
1508
+ `g.V().has('repositoryId', rid).has('id', ${dropWithinExpr}).has('entityType').drop()`,
1509
+ dropBindings
1510
+ );
1511
+ deleted.push(...found);
1512
+ }
1513
+ }
1514
+ const deletedSet = new Set(deleted);
1515
+ return { deleted, notFound: ids.filter((id) => !deletedSet.has(id)) };
1516
+ }
1419
1517
  async deleteEntitiesByType(repositoryId, entityType) {
1518
+ this.assertValidRepositoryId(repositoryId);
1420
1519
  return deleteEntitiesByType(this.conn, repositoryId, entityType);
1421
1520
  }
1422
1521
  async findEntities(repositoryId, query) {
1522
+ this.assertValidRepositoryId(repositoryId);
1423
1523
  return findEntities(this.conn, repositoryId, query);
1424
1524
  }
1425
1525
  // ─── Relationships ─────────────────────────────────────────────────
1426
1526
  async createRelationship(repositoryId, relationship) {
1527
+ this.assertValidRepositoryId(repositoryId);
1427
1528
  return createRelationship(this.conn, repositoryId, relationship);
1428
1529
  }
1429
1530
  async getRelationship(repositoryId, relationshipId) {
1531
+ this.assertValidRepositoryId(repositoryId);
1430
1532
  return getRelationship(this.conn, repositoryId, relationshipId);
1431
1533
  }
1432
1534
  async getEntityRelationships(repositoryId, entityId, options) {
1535
+ this.assertValidRepositoryId(repositoryId);
1433
1536
  return getEntityRelationships(this.conn, repositoryId, entityId, options);
1434
1537
  }
1435
1538
  async deleteRelationship(repositoryId, relationshipId) {
1539
+ this.assertValidRepositoryId(repositoryId);
1436
1540
  return deleteRelationship(this.conn, repositoryId, relationshipId);
1437
1541
  }
1542
+ async deleteRelationships(repositoryId, ids) {
1543
+ if (ids.length === 0) return { deleted: [], notFound: [] };
1544
+ this.assertValidRepositoryId(repositoryId);
1545
+ const deleted = [];
1546
+ const CHUNK = 100;
1547
+ for (let i = 0; i < ids.length; i += CHUNK) {
1548
+ const chunk = ids.slice(i, i + CHUNK);
1549
+ const bindings = { rid: repositoryId };
1550
+ const idParams = [];
1551
+ for (let j = 0; j < chunk.length; j++) {
1552
+ const p = `id${j}`;
1553
+ bindings[p] = chunk[j];
1554
+ idParams.push(p);
1555
+ }
1556
+ const withinExpr = `P.within(${idParams.join(", ")})`;
1557
+ const existResult = await this.conn.submit(
1558
+ `g.E().has('repositoryId', rid).has('id', ${withinExpr}).values('id')`,
1559
+ bindings
1560
+ );
1561
+ const found = existResult.items;
1562
+ if (found.length > 0) {
1563
+ await cosmosRestBatchDelete(
1564
+ this.getRestEndpoint(),
1565
+ this.config.key,
1566
+ this.config.database,
1567
+ this.config.container,
1568
+ repositoryId,
1569
+ found,
1570
+ this.config.rejectUnauthorized ?? true
1571
+ );
1572
+ deleted.push(...found);
1573
+ }
1574
+ }
1575
+ const deletedSet = new Set(deleted);
1576
+ return { deleted, notFound: ids.filter((id) => !deletedSet.has(id)) };
1577
+ }
1438
1578
  async deleteRelationshipsByType(repositoryId, relationshipType) {
1579
+ this.assertValidRepositoryId(repositoryId);
1439
1580
  return deleteRelationshipsByType(this.conn, repositoryId, relationshipType);
1440
1581
  }
1441
1582
  // ─── Graph Traversal (StorageProvider) ─────────────────────────────
1442
1583
  async exploreNeighborhood(repositoryId, entityId, options) {
1584
+ this.assertValidRepositoryId(repositoryId);
1443
1585
  return exploreNeighborhood(this.conn, repositoryId, entityId, options);
1444
1586
  }
1445
1587
  async findPaths(repositoryId, sourceId, targetId, options) {
1588
+ this.assertValidRepositoryId(repositoryId);
1446
1589
  return findPaths(this.conn, repositoryId, sourceId, targetId, options);
1447
1590
  }
1448
1591
  // ─── Timeline ──────────────────────────────────────────────────────
1449
1592
  async getTimeline(repositoryId, entityId, options) {
1593
+ this.assertValidRepositoryId(repositoryId);
1450
1594
  return getTimeline(this.conn, repositoryId, entityId, options);
1451
1595
  }
1452
1596
  // ─── Bulk Operations ───────────────────────────────────────────────
1453
1597
  exportAll(repositoryId) {
1598
+ this.assertValidRepositoryId(repositoryId);
1454
1599
  return exportAll(this.conn, repositoryId);
1455
1600
  }
1456
1601
  async importBulk(repositoryId, data, options) {
1602
+ this.assertValidRepositoryId(repositoryId);
1457
1603
  return importBulk(this.conn, repositoryId, data, options);
1458
1604
  }
1459
1605
  // ─── GraphTraversalProvider ────────────────────────────────────────
@@ -1470,17 +1616,20 @@ var CosmosDbProvider = class {
1470
1616
  supportsRelationshipSummary: false
1471
1617
  };
1472
1618
  }
1473
- async traverse(repositoryId, spec, compiledQuery) {
1619
+ async traverse(repositoryId, spec) {
1620
+ this.assertValidRepositoryId(repositoryId);
1474
1621
  const startTime = Date.now();
1475
1622
  const vocabulary = await this.getVocabulary(repositoryId);
1476
- const compiled = compiledQuery ? { query: compiledQuery, params: {}, estimatedFanOut: 100 } : this.compiler.compile(spec, vocabulary);
1623
+ const compiled = this.compiler.compile(spec, vocabulary);
1477
1624
  const scopedQuery = compiled.query.replace(
1478
1625
  "g.V()",
1479
- `g.V().has('repositoryId', '${repositoryId}')`
1626
+ "g.V().has('repositoryId', pRid)"
1480
1627
  );
1628
+ const scopedParams = { ...compiled.params, pRid: repositoryId };
1481
1629
  try {
1482
- const result = await this.conn.submit(scopedQuery, compiled.params);
1630
+ const result = await this.conn.submit(scopedQuery, scopedParams);
1483
1631
  const executionTimeMs = Date.now() - startTime;
1632
+ const detailLevel = spec.detailLevel ?? "summary";
1484
1633
  const entities = [];
1485
1634
  const relationships = [];
1486
1635
  const paths = [];
@@ -1492,7 +1641,8 @@ var CosmosDbProvider = class {
1492
1641
  for (const obj of pathData.objects) {
1493
1642
  const props = obj;
1494
1643
  if (props["entityType"]) {
1495
- pathEntities.push(entityFromGremlin(props));
1644
+ const stored = entityFromGremlin(props);
1645
+ pathEntities.push(projectEntity(stored, detailLevel));
1496
1646
  }
1497
1647
  }
1498
1648
  if (pathEntities.length > 0) {
@@ -1506,8 +1656,8 @@ var CosmosDbProvider = class {
1506
1656
  }
1507
1657
  } else {
1508
1658
  for (const item of result.items) {
1509
- const entity = entityFromGremlin(item);
1510
- entities.push(entity);
1659
+ const stored = entityFromGremlin(item);
1660
+ entities.push(projectEntity(stored, detailLevel));
1511
1661
  }
1512
1662
  }
1513
1663
  const limit = spec.limit ?? 50;
@@ -1539,6 +1689,27 @@ var CosmosDbProvider = class {
1539
1689
  );
1540
1690
  }
1541
1691
  }
1692
+ /**
1693
+ * Execute a raw Gremlin query with caller-supplied bindings.
1694
+ *
1695
+ * ⚠️ ELEVATED PRIVILEGE — SYSTEM-LEVEL OPERATION ⚠️
1696
+ *
1697
+ * This method is an unscoped pass-through: it does not filter by repository,
1698
+ * does not inject the partition key, and performs no validation on the query
1699
+ * string. A single call can read or mutate any vertex or edge in the
1700
+ * container regardless of which repository (partition) it belongs to.
1701
+ *
1702
+ * DO NOT expose this method to AI agents, end users, or any untrusted caller.
1703
+ * It is intended for:
1704
+ * - administrative tooling (migrations, diagnostics, repairs)
1705
+ * - internal library operations that need cross-partition reach
1706
+ *
1707
+ * `repositoryId` is accepted for interface symmetry but is intentionally
1708
+ * ignored here — the caller is trusted to scope the query themselves.
1709
+ *
1710
+ * For agent-facing graph queries use {@link traverse}, which enforces the
1711
+ * repositoryId partition predicate.
1712
+ */
1542
1713
  async executeNativeQuery(_repositoryId, query, params) {
1543
1714
  const startTime = Date.now();
1544
1715
  try {
@@ -1617,6 +1788,32 @@ async function cosmosRestPut(restBase, key, urlPath, resourceLink, resourceType,
1617
1788
  const text = await response.text();
1618
1789
  throw new Error(`CosmosDB REST ${response.status}: ${text}`);
1619
1790
  }
1791
+ async function cosmosRestBatchDelete(restBase, key, database, container, partitionKeyValue, ids, rejectUnauthorized) {
1792
+ const resourceLink = `dbs/${database}/colls/${container}`;
1793
+ const date = (/* @__PURE__ */ new Date()).toUTCString();
1794
+ const token = cosmosAuthToken("post", "docs", resourceLink, date, key);
1795
+ if (!rejectUnauthorized) {
1796
+ process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
1797
+ }
1798
+ const ops = ids.map((id) => ({ operationType: "Delete", id }));
1799
+ const response = await fetch(`${restBase}/${resourceLink}/docs`, {
1800
+ method: "POST",
1801
+ headers: {
1802
+ "Authorization": token,
1803
+ "x-ms-version": "2020-07-15",
1804
+ "x-ms-date": date,
1805
+ "Content-Type": "application/json",
1806
+ "x-ms-documentdb-partitionkey": JSON.stringify([partitionKeyValue]),
1807
+ "x-ms-cosmos-is-batch-request": "true",
1808
+ "x-ms-cosmos-batch-atomic": "true"
1809
+ },
1810
+ body: JSON.stringify(ops)
1811
+ });
1812
+ if (response.status !== 200 && response.status !== 207) {
1813
+ const text = await response.text();
1814
+ throw new ProviderError(`CosmosDB batch delete failed (${response.status}): ${text}`);
1815
+ }
1816
+ }
1620
1817
  export {
1621
1818
  CosmosDbConnection,
1622
1819
  CosmosDbProvider