@utaba/deep-memory-storage-cosmosdb 0.18.0 → 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.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/CosmosDbProvider.ts
2
2
  import {
3
3
  GremlinCompiler,
4
- ProviderError,
4
+ ProviderError as ProviderError2,
5
5
  InvalidInputError,
6
6
  isValidUuid,
7
7
  projectEntity,
@@ -290,6 +290,7 @@ function sleep2(ms) {
290
290
  }
291
291
 
292
292
  // src/mapping.ts
293
+ import { ProviderError } from "@utaba/deep-memory";
293
294
  var STORED_ENTITY_FIELDS = [
294
295
  "id",
295
296
  "entityType",
@@ -630,6 +631,89 @@ function repositoryConfigToLadderBindings(config) {
630
631
  bindings[SENTINEL_BINDING] = ABSENT_STRING_SENTINEL;
631
632
  return bindings;
632
633
  }
634
+ var RESERVED_ENTITY_PROPERTY_KEYS = /* @__PURE__ */ new Set([
635
+ "id",
636
+ "repositoryId",
637
+ ...ENTITY_REQUIRED_SLOTS,
638
+ ...ENTITY_OPTIONAL_SLOTS
639
+ ]);
640
+ var RESERVED_RELATIONSHIP_PROPERTY_KEYS = /* @__PURE__ */ new Set([
641
+ "id",
642
+ "repositoryId",
643
+ "label",
644
+ ...RELATIONSHIP_REQUIRED_SLOTS,
645
+ ...RELATIONSHIP_OPTIONAL_SLOTS
646
+ ]);
647
+ var USER_PROPERTY_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
648
+ function assertSafeUserPropertyKey(key, reserved, scope) {
649
+ if (!USER_PROPERTY_KEY_PATTERN.test(key)) {
650
+ throw new ProviderError(
651
+ `${scope} property key "${key}" is not a valid Gremlin identifier \u2014 must match ${USER_PROPERTY_KEY_PATTERN.source}. User-property keys are interpolated into Gremlin .property(...) / values(...) / .drop() slots (the key slot cannot be parameterised), so an unsafe value would widen the injection surface.`
652
+ );
653
+ }
654
+ if (reserved.has(key)) {
655
+ throw new ProviderError(
656
+ `${scope} property key "${key}" collides with a schema-managed field. Reserved names: ${Array.from(reserved).join(", ")}.`
657
+ );
658
+ }
659
+ return key;
660
+ }
661
+ function assertSafeEntityUserPropertyKey(key) {
662
+ return assertSafeUserPropertyKey(key, RESERVED_ENTITY_PROPERTY_KEYS, "Entity");
663
+ }
664
+ function assertSafeRelationshipUserPropertyKey(key) {
665
+ return assertSafeUserPropertyKey(key, RESERVED_RELATIONSHIP_PROPERTY_KEYS, "Relationship");
666
+ }
667
+ function isNativeStorableValue(value) {
668
+ if (typeof value === "string") return true;
669
+ if (typeof value === "boolean") return true;
670
+ if (typeof value === "number") return Number.isFinite(value);
671
+ if (Array.isArray(value)) {
672
+ if (value.length === 0) return true;
673
+ const first = value[0];
674
+ const t = typeof first;
675
+ if (t !== "string" && t !== "boolean" && (t !== "number" || !Number.isFinite(first))) {
676
+ return false;
677
+ }
678
+ for (const v of value) {
679
+ if (typeof v !== t) return false;
680
+ if (t === "number" && !Number.isFinite(v)) return false;
681
+ }
682
+ return true;
683
+ }
684
+ return false;
685
+ }
686
+ function entityUserPropertyParams(properties) {
687
+ const out = [];
688
+ for (const [key, value] of Object.entries(properties)) {
689
+ assertSafeEntityUserPropertyKey(key);
690
+ if (isNativeStorableValue(value)) {
691
+ out.push({ key, value });
692
+ }
693
+ }
694
+ return out;
695
+ }
696
+ function relationshipUserPropertyParams(properties) {
697
+ const out = [];
698
+ for (const [key, value] of Object.entries(properties)) {
699
+ assertSafeRelationshipUserPropertyKey(key);
700
+ if (isNativeStorableValue(value)) {
701
+ out.push({ key, value });
702
+ }
703
+ }
704
+ return out;
705
+ }
706
+ function existingEntityScalarUserKeys(blob) {
707
+ if (blob == null) return [];
708
+ const out = [];
709
+ for (const [key, value] of Object.entries(blob)) {
710
+ if (!USER_PROPERTY_KEY_PATTERN.test(key)) continue;
711
+ if (RESERVED_ENTITY_PROPERTY_KEYS.has(key)) continue;
712
+ if (!isNativeStorableValue(value)) continue;
713
+ out.push(key);
714
+ }
715
+ return out;
716
+ }
633
717
 
634
718
  // src/queries/repository.ts
635
719
  import { DuplicateRepositoryError, RepositoryNotFoundError } from "@utaba/deep-memory";
@@ -1011,7 +1095,8 @@ import {
1011
1095
  matchesPropertyFilters
1012
1096
  } from "@utaba/deep-memory";
1013
1097
  var DUPLICATE_SENTINEL = "__duplicate";
1014
- var ENTITY_CREATE_QUERY = `g.V().has('repositoryId', rid).hasId(vid).fold().coalesce(unfold().constant('${DUPLICATE_SENTINEL}'),addV(vertexLabel).property('id', vid).property('repositoryId', rid)${buildEntityPropertyLadder()})`;
1098
+ var ENTITY_CREATE_PREFIX = `g.V().has('repositoryId', rid).hasId(vid).fold().coalesce(unfold().constant('${DUPLICATE_SENTINEL}'),addV(vertexLabel).property('id', vid).property('repositoryId', rid)${buildEntityPropertyLadder()}`;
1099
+ var ENTITY_CREATE_QUERY = `${ENTITY_CREATE_PREFIX})`;
1015
1100
  async function createEntity(conn, repositoryId, entity) {
1016
1101
  const bindings = {
1017
1102
  rid: repositoryId,
@@ -1019,7 +1104,20 @@ async function createEntity(conn, repositoryId, entity) {
1019
1104
  vertexLabel: entity.entityType,
1020
1105
  ...entityToLadderBindings(entity)
1021
1106
  };
1022
- const result = await conn.submit(ENTITY_CREATE_QUERY, bindings);
1107
+ const userProps = entityUserPropertyParams(entity.properties ?? {});
1108
+ let query;
1109
+ if (userProps.length === 0) {
1110
+ query = ENTITY_CREATE_QUERY;
1111
+ } else {
1112
+ let suffix = "";
1113
+ for (let i = 0; i < userProps.length; i++) {
1114
+ const { key, value } = userProps[i];
1115
+ suffix += `.property('${key}', p_user_${i})`;
1116
+ bindings[`p_user_${i}`] = value;
1117
+ }
1118
+ query = `${ENTITY_CREATE_PREFIX}${suffix})`;
1119
+ }
1120
+ const result = await conn.submit(query, bindings);
1023
1121
  if (result.items[0] === DUPLICATE_SENTINEL) {
1024
1122
  throw new DuplicateEntityError(entity.id);
1025
1123
  }
@@ -1065,7 +1163,36 @@ async function getEntities(conn, repositoryId, entityIds, options) {
1065
1163
  }
1066
1164
  return map;
1067
1165
  }
1166
+ async function readExistingEntityPropertiesBlob(conn, repositoryId, entityId) {
1167
+ const result = await conn.submit(
1168
+ "g.V().has('repositoryId', rid).hasId(eid).has('entityType').values('properties').limit(1)",
1169
+ { rid: repositoryId, eid: entityId }
1170
+ );
1171
+ if (result.items.length === 0) return { found: false };
1172
+ const raw = result.items[0];
1173
+ const json = typeof raw === "string" ? raw : String(raw ?? "");
1174
+ if (!json) return { found: true, blob: {} };
1175
+ try {
1176
+ const parsed = JSON.parse(json);
1177
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1178
+ return { found: true, blob: parsed };
1179
+ }
1180
+ } catch {
1181
+ }
1182
+ return { found: true, blob: {} };
1183
+ }
1068
1184
  async function updateEntity(conn, repositoryId, entityId, updates) {
1185
+ const userProps = updates.properties !== void 0 ? entityUserPropertyParams(updates.properties) : null;
1186
+ let droppedUserKeys = [];
1187
+ if (updates.properties !== void 0) {
1188
+ const existing = await readExistingEntityPropertiesBlob(conn, repositoryId, entityId);
1189
+ if (!existing.found) {
1190
+ throw new EntityNotFoundError(entityId);
1191
+ }
1192
+ const existingKeys = existingEntityScalarUserKeys(existing.blob);
1193
+ const newKeySet = new Set(userProps.map((p) => p.key));
1194
+ droppedUserKeys = existingKeys.filter((k) => !newKeySet.has(k));
1195
+ }
1069
1196
  const bindings = { rid: repositoryId, eid: entityId };
1070
1197
  const propParts = [];
1071
1198
  let idx = 0;
@@ -1082,7 +1209,18 @@ async function updateEntity(conn, repositoryId, entityId, updates) {
1082
1209
  if (updates.slug !== void 0) addProp("slug", updates.slug);
1083
1210
  if (updates.summary === null) dropProp("summary");
1084
1211
  else if (updates.summary !== void 0) addProp("summary", updates.summary);
1085
- if (updates.properties !== void 0) addProp("properties", JSON.stringify(updates.properties));
1212
+ if (updates.properties !== void 0 && userProps !== null) {
1213
+ addProp("properties", JSON.stringify(updates.properties));
1214
+ for (const dropKey of droppedUserKeys) {
1215
+ dropProp(dropKey);
1216
+ }
1217
+ for (let i = 0; i < userProps.length; i++) {
1218
+ const { key, value } = userProps[i];
1219
+ const paramName = `p_user_${i}`;
1220
+ bindings[paramName] = value;
1221
+ propParts.push(`.property('${key}', ${paramName})`);
1222
+ }
1223
+ }
1086
1224
  if (updates.data === null) dropProp("data");
1087
1225
  else if (updates.data !== void 0) addProp("data", updates.data);
1088
1226
  if (updates.dataFormat === null) dropProp("dataFormat");
@@ -1137,16 +1275,33 @@ function buildWhereClause(query, repositoryId) {
1137
1275
  `(CONTAINS(${sqlPath("entityLabel")}, @term, true) OR CONTAINS(${sqlPath("slug")}, @term, true) OR CONTAINS(${sqlPath("summary")}, @term, true))`
1138
1276
  );
1139
1277
  }
1140
- if (query.properties != null) {
1141
- let i = 0;
1142
- for (const [key, value] of Object.entries(query.properties)) {
1143
- const fragment = JSON.stringify({ [key]: value }).slice(1, -1);
1144
- const name = `@kv${i++}`;
1145
- params.push({ name, value: fragment });
1146
- predicates.push(`CONTAINS(${sqlPath("properties")}, ${name}, false)`);
1278
+ let propertyFilterMode = "none";
1279
+ if (query.properties != null && Object.keys(query.properties).length > 0) {
1280
+ const entries = Object.entries(query.properties);
1281
+ const allStorable = entries.every(([, value]) => isNativeStorableValue(value));
1282
+ if (allStorable) {
1283
+ for (const [key] of entries) {
1284
+ assertSafeEntityUserPropertyKey(key);
1285
+ }
1286
+ let i = 0;
1287
+ for (const [key, value] of entries) {
1288
+ const name = `@val${i++}`;
1289
+ params.push({ name, value });
1290
+ predicates.push(`${sqlPath(key)} = ${name}`);
1291
+ }
1292
+ propertyFilterMode = "exact";
1293
+ } else {
1294
+ let i = 0;
1295
+ for (const [key, value] of entries) {
1296
+ const fragment = JSON.stringify({ [key]: value }).slice(1, -1);
1297
+ const name = `@kv${i++}`;
1298
+ params.push({ name, value: fragment });
1299
+ predicates.push(`CONTAINS(${sqlPath("properties")}, ${name}, false)`);
1300
+ }
1301
+ propertyFilterMode = "approximate";
1147
1302
  }
1148
1303
  }
1149
- return { sqlWhere: `WHERE ${predicates.join(" AND ")}`, params };
1304
+ return { sqlWhere: `WHERE ${predicates.join(" AND ")}`, params, propertyFilterMode };
1150
1305
  }
1151
1306
  function buildSelectClause(loadEmbeddings) {
1152
1307
  const fields = ["c.id", ...STORED_ENTITY_FIELDS.filter((f) => f !== "id").map((f) => `c.${f}`)];
@@ -1154,7 +1309,7 @@ function buildSelectClause(loadEmbeddings) {
1154
1309
  return `SELECT ${fields.join(", ")}`;
1155
1310
  }
1156
1311
  async function findEntities(docClient, repositoryId, query, options) {
1157
- const { sqlWhere, params } = buildWhereClause(query, repositoryId);
1312
+ const { sqlWhere, params, propertyFilterMode } = buildWhereClause(query, repositoryId);
1158
1313
  const dataParams = [
1159
1314
  ...params,
1160
1315
  { name: "@off", value: query.offset },
@@ -1163,7 +1318,7 @@ async function findEntities(docClient, repositoryId, query, options) {
1163
1318
  const selectClause = buildSelectClause(options?.loadEmbeddings === true);
1164
1319
  const dataSql = `${selectClause} FROM c ${sqlWhere} ORDER BY c.id OFFSET @off LIMIT @lim`;
1165
1320
  const countSql = `SELECT VALUE COUNT(1) FROM c ${sqlWhere}`;
1166
- const skipCount = query.properties != null && Object.keys(query.properties).length > 0;
1321
+ const skipCount = propertyFilterMode === "approximate";
1167
1322
  const [dataResult, countResult] = await Promise.all([
1168
1323
  docClient.query(dataSql, dataParams, {
1169
1324
  partitionKey: repositoryId
@@ -1191,7 +1346,8 @@ async function findEntities(docClient, repositoryId, query, options) {
1191
1346
  // src/queries/relationship.ts
1192
1347
  import { DuplicateRelationshipError, matchesPropertyFilters as matchesPropertyFilters2, buildEdgeProjectChain } from "@utaba/deep-memory";
1193
1348
  var DUPLICATE_SENTINEL2 = "__duplicate";
1194
- var RELATIONSHIP_CREATE_QUERY = `g.E().has('repositoryId', rid).hasId(relId).fold().coalesce(unfold().constant('${DUPLICATE_SENTINEL2}'),g.V().has('repositoryId', rid).hasId(srcId).has('entityType').addE(edgeLabel).to(g.V().has('repositoryId', rid).hasId(tgtId).has('entityType')).property('id', relId).property('repositoryId', rid)${buildRelationshipPropertyLadder()})`;
1349
+ var RELATIONSHIP_CREATE_PREFIX = `g.E().has('repositoryId', rid).hasId(relId).fold().coalesce(unfold().constant('${DUPLICATE_SENTINEL2}'),g.V().has('repositoryId', rid).hasId(srcId).has('entityType').addE(edgeLabel).to(g.V().has('repositoryId', rid).hasId(tgtId).has('entityType')).property('id', relId).property('repositoryId', rid)${buildRelationshipPropertyLadder()}`;
1350
+ var RELATIONSHIP_CREATE_QUERY = `${RELATIONSHIP_CREATE_PREFIX})`;
1195
1351
  async function createRelationship(conn, repositoryId, relationship) {
1196
1352
  const bindings = {
1197
1353
  rid: repositoryId,
@@ -1201,7 +1357,20 @@ async function createRelationship(conn, repositoryId, relationship) {
1201
1357
  edgeLabel: relationship.relationshipType,
1202
1358
  ...relationshipToLadderBindings(relationship)
1203
1359
  };
1204
- const result = await conn.submit(RELATIONSHIP_CREATE_QUERY, bindings);
1360
+ const userProps = relationshipUserPropertyParams(relationship.properties ?? {});
1361
+ let query;
1362
+ if (userProps.length === 0) {
1363
+ query = RELATIONSHIP_CREATE_QUERY;
1364
+ } else {
1365
+ let suffix = "";
1366
+ for (let i = 0; i < userProps.length; i++) {
1367
+ const { key, value } = userProps[i];
1368
+ suffix += `.property('${key}', p_user_${i})`;
1369
+ bindings[`p_user_${i}`] = value;
1370
+ }
1371
+ query = `${RELATIONSHIP_CREATE_PREFIX}${suffix})`;
1372
+ }
1373
+ const result = await conn.submit(query, bindings);
1205
1374
  if (result.items[0] === DUPLICATE_SENTINEL2) {
1206
1375
  throw new DuplicateRelationshipError(relationship.id);
1207
1376
  }
@@ -1754,8 +1923,12 @@ var ENTITY_LADDER_CHAIN = buildEntityPropertyLadder();
1754
1923
  var RELATIONSHIP_LADDER_CHAIN = buildRelationshipPropertyLadder();
1755
1924
  var INSERT_ENTITY_QUERY = `g.addV(vertexLabel).property('id', vid).property('repositoryId', rid)${ENTITY_LADDER_CHAIN}`;
1756
1925
  var INSERT_RELATIONSHIP_QUERY = `g.V().has('repositoryId', rid).hasId(srcId).has('entityType').addE(edgeLabel).to(g.V().has('repositoryId', rid).hasId(tgtId).has('entityType')).property('id', relId).property('repositoryId', rid)${RELATIONSHIP_LADDER_CHAIN}`;
1757
- var UPSERT_ENTITY_QUERY = `g.V().has('repositoryId', rid).hasId(vid).has('entityType').fold().coalesce(unfold()${ENTITY_LADDER_CHAIN}, addV(vertexLabel).property('id', vid).property('repositoryId', rid)${ENTITY_LADDER_CHAIN})`;
1758
- var UPSERT_RELATIONSHIP_QUERY = `g.E().has('repositoryId', rid).hasId(relId).fold().coalesce(unfold()${RELATIONSHIP_LADDER_CHAIN}, g.V().has('repositoryId', rid).hasId(srcId).has('entityType').addE(edgeLabel).to(g.V().has('repositoryId', rid).hasId(tgtId).has('entityType')).property('id', relId).property('repositoryId', rid)${RELATIONSHIP_LADDER_CHAIN})`;
1926
+ var UPSERT_ENTITY_OPEN = `g.V().has('repositoryId', rid).hasId(vid).has('entityType').fold().coalesce(unfold()${ENTITY_LADDER_CHAIN}`;
1927
+ var UPSERT_ENTITY_CREATE_BRANCH = `, addV(vertexLabel).property('id', vid).property('repositoryId', rid)${ENTITY_LADDER_CHAIN}`;
1928
+ var UPSERT_ENTITY_QUERY = `${UPSERT_ENTITY_OPEN}${UPSERT_ENTITY_CREATE_BRANCH})`;
1929
+ var UPSERT_RELATIONSHIP_OPEN = `g.E().has('repositoryId', rid).hasId(relId).fold().coalesce(unfold()${RELATIONSHIP_LADDER_CHAIN}`;
1930
+ var UPSERT_RELATIONSHIP_CREATE_BRANCH = `, g.V().has('repositoryId', rid).hasId(srcId).has('entityType').addE(edgeLabel).to(g.V().has('repositoryId', rid).hasId(tgtId).has('entityType')).property('id', relId).property('repositoryId', rid)${RELATIONSHIP_LADDER_CHAIN}`;
1931
+ var UPSERT_RELATIONSHIP_QUERY = `${UPSERT_RELATIONSHIP_OPEN}${UPSERT_RELATIONSHIP_CREATE_BRANCH})`;
1759
1932
  async function insertEntity(conn, repositoryId, entity) {
1760
1933
  const bindings = {
1761
1934
  rid: repositoryId,
@@ -1763,7 +1936,20 @@ async function insertEntity(conn, repositoryId, entity) {
1763
1936
  vertexLabel: entity.entityType,
1764
1937
  ...entityToLadderBindings(entity)
1765
1938
  };
1766
- await conn.submit(INSERT_ENTITY_QUERY, bindings);
1939
+ const userProps = entityUserPropertyParams(entity.properties ?? {});
1940
+ let query;
1941
+ if (userProps.length === 0) {
1942
+ query = INSERT_ENTITY_QUERY;
1943
+ } else {
1944
+ let suffix = "";
1945
+ for (let i = 0; i < userProps.length; i++) {
1946
+ const { key, value } = userProps[i];
1947
+ suffix += `.property('${key}', p_user_${i})`;
1948
+ bindings[`p_user_${i}`] = value;
1949
+ }
1950
+ query = `${INSERT_ENTITY_QUERY}${suffix}`;
1951
+ }
1952
+ await conn.submit(query, bindings);
1767
1953
  }
1768
1954
  async function insertRelationship(conn, repositoryId, rel) {
1769
1955
  const bindings = {
@@ -1774,7 +1960,20 @@ async function insertRelationship(conn, repositoryId, rel) {
1774
1960
  edgeLabel: rel.relationshipType,
1775
1961
  ...relationshipToLadderBindings(rel)
1776
1962
  };
1777
- await conn.submit(INSERT_RELATIONSHIP_QUERY, bindings);
1963
+ const userProps = relationshipUserPropertyParams(rel.properties ?? {});
1964
+ let query;
1965
+ if (userProps.length === 0) {
1966
+ query = INSERT_RELATIONSHIP_QUERY;
1967
+ } else {
1968
+ let suffix = "";
1969
+ for (let i = 0; i < userProps.length; i++) {
1970
+ const { key, value } = userProps[i];
1971
+ suffix += `.property('${key}', p_user_${i})`;
1972
+ bindings[`p_user_${i}`] = value;
1973
+ }
1974
+ query = `${INSERT_RELATIONSHIP_QUERY}${suffix}`;
1975
+ }
1976
+ await conn.submit(query, bindings);
1778
1977
  }
1779
1978
  async function upsertEntity(conn, repositoryId, entity) {
1780
1979
  const bindings = {
@@ -1783,7 +1982,20 @@ async function upsertEntity(conn, repositoryId, entity) {
1783
1982
  vertexLabel: entity.entityType,
1784
1983
  ...entityToLadderBindings(entity)
1785
1984
  };
1786
- await conn.submit(UPSERT_ENTITY_QUERY, bindings);
1985
+ const userProps = entityUserPropertyParams(entity.properties ?? {});
1986
+ let query;
1987
+ if (userProps.length === 0) {
1988
+ query = UPSERT_ENTITY_QUERY;
1989
+ } else {
1990
+ let suffix = "";
1991
+ for (let i = 0; i < userProps.length; i++) {
1992
+ const { key, value } = userProps[i];
1993
+ suffix += `.property('${key}', p_user_${i})`;
1994
+ bindings[`p_user_${i}`] = value;
1995
+ }
1996
+ query = `${UPSERT_ENTITY_OPEN}${suffix}${UPSERT_ENTITY_CREATE_BRANCH}${suffix})`;
1997
+ }
1998
+ await conn.submit(query, bindings);
1787
1999
  }
1788
2000
  async function upsertRelationship(conn, repositoryId, rel) {
1789
2001
  const bindings = {
@@ -1794,7 +2006,20 @@ async function upsertRelationship(conn, repositoryId, rel) {
1794
2006
  edgeLabel: rel.relationshipType,
1795
2007
  ...relationshipToLadderBindings(rel)
1796
2008
  };
1797
- await conn.submit(UPSERT_RELATIONSHIP_QUERY, bindings);
2009
+ const userProps = relationshipUserPropertyParams(rel.properties ?? {});
2010
+ let query;
2011
+ if (userProps.length === 0) {
2012
+ query = UPSERT_RELATIONSHIP_QUERY;
2013
+ } else {
2014
+ let suffix = "";
2015
+ for (let i = 0; i < userProps.length; i++) {
2016
+ const { key, value } = userProps[i];
2017
+ suffix += `.property('${key}', p_user_${i})`;
2018
+ bindings[`p_user_${i}`] = value;
2019
+ }
2020
+ query = `${UPSERT_RELATIONSHIP_OPEN}${suffix}${UPSERT_RELATIONSHIP_CREATE_BRANCH}${suffix})`;
2021
+ }
2022
+ await conn.submit(query, bindings);
1798
2023
  }
1799
2024
 
1800
2025
  // src/CosmosDbProvider.ts
@@ -1965,7 +2190,7 @@ var CosmosDbProvider = class {
1965
2190
  schemaVersion: SCHEMA_VERSION
1966
2191
  };
1967
2192
  } catch (err) {
1968
- throw new ProviderError(
2193
+ throw new ProviderError2(
1969
2194
  `Failed to ensure CosmosDB schema: ${err instanceof Error ? err.message : String(err)}`,
1970
2195
  "Verify the CosmosDB REST endpoint is accessible and the Gremlin endpoint is reachable."
1971
2196
  );
@@ -2382,7 +2607,7 @@ Verify the ARM/Bicep template that provisioned the container.`
2382
2607
  }
2383
2608
  const layer = {};
2384
2609
  const nextFrontier = /* @__PURE__ */ new Set();
2385
- const layerEdgeSeen = /* @__PURE__ */ new Set();
2610
+ const layerBucketSeen = /* @__PURE__ */ new Map();
2386
2611
  for (const fv of frontier) {
2387
2612
  const incident = edgesByVertex.get(fv) ?? [];
2388
2613
  for (const rel of incident) {
@@ -2415,10 +2640,14 @@ Verify the ARM/Bicep template that provisioned the container.`
2415
2640
  if (options.entityTypes && options.entityTypes.length > 0 && !options.entityTypes.includes(connectedEntity.entityType)) {
2416
2641
  continue;
2417
2642
  }
2418
- const edgeKey = `${rel.id}|${fv}->${connectedId}`;
2419
- if (layerEdgeSeen.has(edgeKey)) continue;
2420
- layerEdgeSeen.add(edgeKey);
2421
2643
  const relType = rel.relationshipType;
2644
+ let bucketSeen = layerBucketSeen.get(relType);
2645
+ if (!bucketSeen) {
2646
+ bucketSeen = /* @__PURE__ */ new Set();
2647
+ layerBucketSeen.set(relType, bucketSeen);
2648
+ }
2649
+ if (bucketSeen.has(connectedId)) continue;
2650
+ bucketSeen.add(connectedId);
2422
2651
  if (!layer[relType]) {
2423
2652
  layer[relType] = { total: 0, entities: [], relationships: [] };
2424
2653
  }
@@ -2653,8 +2882,11 @@ Verify the ARM/Bicep template that provisioned the container.`
2653
2882
  let entities = [];
2654
2883
  let relationships;
2655
2884
  let paths;
2885
+ const aggregations = raw.aggregations;
2886
+ const projectionEmitted = aggregations !== void 0;
2656
2887
  const rangeRowCount = spec.returnMode === "all" ? raw.allEntities.length + raw.allRelationships.length : 0;
2657
- if (spec.returnMode === "terminal") {
2888
+ if (projectionEmitted) {
2889
+ } else if (spec.returnMode === "terminal") {
2658
2890
  entities = raw.terminalEntities.map(projectStoredEntity);
2659
2891
  relationships = void 0;
2660
2892
  } else if (spec.returnMode === "all") {
@@ -2682,7 +2914,7 @@ Verify the ARM/Bicep template that provisioned the container.`
2682
2914
  entities: row.entityIds.map((id) => {
2683
2915
  const stored = raw.entityMap.get(id);
2684
2916
  if (!stored) {
2685
- throw new ProviderError(
2917
+ throw new ProviderError2(
2686
2918
  "Unpacking Gremlin path: entity referenced by path is missing from the result",
2687
2919
  "This indicates a Gremlin response shape mismatch \u2014 inspect compiledQuery."
2688
2920
  );
@@ -2692,7 +2924,7 @@ Verify the ARM/Bicep template that provisioned the container.`
2692
2924
  relationships: row.relationshipIds.map((id, i) => {
2693
2925
  const stored = raw.relationshipMap.get(id);
2694
2926
  if (!stored) {
2695
- throw new ProviderError(
2927
+ throw new ProviderError2(
2696
2928
  "Unpacking Gremlin path: relationship referenced by path is missing from the result",
2697
2929
  "This indicates a Gremlin response shape mismatch \u2014 inspect compiledQuery."
2698
2930
  );
@@ -2706,7 +2938,9 @@ Verify the ARM/Bicep template that provisioned the container.`
2706
2938
  }
2707
2939
  const limit = spec.limit ?? 50;
2708
2940
  let total;
2709
- if (spec.returnMode === "path") {
2941
+ if (projectionEmitted) {
2942
+ total = aggregations.length;
2943
+ } else if (spec.returnMode === "path") {
2710
2944
  total = paths?.length ?? 0;
2711
2945
  } else if (spec.returnMode === "all") {
2712
2946
  total = entities.length + (relationships?.length ?? 0);
@@ -2731,6 +2965,7 @@ Verify the ARM/Bicep template that provisioned the container.`
2731
2965
  entities,
2732
2966
  relationships,
2733
2967
  paths,
2968
+ ...aggregations !== void 0 ? { aggregations } : {},
2734
2969
  total,
2735
2970
  returned: total,
2736
2971
  hasMore: truncated,
@@ -2767,7 +3002,7 @@ Verify the ARM/Bicep template that provisioned the container.`
2767
3002
  try {
2768
3003
  result = await this.conn.submit(scopedQuery, scopedParams);
2769
3004
  } catch (err) {
2770
- throw new ProviderError(
3005
+ throw new ProviderError2(
2771
3006
  `Gremlin traversal failed: ${err instanceof Error ? err.message : String(err)}`,
2772
3007
  "Check the traversal spec and ensure the CosmosDB connection is healthy."
2773
3008
  );
@@ -2785,7 +3020,14 @@ Verify the ARM/Bicep template that provisioned the container.`
2785
3020
  requestCharge: result.requestCharge,
2786
3021
  compiledQuery: scopedQuery
2787
3022
  };
2788
- if (spec.returnMode === "terminal") {
3023
+ const emitsProjection = spec.projection !== void 0 && spec.returnMode !== "all" && spec.returnMode !== "path";
3024
+ if (emitsProjection) {
3025
+ raw.aggregations = parseProjectionRows(
3026
+ result.items,
3027
+ spec.projection.properties,
3028
+ spec.projection.mode ?? "values"
3029
+ );
3030
+ } else if (spec.returnMode === "terminal") {
2789
3031
  for (const item of result.items) {
2790
3032
  const stored = entityFromGremlin(item);
2791
3033
  raw.terminalEntities.push(stored);
@@ -2873,6 +3115,81 @@ function unwrapGremlinValue(val) {
2873
3115
  if (Array.isArray(val) && val.length > 0) return val[0];
2874
3116
  return val;
2875
3117
  }
3118
+ function parseProjectionRows(items, properties, mode) {
3119
+ const singleProperty = properties.length === 1;
3120
+ const aggregations = [];
3121
+ if (mode === "count") {
3122
+ for (const item of items) {
3123
+ const entry = readEntry(item);
3124
+ if (entry === void 0) continue;
3125
+ const values = singleProperty ? { [properties[0]]: normaliseProjectionScalar(entry.key) } : readProjectedMap(entry.key, properties);
3126
+ aggregations.push({ values, count: coerceCount(entry.value) });
3127
+ }
3128
+ return aggregations;
3129
+ }
3130
+ for (const item of items) {
3131
+ const values = singleProperty ? { [properties[0]]: normaliseProjectionScalar(item) } : readProjectedMap(item, properties);
3132
+ aggregations.push({ values });
3133
+ }
3134
+ return aggregations;
3135
+ }
3136
+ function readEntry(item) {
3137
+ if (item instanceof Map) {
3138
+ if (item.has("key") && item.has("value")) {
3139
+ return { key: item.get("key"), value: item.get("value") };
3140
+ }
3141
+ const first = item.entries().next();
3142
+ if (!first.done) {
3143
+ const [key, value] = first.value;
3144
+ return { key, value };
3145
+ }
3146
+ return void 0;
3147
+ }
3148
+ if (typeof item !== "object" || item === null) return void 0;
3149
+ const obj = item;
3150
+ if ("key" in obj && "value" in obj) {
3151
+ return { key: obj["key"], value: obj["value"] };
3152
+ }
3153
+ const keys = Object.keys(obj);
3154
+ if (keys.length === 1) {
3155
+ const k = keys[0];
3156
+ return { key: k, value: obj[k] };
3157
+ }
3158
+ return void 0;
3159
+ }
3160
+ function readProjectedMap(row, properties) {
3161
+ const out = {};
3162
+ if (row instanceof Map) {
3163
+ for (const prop of properties) {
3164
+ out[prop] = normaliseProjectionScalar(row.get(prop));
3165
+ }
3166
+ return out;
3167
+ }
3168
+ if (typeof row === "object" && row !== null) {
3169
+ const obj = row;
3170
+ for (const prop of properties) {
3171
+ out[prop] = normaliseProjectionScalar(obj[prop]);
3172
+ }
3173
+ return out;
3174
+ }
3175
+ for (const prop of properties) {
3176
+ out[prop] = void 0;
3177
+ }
3178
+ return out;
3179
+ }
3180
+ function normaliseProjectionScalar(value) {
3181
+ if (typeof value === "bigint") return Number(value);
3182
+ return value;
3183
+ }
3184
+ function coerceCount(value) {
3185
+ if (typeof value === "number") return value;
3186
+ if (typeof value === "bigint") return Number(value);
3187
+ if (typeof value === "string") {
3188
+ const n = Number(value);
3189
+ return Number.isFinite(n) ? n : 0;
3190
+ }
3191
+ return 0;
3192
+ }
2876
3193
  function buildExploreSteps(depth, options) {
2877
3194
  const base = { direction: "both" };
2878
3195
  if (options.relationshipTypes && options.relationshipTypes.length > 0) {