@utaba/deep-memory-storage-cosmosdb 0.17.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
  }
@@ -1227,10 +1396,10 @@ async function getEntityRelationships(conn, repositoryId, entityId, options) {
1227
1396
  };
1228
1397
  let edgeTraversal;
1229
1398
  switch (direction) {
1230
- case "outbound":
1399
+ case "out":
1231
1400
  edgeTraversal = "g.V().has('repositoryId', rid).hasId(eid).has('entityType').outE()";
1232
1401
  break;
1233
- case "inbound":
1402
+ case "in":
1234
1403
  edgeTraversal = "g.V().has('repositoryId', rid).hasId(eid).has('entityType').inE()";
1235
1404
  break;
1236
1405
  case "both":
@@ -1249,9 +1418,9 @@ async function getEntityRelationships(conn, repositoryId, entityId, options) {
1249
1418
  typeFilter = `.hasLabel(${typeParams.join(", ")})`;
1250
1419
  }
1251
1420
  let unionQuery = null;
1252
- if (direction === "outbound") {
1421
+ if (direction === "out") {
1253
1422
  unionQuery = `g.V().has('repositoryId', rid).hasId(eid).has('entityType').union(outE()${typeFilter}, inE()${typeFilter}.has('bidirectional', true))`;
1254
- } else if (direction === "inbound") {
1423
+ } else if (direction === "in") {
1255
1424
  unionQuery = `g.V().has('repositoryId', rid).hasId(eid).has('entityType').union(inE()${typeFilter}, outE()${typeFilter}.has('bidirectional', true))`;
1256
1425
  }
1257
1426
  const baseQuery = unionQuery ?? `${edgeTraversal}${typeFilter}`;
@@ -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
  );
@@ -2340,11 +2565,11 @@ Verify the ARM/Bicep template that provisioned the container.`
2340
2565
  *
2341
2566
  * The server-side step direction is fixed to `'both'` (catches every edge
2342
2567
  * in either direction). The directional + bidirectional filter is applied
2343
- * client-side per hop — i.e. `direction: 'outbound'` includes inbound edges
2568
+ * client-side per hop — i.e. `direction: 'out'` includes inbound edges
2344
2569
  * where `bidirectional` is true. The CosmosDB Gremlin compiler does not
2345
2570
  * natively express the `union(outE, inE.has(bidirectional))` shape that
2346
2571
  * would push this filter server-side, so it stays client-side; the
2347
- * observable contract (which edges count toward "outbound" given
2572
+ * observable contract (which edges count toward `'out'` given
2348
2573
  * bidirectionality) is preserved.
2349
2574
  *
2350
2575
  * Round-trips per call: `options.depth` (one per layer). The previous BFS
@@ -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) {
@@ -2390,17 +2615,17 @@ Verify the ARM/Bicep template that provisioned the container.`
2390
2615
  const isTarget = rel.targetEntityId === fv;
2391
2616
  let matchesDirection = false;
2392
2617
  let connectedId;
2393
- if (isSource && (options.direction === "outbound" || options.direction === "both")) {
2618
+ if (isSource && (options.direction === "out" || options.direction === "both")) {
2394
2619
  matchesDirection = true;
2395
2620
  connectedId = rel.targetEntityId;
2396
- } else if (isTarget && (options.direction === "inbound" || options.direction === "both")) {
2621
+ } else if (isTarget && (options.direction === "in" || options.direction === "both")) {
2397
2622
  matchesDirection = true;
2398
2623
  connectedId = rel.sourceEntityId;
2399
2624
  } else if (rel.bidirectional) {
2400
- if (isSource && options.direction === "inbound") {
2625
+ if (isSource && options.direction === "in") {
2401
2626
  matchesDirection = true;
2402
2627
  connectedId = rel.targetEntityId;
2403
- } else if (isTarget && options.direction === "outbound") {
2628
+ } else if (isTarget && options.direction === "out") {
2404
2629
  matchesDirection = true;
2405
2630
  connectedId = rel.sourceEntityId;
2406
2631
  }
@@ -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
  }
@@ -2642,7 +2871,7 @@ Verify the ARM/Bicep template that provisioned the container.`
2642
2871
  }
2643
2872
  return projected;
2644
2873
  };
2645
- const projectStoredRelationship = (rel, direction = "outbound") => ({
2874
+ const projectStoredRelationship = (rel, direction = "out") => ({
2646
2875
  id: rel.id,
2647
2876
  type: rel.relationshipType,
2648
2877
  sourceEntityId: rel.sourceEntityId,
@@ -2653,10 +2882,30 @@ Verify the ARM/Bicep template that provisioned the container.`
2653
2882
  let entities = [];
2654
2883
  let relationships;
2655
2884
  let paths;
2656
- if (spec.returnMode === "terminal") {
2885
+ const aggregations = raw.aggregations;
2886
+ const projectionEmitted = aggregations !== void 0;
2887
+ const rangeRowCount = spec.returnMode === "all" ? raw.allEntities.length + raw.allRelationships.length : 0;
2888
+ if (projectionEmitted) {
2889
+ } else if (spec.returnMode === "terminal") {
2657
2890
  entities = raw.terminalEntities.map(projectStoredEntity);
2658
2891
  relationships = void 0;
2659
2892
  } else if (spec.returnMode === "all") {
2893
+ const missingIds = /* @__PURE__ */ new Set();
2894
+ for (const rel of raw.allRelationships) {
2895
+ if (!raw.entityMap.has(rel.sourceEntityId)) {
2896
+ missingIds.add(rel.sourceEntityId);
2897
+ }
2898
+ if (!raw.entityMap.has(rel.targetEntityId)) {
2899
+ missingIds.add(rel.targetEntityId);
2900
+ }
2901
+ }
2902
+ if (missingIds.size > 0) {
2903
+ const fetched = await this.getEntities(repositoryId, [...missingIds]);
2904
+ for (const stored of fetched.values()) {
2905
+ raw.allEntities.push(stored);
2906
+ raw.entityMap.set(stored.id, stored);
2907
+ }
2908
+ }
2660
2909
  entities = raw.allEntities.map(projectStoredEntity);
2661
2910
  relationships = raw.allRelationships.map((r) => projectStoredRelationship(r));
2662
2911
  } else {
@@ -2665,7 +2914,7 @@ Verify the ARM/Bicep template that provisioned the container.`
2665
2914
  entities: row.entityIds.map((id) => {
2666
2915
  const stored = raw.entityMap.get(id);
2667
2916
  if (!stored) {
2668
- throw new ProviderError(
2917
+ throw new ProviderError2(
2669
2918
  "Unpacking Gremlin path: entity referenced by path is missing from the result",
2670
2919
  "This indicates a Gremlin response shape mismatch \u2014 inspect compiledQuery."
2671
2920
  );
@@ -2675,7 +2924,7 @@ Verify the ARM/Bicep template that provisioned the container.`
2675
2924
  relationships: row.relationshipIds.map((id, i) => {
2676
2925
  const stored = raw.relationshipMap.get(id);
2677
2926
  if (!stored) {
2678
- throw new ProviderError(
2927
+ throw new ProviderError2(
2679
2928
  "Unpacking Gremlin path: relationship referenced by path is missing from the result",
2680
2929
  "This indicates a Gremlin response shape mismatch \u2014 inspect compiledQuery."
2681
2930
  );
@@ -2684,18 +2933,22 @@ Verify the ARM/Bicep template that provisioned the container.`
2684
2933
  })
2685
2934
  }));
2686
2935
  relationships = Array.from(raw.relationshipMap.values()).map(
2687
- (rel) => projectStoredRelationship(rel, raw.pathRelFirstDirection.get(rel.id) ?? "outbound")
2936
+ (rel) => projectStoredRelationship(rel, raw.pathRelFirstDirection.get(rel.id) ?? "out")
2688
2937
  );
2689
2938
  }
2690
2939
  const limit = spec.limit ?? 50;
2691
2940
  let total;
2692
- if (spec.returnMode === "path") {
2941
+ if (projectionEmitted) {
2942
+ total = aggregations.length;
2943
+ } else if (spec.returnMode === "path") {
2693
2944
  total = paths?.length ?? 0;
2694
2945
  } else if (spec.returnMode === "all") {
2695
2946
  total = entities.length + (relationships?.length ?? 0);
2696
2947
  } else {
2697
2948
  total = entities.length;
2698
2949
  }
2950
+ const paginationSignal = spec.returnMode === "all" ? rangeRowCount : total;
2951
+ const truncated = paginationSignal >= limit;
2699
2952
  const queryMetadata = {
2700
2953
  executionTimeMs: raw.executionTimeMs,
2701
2954
  resourceCost: raw.requestCharge != null ? { units: "RU", value: raw.requestCharge } : void 0,
@@ -2705,16 +2958,17 @@ Verify the ARM/Bicep template that provisioned the container.`
2705
2958
  maxResults: limit,
2706
2959
  maxDepth: spec.steps?.length
2707
2960
  },
2708
- truncated: total >= limit,
2709
- truncationReason: total >= limit ? "result_limit" : void 0
2961
+ truncated,
2962
+ truncationReason: truncated ? "result_limit" : void 0
2710
2963
  };
2711
2964
  return {
2712
2965
  entities,
2713
2966
  relationships,
2714
2967
  paths,
2968
+ ...aggregations !== void 0 ? { aggregations } : {},
2715
2969
  total,
2716
2970
  returned: total,
2717
- hasMore: total >= limit,
2971
+ hasMore: truncated,
2718
2972
  queryMetadata
2719
2973
  };
2720
2974
  }
@@ -2748,7 +3002,7 @@ Verify the ARM/Bicep template that provisioned the container.`
2748
3002
  try {
2749
3003
  result = await this.conn.submit(scopedQuery, scopedParams);
2750
3004
  } catch (err) {
2751
- throw new ProviderError(
3005
+ throw new ProviderError2(
2752
3006
  `Gremlin traversal failed: ${err instanceof Error ? err.message : String(err)}`,
2753
3007
  "Check the traversal spec and ensure the CosmosDB connection is healthy."
2754
3008
  );
@@ -2766,7 +3020,14 @@ Verify the ARM/Bicep template that provisioned the container.`
2766
3020
  requestCharge: result.requestCharge,
2767
3021
  compiledQuery: scopedQuery
2768
3022
  };
2769
- 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") {
2770
3031
  for (const item of result.items) {
2771
3032
  const stored = entityFromGremlin(item);
2772
3033
  raw.terminalEntities.push(stored);
@@ -2805,7 +3066,7 @@ Verify the ARM/Bicep template that provisioned the container.`
2805
3066
  const stored = relationshipFromGremlin(props);
2806
3067
  raw.relationshipMap.set(stored.id, stored);
2807
3068
  pathRelIds.push(stored.id);
2808
- const direction = lastVertexId === stored.sourceEntityId ? "outbound" : "inbound";
3069
+ const direction = lastVertexId === stored.sourceEntityId ? "out" : "in";
2809
3070
  pathRelDirections.push(direction);
2810
3071
  if (!raw.pathRelFirstDirection.has(stored.id)) {
2811
3072
  raw.pathRelFirstDirection.set(stored.id, direction);
@@ -2854,6 +3115,81 @@ function unwrapGremlinValue(val) {
2854
3115
  if (Array.isArray(val) && val.length > 0) return val[0];
2855
3116
  return val;
2856
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
+ }
2857
3193
  function buildExploreSteps(depth, options) {
2858
3194
  const base = { direction: "both" };
2859
3195
  if (options.relationshipTypes && options.relationshipTypes.length > 0) {