@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.cjs CHANGED
@@ -672,12 +672,19 @@ async function updateEntity(conn, repositoryId, entityId, updates) {
672
672
  bindings[paramName] = value;
673
673
  propParts.push(`.property('${key}', ${paramName})`);
674
674
  };
675
+ const dropProp = (key) => {
676
+ propParts.push(`.sideEffect(properties('${key}').drop())`);
677
+ };
678
+ if (updates.entityType !== void 0) addProp("entityType", updates.entityType);
675
679
  if (updates.label !== void 0) addProp("entityLabel", updates.label);
676
680
  if (updates.slug !== void 0) addProp("slug", updates.slug);
677
- if (updates.summary !== void 0) addProp("summary", updates.summary);
681
+ if (updates.summary === null) dropProp("summary");
682
+ else if (updates.summary !== void 0) addProp("summary", updates.summary);
678
683
  if (updates.properties !== void 0) addProp("properties", JSON.stringify(updates.properties));
679
- if (updates.data !== void 0) addProp("data", updates.data);
680
- if (updates.dataFormat !== void 0) addProp("dataFormat", updates.dataFormat);
684
+ if (updates.data === null) dropProp("data");
685
+ else if (updates.data !== void 0) addProp("data", updates.data);
686
+ if (updates.dataFormat === null) dropProp("dataFormat");
687
+ else if (updates.dataFormat !== void 0) addProp("dataFormat", updates.dataFormat);
681
688
  if (updates.embedding !== void 0) addProp("embedding", JSON.stringify(updates.embedding));
682
689
  addProp("modifiedBy", updates.provenance.modifiedBy);
683
690
  addProp("modifiedByType", updates.provenance.modifiedByType);
@@ -725,9 +732,37 @@ async function findEntities(conn, repositoryId, query) {
725
732
  });
726
733
  filterClause += `.has('entityType', within(${typeParams.join(", ")}))`;
727
734
  }
