@utaba/deep-memory-storage-cosmosdb 0.18.0 → 0.20.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
@@ -36,7 +36,7 @@ __export(index_exports, {
36
36
  module.exports = __toCommonJS(index_exports);
37
37
 
38
38
  // src/CosmosDbProvider.ts
39
- var import_deep_memory5 = require("@utaba/deep-memory");
39
+ var import_deep_memory6 = require("@utaba/deep-memory");
40
40
 
41
41
  // src/CosmosDbConnection.ts
42
42
  var import_gremlin = __toESM(require("gremlin"), 1);
@@ -319,6 +319,7 @@ function sleep2(ms) {
319
319
  }
320
320
 
321
321
  // src/mapping.ts
322
+ var import_deep_memory = require("@utaba/deep-memory");
322
323
  var STORED_ENTITY_FIELDS = [
323
324
  "id",
324
325
  "entityType",
@@ -659,9 +660,92 @@ function repositoryConfigToLadderBindings(config) {
659
660
  bindings[SENTINEL_BINDING] = ABSENT_STRING_SENTINEL;
660
661
  return bindings;
661
662
  }
663
+ var RESERVED_ENTITY_PROPERTY_KEYS = /* @__PURE__ */ new Set([
664
+ "id",
665
+ "repositoryId",
666
+ ...ENTITY_REQUIRED_SLOTS,
667
+ ...ENTITY_OPTIONAL_SLOTS
668
+ ]);
669
+ var RESERVED_RELATIONSHIP_PROPERTY_KEYS = /* @__PURE__ */ new Set([
670
+ "id",
671
+ "repositoryId",
672
+ "label",
673
+ ...RELATIONSHIP_REQUIRED_SLOTS,
674
+ ...RELATIONSHIP_OPTIONAL_SLOTS
675
+ ]);
676
+ var USER_PROPERTY_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
677
+ function assertSafeUserPropertyKey(key, reserved, scope) {
678
+ if (!USER_PROPERTY_KEY_PATTERN.test(key)) {
679
+ throw new import_deep_memory.ProviderError(
680
+ `${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.`
681
+ );
682
+ }
683
+ if (reserved.has(key)) {
684
+ throw new import_deep_memory.ProviderError(
685
+ `${scope} property key "${key}" collides with a schema-managed field. Reserved names: ${Array.from(reserved).join(", ")}.`
686
+ );
687
+ }
688
+ return key;
689
+ }
690
+ function assertSafeEntityUserPropertyKey(key) {
691
+ return assertSafeUserPropertyKey(key, RESERVED_ENTITY_PROPERTY_KEYS, "Entity");
692
+ }
693
+ function assertSafeRelationshipUserPropertyKey(key) {
694
+ return assertSafeUserPropertyKey(key, RESERVED_RELATIONSHIP_PROPERTY_KEYS, "Relationship");
695
+ }
696
+ function isNativeStorableValue(value) {
697
+ if (typeof value === "string") return true;
698
+ if (typeof value === "boolean") return true;
699
+ if (typeof value === "number") return Number.isFinite(value);
700
+ if (Array.isArray(value)) {
701
+ if (value.length === 0) return true;
702
+ const first = value[0];
703
+ const t = typeof first;
704
+ if (t !== "string" && t !== "boolean" && (t !== "number" || !Number.isFinite(first))) {
705
+ return false;
706
+ }
707
+ for (const v of value) {
708
+ if (typeof v !== t) return false;
709
+ if (t === "number" && !Number.isFinite(v)) return false;
710
+ }
711
+ return true;
712
+ }
713
+ return false;
714
+ }
715
+ function entityUserPropertyParams(properties) {
716
+ const out = [];
717
+ for (const [key, value] of Object.entries(properties)) {
718
+ assertSafeEntityUserPropertyKey(key);
719
+ if (isNativeStorableValue(value)) {
720
+ out.push({ key, value });
721
+ }
722
+ }
723
+ return out;
724
+ }
725
+ function relationshipUserPropertyParams(properties) {
726
+ const out = [];
727
+ for (const [key, value] of Object.entries(properties)) {
728
+ assertSafeRelationshipUserPropertyKey(key);
729
+ if (isNativeStorableValue(value)) {
730
+ out.push({ key, value });
731
+ }
732
+ }
733
+ return out;
734
+ }
735
+ function existingEntityScalarUserKeys(blob) {
736
+ if (blob == null) return [];
737
+ const out = [];
738
+ for (const [key, value] of Object.entries(blob)) {
739
+ if (!USER_PROPERTY_KEY_PATTERN.test(key)) continue;
740
+ if (RESERVED_ENTITY_PROPERTY_KEYS.has(key)) continue;
741
+ if (!isNativeStorableValue(value)) continue;
742
+ out.push(key);
743
+ }
744
+ return out;
745
+ }
662
746
 
663
747
  // src/queries/repository.ts
664
- var import_deep_memory = require("@utaba/deep-memory");
748
+ var import_deep_memory2 = require("@utaba/deep-memory");
665
749
  var REPO_LABEL = "_repository";
666
750
  var REPOSITORY_INDEX_VERTEX_ID = "_repository_index";
667
751
  var REPOSITORY_INDEX_PARTITION = "_index";
@@ -727,7 +811,7 @@ async function createRepository(conn, config) {
727
811
  { vid: vertexId, rid: config.repositoryId, lbl: REPO_LABEL }
728
812
  );
729
813
  if (existing.items.length > 0 && Number(existing.items[0]) > 0) {
730
- throw new import_deep_memory.DuplicateRepositoryError(config.repositoryId);
814
+ throw new import_deep_memory2.DuplicateRepositoryError(config.repositoryId);
731
815
  }
732
816
  const currentIds = await readRepositoryIndex(conn);
733
817
  const updatedIds = currentIds.includes(config.repositoryId) ? currentIds : [...currentIds, config.repositoryId];
@@ -798,7 +882,7 @@ async function listRepositories(conn, filter) {
798
882
  async function updateRepository(conn, repositoryId, updates) {
799
883
  const vertexId = repoVertexId(repositoryId);
800
884
  const existing = await getRepository(conn, repositoryId);
801
- if (!existing) throw new import_deep_memory.RepositoryNotFoundError(repositoryId);
885
+ if (!existing) throw new import_deep_memory2.RepositoryNotFoundError(repositoryId);
802
886
  const bindings = { vid: vertexId, rid: repositoryId };
803
887
  const props = {};
804
888
  if (updates.label !== void 0) props["repoLabel"] = updates.label;
@@ -1033,9 +1117,10 @@ async function getVocabularyChangeLog(conn, repositoryId, options) {
1033
1117
  }
1034
1118
 
1035
1119
  // src/queries/entity.ts
1036
- var import_deep_memory2 = require("@utaba/deep-memory");
1120
+ var import_deep_memory3 = require("@utaba/deep-memory");
1037
1121
  var DUPLICATE_SENTINEL = "__duplicate";
1038
- 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()})`;
1122
+ 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()}`;
1123
+ var ENTITY_CREATE_QUERY = `${ENTITY_CREATE_PREFIX})`;
1039
1124
  async function createEntity(conn, repositoryId, entity) {
1040
1125
  const bindings = {
1041
1126
  rid: repositoryId,
@@ -1043,14 +1128,27 @@ async function createEntity(conn, repositoryId, entity) {
1043
1128
  vertexLabel: entity.entityType,
1044
1129
  ...entityToLadderBindings(entity)
1045
1130
  };
1046
- const result = await conn.submit(ENTITY_CREATE_QUERY, bindings);
1131
+ const userProps = entityUserPropertyParams(entity.properties ?? {});
1132
+ let query;
1133
+ if (userProps.length === 0) {
1134
+ query = ENTITY_CREATE_QUERY;
1135
+ } else {
1136
+ let suffix = "";
1137
+ for (let i = 0; i < userProps.length; i++) {
1138
+ const { key, value } = userProps[i];
1139
+ suffix += `.property('${key}', p_user_${i})`;
1140
+ bindings[`p_user_${i}`] = value;
1141
+ }
1142
+ query = `${ENTITY_CREATE_PREFIX}${suffix})`;
1143
+ }
1144
+ const result = await conn.submit(query, bindings);
1047
1145
  if (result.items[0] === DUPLICATE_SENTINEL) {
1048
- throw new import_deep_memory2.DuplicateEntityError(entity.id);
1146
+ throw new import_deep_memory3.DuplicateEntityError(entity.id);
1049
1147
  }
1050
1148
  return entity;
1051
1149
  }
1052
1150
  async function getEntity(conn, repositoryId, entityId, options) {
1053
- const projection = (0, import_deep_memory2.buildVertexProjectChain)({ withEmbedding: options?.loadEmbeddings });
1151
+ const projection = (0, import_deep_memory3.buildVertexProjectChain)({ withEmbedding: options?.loadEmbeddings });
1054
1152
  const result = await conn.submit(
1055
1153
  `g.V().has('repositoryId', rid).hasId(eid).has('entityType').${projection}`,
1056
1154
  { rid: repositoryId, eid: entityId }
@@ -1059,7 +1157,7 @@ async function getEntity(conn, repositoryId, entityId, options) {
1059
1157
  return entityFromGremlin(result.items[0]);
1060
1158
  }
1061
1159
  async function getEntityBySlug(conn, repositoryId, slug, options) {
1062
- const projection = (0, import_deep_memory2.buildVertexProjectChain)({ withEmbedding: options?.loadEmbeddings });
1160
+ const projection = (0, import_deep_memory3.buildVertexProjectChain)({ withEmbedding: options?.loadEmbeddings });
1063
1161
  const result = await conn.submit(
1064
1162
  `g.V().has('repositoryId', rid).has('slug', slugVal).has('entityType').${projection}`,
1065
1163
  { rid: repositoryId, slugVal: slug }
@@ -1077,7 +1175,7 @@ async function getEntities(conn, repositoryId, entityIds, options) {
1077
1175
  idParams.push(paramName);
1078
1176
  });
1079
1177
  const withinClause = `within(${idParams.join(", ")})`;
1080
- const projection = (0, import_deep_memory2.buildVertexProjectChain)({ withEmbedding: options?.loadEmbeddings });
1178
+ const projection = (0, import_deep_memory3.buildVertexProjectChain)({ withEmbedding: options?.loadEmbeddings });
1081
1179
  const result = await conn.submit(
1082
1180
  `g.V().has('repositoryId', rid).hasId(${withinClause}).has('entityType').${projection}`,
1083
1181
  bindings
@@ -1089,7 +1187,36 @@ async function getEntities(conn, repositoryId, entityIds, options) {
1089
1187
  }
1090
1188
  return map;
1091
1189
  }
1190
+ async function readExistingEntityPropertiesBlob(conn, repositoryId, entityId) {
1191
+ const result = await conn.submit(
1192
+ "g.V().has('repositoryId', rid).hasId(eid).has('entityType').values('properties').limit(1)",
1193
+ { rid: repositoryId, eid: entityId }
1194
+ );
1195
+ if (result.items.length === 0) return { found: false };
1196
+ const raw = result.items[0];
1197
+ const json = typeof raw === "string" ? raw : String(raw ?? "");
1198
+ if (!json) return { found: true, blob: {} };
1199
+ try {
1200
+ const parsed = JSON.parse(json);
1201
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1202
+ return { found: true, blob: parsed };
1203
+ }
1204
+ } catch {
1205
+ }
1206
+ return { found: true, blob: {} };
1207
+ }
1092
1208
  async function updateEntity(conn, repositoryId, entityId, updates) {
1209
+ const userProps = updates.properties !== void 0 ? entityUserPropertyParams(updates.properties) : null;
1210
+ let droppedUserKeys = [];
1211
+ if (updates.properties !== void 0) {
1212
+ const existing = await readExistingEntityPropertiesBlob(conn, repositoryId, entityId);
1213
+ if (!existing.found) {
1214
+ throw new import_deep_memory3.EntityNotFoundError(entityId);
1215
+ }
1216
+ const existingKeys = existingEntityScalarUserKeys(existing.blob);
1217
+ const newKeySet = new Set(userProps.map((p) => p.key));
1218
+ droppedUserKeys = existingKeys.filter((k) => !newKeySet.has(k));
1219
+ }
1093
1220
  const bindings = { rid: repositoryId, eid: entityId };
1094
1221
  const propParts = [];
1095
1222
  let idx = 0;
@@ -1106,7 +1233,18 @@ async function updateEntity(conn, repositoryId, entityId, updates) {
1106
1233
  if (updates.slug !== void 0) addProp("slug", updates.slug);
1107
1234
  if (updates.summary === null) dropProp("summary");
1108
1235
  else if (updates.summary !== void 0) addProp("summary", updates.summary);
1109
- if (updates.properties !== void 0) addProp("properties", JSON.stringify(updates.properties));
1236
+ if (updates.properties !== void 0 && userProps !== null) {
1237
+ addProp("properties", JSON.stringify(updates.properties));
1238
+ for (const dropKey of droppedUserKeys) {
1239
+ dropProp(dropKey);
1240
+ }
1241
+ for (let i = 0; i < userProps.length; i++) {
1242
+ const { key, value } = userProps[i];
1243
+ const paramName = `p_user_${i}`;
1244
+ bindings[paramName] = value;
1245
+ propParts.push(`.property('${key}', ${paramName})`);
1246
+ }
1247
+ }
1110
1248
  if (updates.data === null) dropProp("data");
1111
1249
  else if (updates.data !== void 0) addProp("data", updates.data);
1112
1250
  if (updates.dataFormat === null) dropProp("dataFormat");
@@ -1117,11 +1255,11 @@ async function updateEntity(conn, repositoryId, entityId, updates) {
1117
1255
  addProp("modifiedAt", updates.provenance.modifiedAt);
1118
1256
  if (updates.provenance.modifiedInConversation != null) addProp("modifiedInConversation", updates.provenance.modifiedInConversation);
1119
1257
  if (updates.provenance.modifiedFromMessage != null) addProp("modifiedFromMessage", updates.provenance.modifiedFromMessage);
1120
- const projection = (0, import_deep_memory2.buildVertexProjectChain)();
1258
+ const projection = (0, import_deep_memory3.buildVertexProjectChain)();
1121
1259
  const query = `g.V().has('repositoryId', rid).hasId(eid).has('entityType')${propParts.join("")}.${projection}`;
1122
1260
  const result = await conn.submit(query, bindings);
1123
1261
  if (result.items.length === 0) {
1124
- throw new import_deep_memory2.EntityNotFoundError(entityId);
1262
+ throw new import_deep_memory3.EntityNotFoundError(entityId);
1125
1263
  }
1126
1264
  return entityFromGremlin(result.items[0]);
1127
1265
  }
@@ -1161,16 +1299,33 @@ function buildWhereClause(query, repositoryId) {
1161
1299
  `(CONTAINS(${sqlPath("entityLabel")}, @term, true) OR CONTAINS(${sqlPath("slug")}, @term, true) OR CONTAINS(${sqlPath("summary")}, @term, true))`
1162
1300
  );
1163
1301
  }
1164
- if (query.properties != null) {
1165
- let i = 0;
1166
- for (const [key, value] of Object.entries(query.properties)) {
1167
- const fragment = JSON.stringify({ [key]: value }).slice(1, -1);
1168
- const name = `@kv${i++}`;
1169
- params.push({ name, value: fragment });
1170
- predicates.push(`CONTAINS(${sqlPath("properties")}, ${name}, false)`);
1302
+ let propertyFilterMode = "none";
1303
+ if (query.properties != null && Object.keys(query.properties).length > 0) {
1304
+ const entries = Object.entries(query.properties);
1305
+ const allStorable = entries.every(([, value]) => isNativeStorableValue(value));
1306
+ if (allStorable) {
1307
+ for (const [key] of entries) {
1308
+ assertSafeEntityUserPropertyKey(key);
1309
+ }
1310
+ let i = 0;
1311
+ for (const [key, value] of entries) {
1312
+ const name = `@val${i++}`;
1313
+ params.push({ name, value });
1314
+ predicates.push(`${sqlPath(key)} = ${name}`);
1315
+ }
1316
+ propertyFilterMode = "exact";
1317
+ } else {
1318
+ let i = 0;
1319
+ for (const [key, value] of entries) {
1320
+ const fragment = JSON.stringify({ [key]: value }).slice(1, -1);
1321
+ const name = `@kv${i++}`;
1322
+ params.push({ name, value: fragment });
1323
+ predicates.push(`CONTAINS(${sqlPath("properties")}, ${name}, false)`);
1324
+ }
1325
+ propertyFilterMode = "approximate";
1171
1326
  }
1172
1327
  }
1173
- return { sqlWhere: `WHERE ${predicates.join(" AND ")}`, params };
1328
+ return { sqlWhere: `WHERE ${predicates.join(" AND ")}`, params, propertyFilterMode };
1174
1329
  }
1175
1330
  function buildSelectClause(loadEmbeddings) {
1176
1331
  const fields = ["c.id", ...STORED_ENTITY_FIELDS.filter((f) => f !== "id").map((f) => `c.${f}`)];
@@ -1178,7 +1333,7 @@ function buildSelectClause(loadEmbeddings) {
1178
1333
  return `SELECT ${fields.join(", ")}`;
1179
1334
  }
1180
1335
  async function findEntities(docClient, repositoryId, query, options) {
1181
- const { sqlWhere, params } = buildWhereClause(query, repositoryId);
1336
+ const { sqlWhere, params, propertyFilterMode } = buildWhereClause(query, repositoryId);
1182
1337
  const dataParams = [
1183
1338
  ...params,
1184
1339
  { name: "@off", value: query.offset },
@@ -1187,7 +1342,7 @@ async function findEntities(docClient, repositoryId, query, options) {
1187
1342
  const selectClause = buildSelectClause(options?.loadEmbeddings === true);
1188
1343
  const dataSql = `${selectClause} FROM c ${sqlWhere} ORDER BY c.id OFFSET @off LIMIT @lim`;
1189
1344
  const countSql = `SELECT VALUE COUNT(1) FROM c ${sqlWhere}`;
1190
- const skipCount = query.properties != null && Object.keys(query.properties).length > 0;
1345
+ const skipCount = propertyFilterMode === "approximate";
1191
1346
  const [dataResult, countResult] = await Promise.all([
1192
1347
  docClient.query(dataSql, dataParams, {
1193
1348
  partitionKey: repositoryId
@@ -1199,7 +1354,7 @@ async function findEntities(docClient, repositoryId, query, options) {
1199
1354
  const filters = Object.entries(query.properties).map(
1200
1355
  ([key, value]) => ({ key, operator: "eq", value })
1201
1356
  );
1202
- items = items.filter((entity) => (0, import_deep_memory2.matchesPropertyFilters)(entity.properties, filters));
1357
+ items = items.filter((entity) => (0, import_deep_memory3.matchesPropertyFilters)(entity.properties, filters));
1203
1358
  }
1204
1359
  const total = countResult && countResult.documents.length > 0 ? Number(countResult.documents[0]) : void 0;
1205
1360
  const hasMore = total != null ? query.offset + items.length < total : items.length === query.limit;
@@ -1213,9 +1368,10 @@ async function findEntities(docClient, repositoryId, query, options) {
1213
1368
  }
1214
1369
 
1215
1370
  // src/queries/relationship.ts
1216
- var import_deep_memory3 = require("@utaba/deep-memory");
1371
+ var import_deep_memory4 = require("@utaba/deep-memory");
1217
1372
  var DUPLICATE_SENTINEL2 = "__duplicate";
1218
- 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()})`;
1373
+ 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()}`;
1374
+ var RELATIONSHIP_CREATE_QUERY = `${RELATIONSHIP_CREATE_PREFIX})`;
1219
1375
  async function createRelationship(conn, repositoryId, relationship) {
1220
1376
  const bindings = {
1221
1377
  rid: repositoryId,
@@ -1225,14 +1381,27 @@ async function createRelationship(conn, repositoryId, relationship) {
1225
1381
  edgeLabel: relationship.relationshipType,
1226
1382
  ...relationshipToLadderBindings(relationship)
1227
1383
  };
1228
- const result = await conn.submit(RELATIONSHIP_CREATE_QUERY, bindings);
1384
+ const userProps = relationshipUserPropertyParams(relationship.properties ?? {});
1385
+ let query;
1386
+ if (userProps.length === 0) {
1387
+ query = RELATIONSHIP_CREATE_QUERY;
1388
+ } else {
1389
+ let suffix = "";
1390
+ for (let i = 0; i < userProps.length; i++) {
1391
+ const { key, value } = userProps[i];
1392
+ suffix += `.property('${key}', p_user_${i})`;
1393
+ bindings[`p_user_${i}`] = value;
1394
+ }
1395
+ query = `${RELATIONSHIP_CREATE_PREFIX}${suffix})`;
1396
+ }
1397
+ const result = await conn.submit(query, bindings);
1229
1398
  if (result.items[0] === DUPLICATE_SENTINEL2) {
1230
- throw new import_deep_memory3.DuplicateRelationshipError(relationship.id);
1399
+ throw new import_deep_memory4.DuplicateRelationshipError(relationship.id);
1231
1400
  }
1232
1401
  return relationship;
1233
1402
  }
1234
1403
  async function getRelationship(conn, repositoryId, relationshipId) {
1235
- const projection = (0, import_deep_memory3.buildEdgeProjectChain)();
1404
+ const projection = (0, import_deep_memory4.buildEdgeProjectChain)();
1236
1405
  const result = await conn.submit(
1237
1406
  `g.E().hasId(relId).has('repositoryId', rid).${projection}`,
1238
1407
  { relId: relationshipId, rid: repositoryId }
@@ -1279,7 +1448,7 @@ async function getEntityRelationships(conn, repositoryId, entityId, options) {
1279
1448
  unionQuery = `g.V().has('repositoryId', rid).hasId(eid).has('entityType').union(inE()${typeFilter}, outE()${typeFilter}.has('bidirectional', true))`;
1280
1449
  }
1281
1450
  const baseQuery = unionQuery ?? `${edgeTraversal}${typeFilter}`;
1282
- const projection = (0, import_deep_memory3.buildEdgeProjectChain)();
1451
+ const projection = (0, import_deep_memory4.buildEdgeProjectChain)();
1283
1452
  const dataBindings = { ...baseBindings, rangeStart: offset, rangeEnd: offset + limit };
1284
1453
  const [countResult, dataResult] = await Promise.all([
1285
1454
  hasPropertyFilters ? Promise.resolve(null) : conn.submit(`${baseQuery}.dedup().count()`, baseBindings),
@@ -1291,7 +1460,7 @@ async function getEntityRelationships(conn, repositoryId, entityId, options) {
1291
1460
  const rawItems = dataResult.items;
1292
1461
  let items = rawItems.map(relationshipFromGremlin);
1293
1462
  if (hasPropertyFilters) {
1294
- items = items.filter((rel) => (0, import_deep_memory3.matchesPropertyFilters)(rel.properties, options.propertyFilters));
1463
+ items = items.filter((rel) => (0, import_deep_memory4.matchesPropertyFilters)(rel.properties, options.propertyFilters));
1295
1464
  }
1296
1465
  const total = countResult ? Number(countResult.items[0] ?? 0) : void 0;
1297
1466
  const hasMore = total != null ? offset + rawItems.length < total : rawItems.length === limit;
@@ -1381,7 +1550,7 @@ function isInTimeRange(timestamp, timeRange) {
1381
1550
  }
1382
1551
 
1383
1552
  // src/queries/adaptive-import.ts
1384
- var import_deep_memory4 = require("@utaba/deep-memory");
1553
+ var import_deep_memory5 = require("@utaba/deep-memory");
1385
1554
  var DEFAULT_MIN = 1;
1386
1555
  var DEFAULT_START = 5;
1387
1556
  var DEFAULT_MAX = 32;
@@ -1632,7 +1801,7 @@ async function runAdaptive(items, controller, fn) {
1632
1801
  }
1633
1802
  await Promise.all(workers);
1634
1803
  if (aborted) {
1635
- throw new import_deep_memory4.ImportThrottleAbortError(
1804
+ throw new import_deep_memory5.ImportThrottleAbortError(
1636
1805
  controller.getConcurrency(),
1637
1806
  controller.getConsecutiveThrottlesAtMin(),
1638
1807
  controller.getCompleted(),
@@ -1778,8 +1947,12 @@ var ENTITY_LADDER_CHAIN = buildEntityPropertyLadder();
1778
1947
  var RELATIONSHIP_LADDER_CHAIN = buildRelationshipPropertyLadder();
1779
1948
  var INSERT_ENTITY_QUERY = `g.addV(vertexLabel).property('id', vid).property('repositoryId', rid)${ENTITY_LADDER_CHAIN}`;
1780
1949
  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}`;
1781
- 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})`;
1782
- 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})`;
1950
+ var UPSERT_ENTITY_OPEN = `g.V().has('repositoryId', rid).hasId(vid).has('entityType').fold().coalesce(unfold()${ENTITY_LADDER_CHAIN}`;
1951
+ var UPSERT_ENTITY_CREATE_BRANCH = `, addV(vertexLabel).property('id', vid).property('repositoryId', rid)${ENTITY_LADDER_CHAIN}`;
1952
+ var UPSERT_ENTITY_QUERY = `${UPSERT_ENTITY_OPEN}${UPSERT_ENTITY_CREATE_BRANCH})`;
1953
+ var UPSERT_RELATIONSHIP_OPEN = `g.E().has('repositoryId', rid).hasId(relId).fold().coalesce(unfold()${RELATIONSHIP_LADDER_CHAIN}`;
1954
+ 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}`;
1955
+ var UPSERT_RELATIONSHIP_QUERY = `${UPSERT_RELATIONSHIP_OPEN}${UPSERT_RELATIONSHIP_CREATE_BRANCH})`;
1783
1956
  async function insertEntity(conn, repositoryId, entity) {
1784
1957
  const bindings = {
1785
1958
  rid: repositoryId,
@@ -1787,7 +1960,20 @@ async function insertEntity(conn, repositoryId, entity) {
1787
1960
  vertexLabel: entity.entityType,
1788
1961
  ...entityToLadderBindings(entity)
1789
1962
  };
1790
- await conn.submit(INSERT_ENTITY_QUERY, bindings);
1963
+ const userProps = entityUserPropertyParams(entity.properties ?? {});
1964
+ let query;
1965
+ if (userProps.length === 0) {
1966
+ query = INSERT_ENTITY_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_ENTITY_QUERY}${suffix}`;
1975
+ }
1976
+ await conn.submit(query, bindings);
1791
1977
  }
1792
1978
  async function insertRelationship(conn, repositoryId, rel) {
1793
1979
  const bindings = {
@@ -1798,7 +1984,20 @@ async function insertRelationship(conn, repositoryId, rel) {
1798
1984
  edgeLabel: rel.relationshipType,
1799
1985
  ...relationshipToLadderBindings(rel)
1800
1986
  };
1801
- await conn.submit(INSERT_RELATIONSHIP_QUERY, bindings);
1987
+ const userProps = relationshipUserPropertyParams(rel.properties ?? {});
1988
+ let query;
1989
+ if (userProps.length === 0) {
1990
+ query = INSERT_RELATIONSHIP_QUERY;
1991
+ } else {
1992
+ let suffix = "";
1993
+ for (let i = 0; i < userProps.length; i++) {
1994
+ const { key, value } = userProps[i];
1995
+ suffix += `.property('${key}', p_user_${i})`;
1996
+ bindings[`p_user_${i}`] = value;
1997
+ }
1998
+ query = `${INSERT_RELATIONSHIP_QUERY}${suffix}`;
1999
+ }
2000
+ await conn.submit(query, bindings);
1802
2001
  }
1803
2002
  async function upsertEntity(conn, repositoryId, entity) {
1804
2003
  const bindings = {
@@ -1807,7 +2006,20 @@ async function upsertEntity(conn, repositoryId, entity) {
1807
2006
  vertexLabel: entity.entityType,
1808
2007
  ...entityToLadderBindings(entity)
1809
2008
  };
1810
- await conn.submit(UPSERT_ENTITY_QUERY, bindings);
2009
+ const userProps = entityUserPropertyParams(entity.properties ?? {});
2010
+ let query;
2011
+ if (userProps.length === 0) {
2012
+ query = UPSERT_ENTITY_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_ENTITY_OPEN}${suffix}${UPSERT_ENTITY_CREATE_BRANCH}${suffix})`;
2021
+ }
2022
+ await conn.submit(query, bindings);
1811
2023
  }
1812
2024
  async function upsertRelationship(conn, repositoryId, rel) {
1813
2025
  const bindings = {
@@ -1818,7 +2030,20 @@ async function upsertRelationship(conn, repositoryId, rel) {
1818
2030
  edgeLabel: rel.relationshipType,
1819
2031
  ...relationshipToLadderBindings(rel)
1820
2032
  };
1821
- await conn.submit(UPSERT_RELATIONSHIP_QUERY, bindings);
2033
+ const userProps = relationshipUserPropertyParams(rel.properties ?? {});
2034
+ let query;
2035
+ if (userProps.length === 0) {
2036
+ query = UPSERT_RELATIONSHIP_QUERY;
2037
+ } else {
2038
+ let suffix = "";
2039
+ for (let i = 0; i < userProps.length; i++) {
2040
+ const { key, value } = userProps[i];
2041
+ suffix += `.property('${key}', p_user_${i})`;
2042
+ bindings[`p_user_${i}`] = value;
2043
+ }
2044
+ query = `${UPSERT_RELATIONSHIP_OPEN}${suffix}${UPSERT_RELATIONSHIP_CREATE_BRANCH}${suffix})`;
2045
+ }
2046
+ await conn.submit(query, bindings);
1822
2047
  }
1823
2048
 
1824
2049
  // src/CosmosDbProvider.ts
@@ -1838,7 +2063,7 @@ var CosmosDbProvider = class {
1838
2063
  conn;
1839
2064
  docClient;
1840
2065
  config;
1841
- compiler = new import_deep_memory5.GremlinCompiler();
2066
+ compiler = new import_deep_memory6.GremlinCompiler();
1842
2067
  reportUsage;
1843
2068
  /**
1844
2069
  * Per-process vocabulary cache, keyed by repositoryId. Read lazily by
@@ -1849,7 +2074,7 @@ var CosmosDbProvider = class {
1849
2074
  vocabularyCache = /* @__PURE__ */ new Map();
1850
2075
  constructor(config) {
1851
2076
  this.config = config;
1852
- this.reportUsage = (0, import_deep_memory5.createSafeSink)(config.reportUsage);
2077
+ this.reportUsage = (0, import_deep_memory6.createSafeSink)(config.reportUsage);
1853
2078
  this.conn = new CosmosDbConnection({
1854
2079
  endpoint: config.endpoint,
1855
2080
  key: config.key,
@@ -1912,8 +2137,8 @@ var CosmosDbProvider = class {
1912
2137
  * ever reaching query construction.
1913
2138
  */
1914
2139
  assertValidRepositoryId(repositoryId) {
1915
- if (!(0, import_deep_memory5.isValidUuid)(repositoryId)) {
1916
- throw new import_deep_memory5.InvalidInputError(
2140
+ if (!(0, import_deep_memory6.isValidUuid)(repositoryId)) {
2141
+ throw new import_deep_memory6.InvalidInputError(
1917
2142
  "repositoryId",
1918
2143
  `repositoryId must be a valid v4 UUID; got '${repositoryId}'`
1919
2144
  );
@@ -1989,7 +2214,7 @@ var CosmosDbProvider = class {
1989
2214
  schemaVersion: SCHEMA_VERSION
1990
2215
  };
1991
2216
  } catch (err) {
1992
- throw new import_deep_memory5.ProviderError(
2217
+ throw new import_deep_memory6.ProviderError(
1993
2218
  `Failed to ensure CosmosDB schema: ${err instanceof Error ? err.message : String(err)}`,
1994
2219
  "Verify the CosmosDB REST endpoint is accessible and the Gremlin endpoint is reachable."
1995
2220
  );
@@ -2406,7 +2631,7 @@ Verify the ARM/Bicep template that provisioned the container.`
2406
2631
  }
2407
2632
  const layer = {};
2408
2633
  const nextFrontier = /* @__PURE__ */ new Set();
2409
- const layerEdgeSeen = /* @__PURE__ */ new Set();
2634
+ const layerBucketSeen = /* @__PURE__ */ new Map();
2410
2635
  for (const fv of frontier) {
2411
2636
  const incident = edgesByVertex.get(fv) ?? [];
2412
2637
  for (const rel of incident) {
@@ -2432,17 +2657,21 @@ Verify the ARM/Bicep template that provisioned the container.`
2432
2657
  if (!matchesDirection || !connectedId) continue;
2433
2658
  if (visited.has(connectedId)) continue;
2434
2659
  if (options.relationshipPropertyFilters && options.relationshipPropertyFilters.length > 0) {
2435
- if (!(0, import_deep_memory5.matchesPropertyFilters)(rel.properties, options.relationshipPropertyFilters)) continue;
2660
+ if (!(0, import_deep_memory6.matchesPropertyFilters)(rel.properties, options.relationshipPropertyFilters)) continue;
2436
2661
  }
2437
2662
  const connectedEntity = raw.entityMap.get(connectedId);
2438
2663
  if (!connectedEntity) continue;
2439
2664
  if (options.entityTypes && options.entityTypes.length > 0 && !options.entityTypes.includes(connectedEntity.entityType)) {
2440
2665
  continue;
2441
2666
  }
2442
- const edgeKey = `${rel.id}|${fv}->${connectedId}`;
2443
- if (layerEdgeSeen.has(edgeKey)) continue;
2444
- layerEdgeSeen.add(edgeKey);
2445
2667
  const relType = rel.relationshipType;
2668
+ let bucketSeen = layerBucketSeen.get(relType);
2669
+ if (!bucketSeen) {
2670
+ bucketSeen = /* @__PURE__ */ new Set();
2671
+ layerBucketSeen.set(relType, bucketSeen);
2672
+ }
2673
+ if (bucketSeen.has(connectedId)) continue;
2674
+ bucketSeen.add(connectedId);
2446
2675
  if (!layer[relType]) {
2447
2676
  layer[relType] = { total: 0, entities: [], relationships: [] };
2448
2677
  }
@@ -2660,7 +2889,7 @@ Verify the ARM/Bicep template that provisioned the container.`
2660
2889
  const raw = await this.executeTraversal(repositoryId, spec);
2661
2890
  const detailLevel = spec.detailLevel ?? "summary";
2662
2891
  const projectStoredEntity = (stored) => {
2663
- const projected = (0, import_deep_memory5.projectEntity)(stored, detailLevel);
2892
+ const projected = (0, import_deep_memory6.projectEntity)(stored, detailLevel);
2664
2893
  if (!spec.includeProvenance) {
2665
2894
  delete projected["provenance"];
2666
2895
  }
@@ -2677,8 +2906,11 @@ Verify the ARM/Bicep template that provisioned the container.`
2677
2906
  let entities = [];
2678
2907
  let relationships;
2679
2908
  let paths;
2909
+ const aggregations = raw.aggregations;
2910
+ const projectionEmitted = aggregations !== void 0;
2680
2911
  const rangeRowCount = spec.returnMode === "all" ? raw.allEntities.length + raw.allRelationships.length : 0;
2681
- if (spec.returnMode === "terminal") {
2912
+ if (projectionEmitted) {
2913
+ } else if (spec.returnMode === "terminal") {
2682
2914
  entities = raw.terminalEntities.map(projectStoredEntity);
2683
2915
  relationships = void 0;
2684
2916
  } else if (spec.returnMode === "all") {
@@ -2706,7 +2938,7 @@ Verify the ARM/Bicep template that provisioned the container.`
2706
2938
  entities: row.entityIds.map((id) => {
2707
2939
  const stored = raw.entityMap.get(id);
2708
2940
  if (!stored) {
2709
- throw new import_deep_memory5.ProviderError(
2941
+ throw new import_deep_memory6.ProviderError(
2710
2942
  "Unpacking Gremlin path: entity referenced by path is missing from the result",
2711
2943
  "This indicates a Gremlin response shape mismatch \u2014 inspect compiledQuery."
2712
2944
  );
@@ -2716,7 +2948,7 @@ Verify the ARM/Bicep template that provisioned the container.`
2716
2948
  relationships: row.relationshipIds.map((id, i) => {
2717
2949
  const stored = raw.relationshipMap.get(id);
2718
2950
  if (!stored) {
2719
- throw new import_deep_memory5.ProviderError(
2951
+ throw new import_deep_memory6.ProviderError(
2720
2952
  "Unpacking Gremlin path: relationship referenced by path is missing from the result",
2721
2953
  "This indicates a Gremlin response shape mismatch \u2014 inspect compiledQuery."
2722
2954
  );
@@ -2730,7 +2962,9 @@ Verify the ARM/Bicep template that provisioned the container.`
2730
2962
  }
2731
2963
  const limit = spec.limit ?? 50;
2732
2964
  let total;
2733
- if (spec.returnMode === "path") {
2965
+ if (projectionEmitted) {
2966
+ total = aggregations.length;
2967
+ } else if (spec.returnMode === "path") {
2734
2968
  total = paths?.length ?? 0;
2735
2969
  } else if (spec.returnMode === "all") {
2736
2970
  total = entities.length + (relationships?.length ?? 0);
@@ -2755,6 +2989,7 @@ Verify the ARM/Bicep template that provisioned the container.`
2755
2989
  entities,
2756
2990
  relationships,
2757
2991
  paths,
2992
+ ...aggregations !== void 0 ? { aggregations } : {},
2758
2993
  total,
2759
2994
  returned: total,
2760
2995
  hasMore: truncated,
@@ -2791,7 +3026,7 @@ Verify the ARM/Bicep template that provisioned the container.`
2791
3026
  try {
2792
3027
  result = await this.conn.submit(scopedQuery, scopedParams);
2793
3028
  } catch (err) {
2794
- throw new import_deep_memory5.ProviderError(
3029
+ throw new import_deep_memory6.ProviderError(
2795
3030
  `Gremlin traversal failed: ${err instanceof Error ? err.message : String(err)}`,
2796
3031
  "Check the traversal spec and ensure the CosmosDB connection is healthy."
2797
3032
  );
@@ -2809,7 +3044,14 @@ Verify the ARM/Bicep template that provisioned the container.`
2809
3044
  requestCharge: result.requestCharge,
2810
3045
  compiledQuery: scopedQuery
2811
3046
  };
2812
- if (spec.returnMode === "terminal") {
3047
+ const emitsProjection = spec.projection !== void 0 && spec.returnMode !== "all" && spec.returnMode !== "path";
3048
+ if (emitsProjection) {
3049
+ raw.aggregations = parseProjectionRows(
3050
+ result.items,
3051
+ spec.projection.properties,
3052
+ spec.projection.mode ?? "values"
3053
+ );
3054
+ } else if (spec.returnMode === "terminal") {
2813
3055
  for (const item of result.items) {
2814
3056
  const stored = entityFromGremlin(item);
2815
3057
  raw.terminalEntities.push(stored);
@@ -2897,6 +3139,81 @@ function unwrapGremlinValue(val) {
2897
3139
  if (Array.isArray(val) && val.length > 0) return val[0];
2898
3140
  return val;
2899
3141
  }
3142
+ function parseProjectionRows(items, properties, mode) {
3143
+ const singleProperty = properties.length === 1;
3144
+ const aggregations = [];
3145
+ if (mode === "count") {
3146
+ for (const item of items) {
3147
+ const entry = readEntry(item);
3148
+ if (entry === void 0) continue;
3149
+ const values = singleProperty ? { [properties[0]]: normaliseProjectionScalar(entry.key) } : readProjectedMap(entry.key, properties);
3150
+ aggregations.push({ values, count: coerceCount(entry.value) });
3151
+ }
3152
+ return aggregations;
3153
+ }
3154
+ for (const item of items) {
3155
+ const values = singleProperty ? { [properties[0]]: normaliseProjectionScalar(item) } : readProjectedMap(item, properties);
3156
+ aggregations.push({ values });
3157
+ }
3158
+ return aggregations;
3159
+ }
3160
+ function readEntry(item) {
3161
+ if (item instanceof Map) {
3162
+ if (item.has("key") && item.has("value")) {
3163
+ return { key: item.get("key"), value: item.get("value") };
3164
+ }
3165
+ const first = item.entries().next();
3166
+ if (!first.done) {
3167
+ const [key, value] = first.value;
3168
+ return { key, value };
3169
+ }
3170
+ return void 0;
3171
+ }
3172
+ if (typeof item !== "object" || item === null) return void 0;
3173
+ const obj = item;
3174
+ if ("key" in obj && "value" in obj) {
3175
+ return { key: obj["key"], value: obj["value"] };
3176
+ }
3177
+ const keys = Object.keys(obj);
3178
+ if (keys.length === 1) {
3179
+ const k = keys[0];
3180
+ return { key: k, value: obj[k] };
3181
+ }
3182
+ return void 0;
3183
+ }
3184
+ function readProjectedMap(row, properties) {
3185
+ const out = {};
3186
+ if (row instanceof Map) {
3187
+ for (const prop of properties) {
3188
+ out[prop] = normaliseProjectionScalar(row.get(prop));
3189
+ }
3190
+ return out;
3191
+ }
3192
+ if (typeof row === "object" && row !== null) {
3193
+ const obj = row;
3194
+ for (const prop of properties) {
3195
+ out[prop] = normaliseProjectionScalar(obj[prop]);
3196
+ }
3197
+ return out;
3198
+ }
3199
+ for (const prop of properties) {
3200
+ out[prop] = void 0;
3201
+ }
3202
+ return out;
3203
+ }
3204
+ function normaliseProjectionScalar(value) {
3205
+ if (typeof value === "bigint") return Number(value);
3206
+ return value;
3207
+ }
3208
+ function coerceCount(value) {
3209
+ if (typeof value === "number") return value;
3210
+ if (typeof value === "bigint") return Number(value);
3211
+ if (typeof value === "string") {
3212
+ const n = Number(value);
3213
+ return Number.isFinite(n) ? n : 0;
3214
+ }
3215
+ return 0;
3216
+ }
2900
3217
  function buildExploreSteps(depth, options) {
2901
3218
  const base = { direction: "both" };
2902
3219
  if (options.relationshipTypes && options.relationshipTypes.length > 0) {