728
- if (query.searchTerm) {
729
- bindings["searchTerm"] = query.searchTerm.toLowerCase();
730
- filterClause += ".has('slug', containing(searchTerm))";
735
+ const hasPropertyFilter = query.properties != null && Object.keys(query.properties).length > 0;
736
+ const needsClientFilter = Boolean(query.searchTerm) || hasPropertyFilter;
737
+ if (needsClientFilter) {
738
+ const dataResult2 = await conn.submit(
739
+ `g.V()${filterClause}.valueMap(true)`,
740
+ bindings
741
+ );
742
+ let items2 = dataResult2.items.map(entityFromGremlin);
743
+ if (query.searchTerm) {
744
+ const term = query.searchTerm.toLowerCase();
745
+ items2 = items2.filter(
746
+ (e) => e.label.toLowerCase().includes(term) || e.slug.toLowerCase().includes(term) || e.summary != null && e.summary.toLowerCase().includes(term)
747
+ );
748
+ }
749
+ if (hasPropertyFilter) {
750
+ items2 = items2.filter((entity) => {
751
+ for (const [key, value] of Object.entries(query.properties)) {
752
+ if (entity.properties[key] !== value) return false;
753
+ }
754
+ return true;
755
+ });
756
+ }
757
+ const total2 = items2.length;
758
+ const paged = items2.slice(query.offset, query.offset + query.limit);
759
+ return {
760
+ items: paged,
761
+ total: total2,
762
+ hasMore: query.offset + query.limit < total2,
763
+ limit: query.limit,
764
+ offset: query.offset
765
+ };
731
766
  }
732
767
  const countResult = await conn.submit(
733
768
  `g.V()${filterClause}.count()`,
@@ -740,15 +775,7 @@ async function findEntities(conn, repositoryId, query) {
740
775
  `g.V()${filterClause}.range(rangeStart, rangeEnd).valueMap(true)`,
741
776
  bindings
742
777
  );
743
- let items = dataResult.items.map(entityFromGremlin);
744
- if (query.properties && Object.keys(query.properties).length > 0) {
745
- items = items.filter((entity) => {
746
- for (const [key, value] of Object.entries(query.properties)) {
747
- if (entity.properties[key] !== value) return false;
748
- }
749
- return true;
750
- });
751
- }
778
+ const items = dataResult.items.map(entityFromGremlin);
752
779
  return {
753
780
  items,
754
781
  total,
@@ -1319,6 +1346,23 @@ var CosmosDbProvider = class {
1319
1346
  rejectUnauthorized: config.rejectUnauthorized
1320
1347
  });
1321
1348
  }
1349
+ /**
1350
+ * Reject any repositoryId that is not a valid v4 UUID.
1351
+ *
1352
+ * Every partition-scoped query filters on repositoryId. Since repositoryId is
1353
+ * the CosmosDB partition key, a predicate with a different value would target
1354
+ * a different partition. Strict format validation at the provider boundary
1355
+ * prevents injection (e.g. strings containing quotes or Gremlin syntax) from
1356
+ * ever reaching query construction.
1357
+ */
1358
+ assertValidRepositoryId(repositoryId) {
1359
+ if (!(0, import_deep_memory5.isValidUuid)(repositoryId)) {
1360
+ throw new import_deep_memory5.InvalidInputError(
1361
+ "repositoryId",
1362
+ `repositoryId must be a valid v4 UUID; got '${repositoryId}'`
1363
+ );
1364
+ }
1365
+ }
1322
1366
  // ─── Lifecycle ─────────────────────────────────────────────────────
1323
1367
  async initialize() {
1324
1368
  await this.conn.connect();
@@ -1404,93 +1448,195 @@ var CosmosDbProvider = class {
1404
1448
  }
1405
1449
  // ─── Repository ────────────────────────────────────────────────────
1406
1450
  async createRepository(config) {
1451
+ this.assertValidRepositoryId(config.repositoryId);
1407
1452
  return createRepository(this.conn, config);
1408
1453
  }
1409
1454
  async getRepository(repositoryId) {
1455
+ this.assertValidRepositoryId(repositoryId);
1410
1456
  return getRepository(this.conn, repositoryId);
1411
1457
  }
1412
1458
  async listRepositories(filter) {
1413
1459
  return listRepositories(this.conn, filter);
1414
1460
  }
1415
1461
  async updateRepository(repositoryId, updates) {
1462
+ this.assertValidRepositoryId(repositoryId);
1416
1463
  return updateRepository(this.conn, repositoryId, updates);
1417
1464
  }
1418
1465
  async deleteRepository(repositoryId, onProgress) {
1466
+ this.assertValidRepositoryId(repositoryId);
1419
1467
  return deleteRepository(this.conn, repositoryId, onProgress);
1420
1468
  }
1421
1469
  async deleteAllContents(repositoryId, onProgress) {
1470
+ this.assertValidRepositoryId(repositoryId);
1422
1471
  return deleteAllContents(this.conn, repositoryId, onProgress);
1423
1472
  }
1424
1473
  async getRepositoryStats(repositoryId) {
1474
+ this.assertValidRepositoryId(repositoryId);
1425
1475
  return getRepositoryStats(this.conn, repositoryId);
1426
1476
  }
1427
1477
  // ─── Vocabulary ────────────────────────────────────────────────────
1428
1478
  async getVocabulary(repositoryId) {
1479
+ this.assertValidRepositoryId(repositoryId);
1429
1480
  return getVocabulary(this.conn, repositoryId);
1430
1481
  }
1431
1482
  async saveVocabulary(repositoryId, vocabulary) {
1483
+ this.assertValidRepositoryId(repositoryId);
1432
1484
  return saveVocabulary(this.conn, repositoryId, vocabulary);
1433
1485
  }
1434
1486
  async getVocabularyChangeLog(repositoryId, options) {
1487
+ this.assertValidRepositoryId(repositoryId);
1435
1488
  return getVocabularyChangeLog(this.conn, repositoryId, options);
1436
1489
  }
1437
1490
  // ─── Entities ──────────────────────────────────────────────────────
1438
1491
  async createEntity(repositoryId, entity) {
1492
+ this.assertValidRepositoryId(repositoryId);
1439
1493
  return createEntity(this.conn, repositoryId, entity);
1440
1494
  }
1441
1495
  async getEntity(repositoryId, entityId) {
1496
+ this.assertValidRepositoryId(repositoryId);
1442
1497
  return getEntity(this.conn, repositoryId, entityId);
1443
1498
  }
1444
1499
  async getEntityBySlug(repositoryId, slug) {
1500
+ this.assertValidRepositoryId(repositoryId);
1445
1501
  return getEntityBySlug(this.conn, repositoryId, slug);
1446
1502
  }
1447
1503
  async getEntities(repositoryId, entityIds) {
1504
+ this.assertValidRepositoryId(repositoryId);
1448
1505
  return getEntities(this.conn, repositoryId, entityIds);
1449
1506
  }
1450
1507
  async updateEntity(repositoryId, entityId, updates) {
1508
+ this.assertValidRepositoryId(repositoryId);
1451
1509
  return updateEntity(this.conn, repositoryId, entityId, updates);
1452
1510
  }
1453
1511
  async deleteEntity(repositoryId, entityId) {
1512
+ this.assertValidRepositoryId(repositoryId);
1454
1513
  return deleteEntity(this.conn, repositoryId, entityId);
1455
1514
  }
1515
+ async deleteEntities(repositoryId, ids) {
1516
+ if (ids.length === 0) return { deleted: [], notFound: [] };
1517
+ this.assertValidRepositoryId(repositoryId);
1518
+ const deleted = [];
1519
+ const CHUNK = 100;
1520
+ for (let i = 0; i < ids.length; i += CHUNK) {
1521
+ const chunk = ids.slice(i, i + CHUNK);
1522
+ const bindings = { rid: repositoryId };
1523
+ const idParams = [];
1524
+ for (let j = 0; j < chunk.length; j++) {
1525
+ const p = `id${j}`;
1526
+ bindings[p] = chunk[j];
1527
+ idParams.push(p);
1528
+ }
1529
+ const withinExpr = `P.within(${idParams.join(", ")})`;
1530
+ const existResult = await this.conn.submit(
1531
+ `g.V().has('repositoryId', rid).has('id', ${withinExpr}).has('entityType').values('id')`,
1532
+ bindings
1533
+ );
1534
+ const found = existResult.items;
1535
+ if (found.length > 0) {
1536
+ const dropBindings = { rid: repositoryId };
1537
+ const dropIdParams = [];
1538
+ for (let j = 0; j < found.length; j++) {
1539
+ const p = `did${j}`;
1540
+ dropBindings[p] = found[j];
1541
+ dropIdParams.push(p);
1542
+ }
1543
+ const dropWithinExpr = `P.within(${dropIdParams.join(", ")})`;
1544
+ await this.conn.submit(
1545
+ `g.V().has('repositoryId', rid).has('id', ${dropWithinExpr}).has('entityType').drop()`,
1546
+ dropBindings
1547
+ );
1548
+ deleted.push(...found);
1549
+ }
1550
+ }
1551
+ const deletedSet = new Set(deleted);
1552
+ return { deleted, notFound: ids.filter((id) => !deletedSet.has(id)) };
1553
+ }
1456
1554
  async deleteEntitiesByType(repositoryId, entityType) {
1555
+ this.assertValidRepositoryId(repositoryId);
1457
1556
  return deleteEntitiesByType(this.conn, repositoryId, entityType);
1458
1557
  }
1459
1558
  async findEntities(repositoryId, query) {
1559
+ this.assertValidRepositoryId(repositoryId);
1460
1560
  return findEntities(this.conn, repositoryId, query);
1461
1561
  }
1462
1562
  // ─── Relationships ─────────────────────────────────────────────────
1463
1563
  async createRelationship(repositoryId, relationship) {
1564
+ this.assertValidRepositoryId(repositoryId);
1464
1565
  return createRelationship(this.conn, repositoryId, relationship);
1465
1566
  }
1466
1567
  async getRelationship(repositoryId, relationshipId) {
1568
+ this.assertValidRepositoryId(repositoryId);
1467
1569
  return getRelationship(this.conn, repositoryId, relationshipId);
1468
1570
  }
1469
1571
  async getEntityRelationships(repositoryId, entityId, options) {
1572
+ this.assertValidRepositoryId(repositoryId);
1470
1573
  return getEntityRelationships(this.conn, repositoryId, entityId, options);
1471
1574
  }
1472
1575
  async deleteRelationship(repositoryId, relationshipId) {
1576
+ this.assertValidRepositoryId(repositoryId);
1473
1577
  return deleteRelationship(this.conn, repositoryId, relationshipId);
1474
1578
  }
1579
+ async deleteRelationships(repositoryId, ids) {
1580
+ if (ids.length === 0) return { deleted: [], notFound: [] };
1581
+ this.assertValidRepositoryId(repositoryId);
1582
+ const deleted = [];
1583
+ const CHUNK = 100;
1584
+ for (let i = 0; i < ids.length; i += CHUNK) {
1585
+ const chunk = ids.slice(i, i + CHUNK);
1586
+ const bindings = { rid: repositoryId };
1587
+ const idParams = [];
1588
+ for (let j = 0; j < chunk.length; j++) {
1589
+ const p = `id${j}`;
1590
+ bindings[p] = chunk[j];
1591
+ idParams.push(p);
1592
+ }
1593
+ const withinExpr = `P.within(${idParams.join(", ")})`;
1594
+ const existResult = await this.conn.submit(
1595
+ `g.E().has('repositoryId', rid).has('id', ${withinExpr}).values('id')`,
1596
+ bindings
1597
+ );
1598
+ const found = existResult.items;
1599
+ if (found.length > 0) {
1600
+ await cosmosRestBatchDelete(
1601
+ this.getRestEndpoint(),
1602
+ this.config.key,
1603
+ this.config.database,
1604
+ this.config.container,
1605
+ repositoryId,
1606
+ found,
1607
+ this.config.rejectUnauthorized ?? true
1608
+ );
1609
+ deleted.push(...found);
1610
+ }
1611
+ }
1612
+ const deletedSet = new Set(deleted);
1613
+ return { deleted, notFound: ids.filter((id) => !deletedSet.has(id)) };
1614
+ }
1475
1615
  async deleteRelationshipsByType(repositoryId, relationshipType) {
1616
+ this.assertValidRepositoryId(repositoryId);
1476
1617
  return deleteRelationshipsByType(this.conn, repositoryId, relationshipType);
1477
1618
  }
1478
1619
  // ─── Graph Traversal (StorageProvider) ─────────────────────────────
1479
1620
  async exploreNeighborhood(repositoryId, entityId, options) {
1621
+ this.assertValidRepositoryId(repositoryId);
1480
1622
  return exploreNeighborhood(this.conn, repositoryId, entityId, options);
1481
1623
  }
1482
1624
  async findPaths(repositoryId, sourceId, targetId, options) {
1625
+ this.assertValidRepositoryId(repositoryId);
1483
1626
  return findPaths(this.conn, repositoryId, sourceId, targetId, options);
1484
1627
  }
1485
1628
  // ─── Timeline ──────────────────────────────────────────────────────
1486
1629
  async getTimeline(repositoryId, entityId, options) {
1630
+ this.assertValidRepositoryId(repositoryId);
1487
1631
  return getTimeline(this.conn, repositoryId, entityId, options);
1488
1632
  }
1489
1633
  // ─── Bulk Operations ───────────────────────────────────────────────
1490
1634
  exportAll(repositoryId) {
1635
+ this.assertValidRepositoryId(repositoryId);
1491
1636
  return exportAll(this.conn, repositoryId);
1492
1637
  }
1493
1638
  async importBulk(repositoryId, data, options) {
1639
+ this.assertValidRepositoryId(repositoryId);
1494
1640
  return importBulk(this.conn, repositoryId, data, options);
1495
1641
  }
1496
1642
  // ─── GraphTraversalProvider ────────────────────────────────────────
@@ -1507,17 +1653,20 @@ var CosmosDbProvider = class {
1507
1653
  supportsRelationshipSummary: false
1508
1654
  };
1509
1655
  }
1510
- async traverse(repositoryId, spec, compiledQuery) {
1656
+ async traverse(repositoryId, spec) {
1657
+ this.assertValidRepositoryId(repositoryId);
1511
1658
  const startTime = Date.now();
1512
1659
  const vocabulary = await this.getVocabulary(repositoryId);
1513
- const compiled = compiledQuery ? { query: compiledQuery, params: {}, estimatedFanOut: 100 } : this.compiler.compile(spec, vocabulary);
1660
+ const compiled = this.compiler.compile(spec, vocabulary);
1514
1661
  const scopedQuery = compiled.query.replace(
1515
1662
  "g.V()",
1516
- `g.V().has('repositoryId', '${repositoryId}')`
1663
+ "g.V().has('repositoryId', pRid)"
1517
1664
  );
1665
+ const scopedParams = { ...compiled.params, pRid: repositoryId };
1518
1666
  try {
1519
- const result = await this.conn.submit(scopedQuery, compiled.params);
1667
+ const result = await this.conn.submit(scopedQuery, scopedParams);
1520
1668
  const executionTimeMs = Date.now() - startTime;
1669
+ const detailLevel = spec.detailLevel ?? "summary";
1521
1670
  const entities = [];
1522
1671
  const relationships = [];
1523
1672
  const paths = [];
@@ -1529,7 +1678,8 @@ var CosmosDbProvider = class {
1529
1678
  for (const obj of pathData.objects) {
1530
1679
  const props = obj;
1531
1680
  if (props["entityType"]) {
1532
- pathEntities.push(entityFromGremlin(props));
1681
+ const stored = entityFromGremlin(props);
1682
+ pathEntities.push((0, import_deep_memory5.projectEntity)(stored, detailLevel));
1533
1683
  }
1534
1684
  }
1535
1685
  if (pathEntities.length > 0) {
@@ -1543,8 +1693,8 @@ var CosmosDbProvider = class {
1543
1693
  }
1544
1694
  } else {
1545
1695
  for (const item of result.items) {
1546
- const entity = entityFromGremlin(item);
1547
- entities.push(entity);
1696
+ const stored = entityFromGremlin(item);
1697
+ entities.push((0, import_deep_memory5.projectEntity)(stored, detailLevel));
1548
1698
  }
1549
1699
  }
1550
1700
  const limit = spec.limit ?? 50;
@@ -1576,6 +1726,27 @@ var CosmosDbProvider = class {
1576
1726
  );
1577
1727
  }
1578
1728
  }
1729
+ /**
1730
+ * Execute a raw Gremlin query with caller-supplied bindings.
1731
+ *
1732
+ * ⚠️ ELEVATED PRIVILEGE — SYSTEM-LEVEL OPERATION ⚠️
1733
+ *
1734
+ * This method is an unscoped pass-through: it does not filter by repository,
1735
+ * does not inject the partition key, and performs no validation on the query
1736
+ * string. A single call can read or mutate any vertex or edge in the
1737
+ * container regardless of which repository (partition) it belongs to.
1738
+ *
1739
+ * DO NOT expose this method to AI agents, end users, or any untrusted caller.
1740
+ * It is intended for:
1741
+ * - administrative tooling (migrations, diagnostics, repairs)
1742
+ * - internal library operations that need cross-partition reach
1743
+ *
1744
+ * `repositoryId` is accepted for interface symmetry but is intentionally
1745
+ * ignored here — the caller is trusted to scope the query themselves.
1746
+ *
1747
+ * For agent-facing graph queries use {@link traverse}, which enforces the
1748
+ * repositoryId partition predicate.
1749
+ */
1579
1750
  async executeNativeQuery(_repositoryId, query, params) {
1580
1751
  const startTime = Date.now();
1581
1752
  try {
@@ -1654,6 +1825,32 @@ async function cosmosRestPut(restBase, key, urlPath, resourceLink, resourceType,
1654
1825
  const text = await response.text();
1655
1826
  throw new Error(`CosmosDB REST ${response.status}: ${text}`);
1656
1827
  }
1828
+ async function cosmosRestBatchDelete(restBase, key, database, container, partitionKeyValue, ids, rejectUnauthorized) {
1829
+ const resourceLink = `dbs/${database}/colls/${container}`;
1830
+ const date = (/* @__PURE__ */ new Date()).toUTCString();
1831
+ const token = cosmosAuthToken("post", "docs", resourceLink, date, key);
1832
+ if (!rejectUnauthorized) {
1833
+ process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
1834
+ }
1835
+ const ops = ids.map((id) => ({ operationType: "Delete", id }));
1836
+ const response = await fetch(`${restBase}/${resourceLink}/docs`, {
1837
+ method: "POST",
1838
+ headers: {
1839
+ "Authorization": token,
1840
+ "x-ms-version": "2020-07-15",
1841
+ "x-ms-date": date,
1842
+ "Content-Type": "application/json",
1843
+ "x-ms-documentdb-partitionkey": JSON.stringify([partitionKeyValue]),
1844
+ "x-ms-cosmos-is-batch-request": "true",
1845
+ "x-ms-cosmos-batch-atomic": "true"
1846
+ },
1847
+ body: JSON.stringify(ops)
1848
+ });
1849
+ if (response.status !== 200 && response.status !== 207) {
1850
+ const text = await response.text();
1851
+ throw new import_deep_memory5.ProviderError(`CosmosDB batch delete failed (${response.status}): ${text}`);
1852
+ }
1853
+ }
1657
1854
  // Annotate the CommonJS export names for ESM import in node:
1658
1855
  0 && (module.exports = {
1659
1856
  CosmosDbConnection,