@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/README.md +19 -12
- package/dist/index.cjs +409 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +385 -49
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1120
|
+
var import_deep_memory3 = require("@utaba/deep-memory");
|
|
1037
1121
|
var DUPLICATE_SENTINEL = "__duplicate";
|
|
1038
|
-
var
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
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 =
|
|
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,
|
|
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
|
|
1371
|
+
var import_deep_memory4 = require("@utaba/deep-memory");
|
|
1217
1372
|
var DUPLICATE_SENTINEL2 = "__duplicate";
|
|
1218
|
-
var
|
|
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
|
|
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
|
|
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,
|
|
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 }
|
|
@@ -1251,10 +1420,10 @@ async function getEntityRelationships(conn, repositoryId, entityId, options) {
|
|
|
1251
1420
|
};
|
|
1252
1421
|
let edgeTraversal;
|
|
1253
1422
|
switch (direction) {
|
|
1254
|
-
case "
|
|
1423
|
+
case "out":
|
|
1255
1424
|
edgeTraversal = "g.V().has('repositoryId', rid).hasId(eid).has('entityType').outE()";
|
|
1256
1425
|
break;
|
|
1257
|
-
case "
|
|
1426
|
+
case "in":
|
|
1258
1427
|
edgeTraversal = "g.V().has('repositoryId', rid).hasId(eid).has('entityType').inE()";
|
|
1259
1428
|
break;
|
|
1260
1429
|
case "both":
|
|
@@ -1273,13 +1442,13 @@ async function getEntityRelationships(conn, repositoryId, entityId, options) {
|
|
|
1273
1442
|
typeFilter = `.hasLabel(${typeParams.join(", ")})`;
|
|
1274
1443
|
}
|
|
1275
1444
|
let unionQuery = null;
|
|
1276
|
-
if (direction === "
|
|
1445
|
+
if (direction === "out") {
|
|
1277
1446
|
unionQuery = `g.V().has('repositoryId', rid).hasId(eid).has('entityType').union(outE()${typeFilter}, inE()${typeFilter}.has('bidirectional', true))`;
|
|
1278
|
-
} else if (direction === "
|
|
1447
|
+
} else if (direction === "in") {
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
1782
|
-
var
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
1916
|
-
throw new
|
|
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
|
|
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
|
);
|
|
@@ -2364,11 +2589,11 @@ Verify the ARM/Bicep template that provisioned the container.`
|
|
|
2364
2589
|
*
|
|
2365
2590
|
* The server-side step direction is fixed to `'both'` (catches every edge
|
|
2366
2591
|
* in either direction). The directional + bidirectional filter is applied
|
|
2367
|
-
* client-side per hop — i.e. `direction: '
|
|
2592
|
+
* client-side per hop — i.e. `direction: 'out'` includes inbound edges
|
|
2368
2593
|
* where `bidirectional` is true. The CosmosDB Gremlin compiler does not
|
|
2369
2594
|
* natively express the `union(outE, inE.has(bidirectional))` shape that
|
|
2370
2595
|
* would push this filter server-side, so it stays client-side; the
|
|
2371
|
-
* observable contract (which edges count toward
|
|
2596
|
+
* observable contract (which edges count toward `'out'` given
|
|
2372
2597
|
* bidirectionality) is preserved.
|
|
2373
2598
|
*
|
|
2374
2599
|
* Round-trips per call: `options.depth` (one per layer). The previous BFS
|
|
@@ -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
|
|
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) {
|
|
@@ -2414,17 +2639,17 @@ Verify the ARM/Bicep template that provisioned the container.`
|
|
|
2414
2639
|
const isTarget = rel.targetEntityId === fv;
|
|
2415
2640
|
let matchesDirection = false;
|
|
2416
2641
|
let connectedId;
|
|
2417
|
-
if (isSource && (options.direction === "
|
|
2642
|
+
if (isSource && (options.direction === "out" || options.direction === "both")) {
|
|
2418
2643
|
matchesDirection = true;
|
|
2419
2644
|
connectedId = rel.targetEntityId;
|
|
2420
|
-
} else if (isTarget && (options.direction === "
|
|
2645
|
+
} else if (isTarget && (options.direction === "in" || options.direction === "both")) {
|
|
2421
2646
|
matchesDirection = true;
|
|
2422
2647
|
connectedId = rel.sourceEntityId;
|
|
2423
2648
|
} else if (rel.bidirectional) {
|
|
2424
|
-
if (isSource && options.direction === "
|
|
2649
|
+
if (isSource && options.direction === "in") {
|
|
2425
2650
|
matchesDirection = true;
|
|
2426
2651
|
connectedId = rel.targetEntityId;
|
|
2427
|
-
} else if (isTarget && options.direction === "
|
|
2652
|
+
} else if (isTarget && options.direction === "out") {
|
|
2428
2653
|
matchesDirection = true;
|
|
2429
2654
|
connectedId = rel.sourceEntityId;
|
|
2430
2655
|
}
|
|
@@ -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,
|
|
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,13 +2889,13 @@ 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,
|
|
2892
|
+
const projected = (0, import_deep_memory6.projectEntity)(stored, detailLevel);
|
|
2664
2893
|
if (!spec.includeProvenance) {
|
|
2665
2894
|
delete projected["provenance"];
|
|
2666
2895
|
}
|
|
2667
2896
|
return projected;
|
|
2668
2897
|
};
|
|
2669
|
-
const projectStoredRelationship = (rel, direction = "
|
|
2898
|
+
const projectStoredRelationship = (rel, direction = "out") => ({
|
|
2670
2899
|
id: rel.id,
|
|
2671
2900
|
type: rel.relationshipType,
|
|
2672
2901
|
sourceEntityId: rel.sourceEntityId,
|
|
@@ -2677,10 +2906,30 @@ Verify the ARM/Bicep template that provisioned the container.`
|
|
|
2677
2906
|
let entities = [];
|
|
2678
2907
|
let relationships;
|
|
2679
2908
|
let paths;
|
|
2680
|
-
|
|
2909
|
+
const aggregations = raw.aggregations;
|
|
2910
|
+
const projectionEmitted = aggregations !== void 0;
|
|
2911
|
+
const rangeRowCount = spec.returnMode === "all" ? raw.allEntities.length + raw.allRelationships.length : 0;
|
|
2912
|
+
if (projectionEmitted) {
|
|
2913
|
+
} else if (spec.returnMode === "terminal") {
|
|
2681
2914
|
entities = raw.terminalEntities.map(projectStoredEntity);
|
|
2682
2915
|
relationships = void 0;
|
|
2683
2916
|
} else if (spec.returnMode === "all") {
|
|
2917
|
+
const missingIds = /* @__PURE__ */ new Set();
|
|
2918
|
+
for (const rel of raw.allRelationships) {
|
|
2919
|
+
if (!raw.entityMap.has(rel.sourceEntityId)) {
|
|
2920
|
+
missingIds.add(rel.sourceEntityId);
|
|
2921
|
+
}
|
|
2922
|
+
if (!raw.entityMap.has(rel.targetEntityId)) {
|
|
2923
|
+
missingIds.add(rel.targetEntityId);
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
if (missingIds.size > 0) {
|
|
2927
|
+
const fetched = await this.getEntities(repositoryId, [...missingIds]);
|
|
2928
|
+
for (const stored of fetched.values()) {
|
|
2929
|
+
raw.allEntities.push(stored);
|
|
2930
|
+
raw.entityMap.set(stored.id, stored);
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2684
2933
|
entities = raw.allEntities.map(projectStoredEntity);
|
|
2685
2934
|
relationships = raw.allRelationships.map((r) => projectStoredRelationship(r));
|
|
2686
2935
|
} else {
|
|
@@ -2689,7 +2938,7 @@ Verify the ARM/Bicep template that provisioned the container.`
|
|
|
2689
2938
|
entities: row.entityIds.map((id) => {
|
|
2690
2939
|
const stored = raw.entityMap.get(id);
|
|
2691
2940
|
if (!stored) {
|
|
2692
|
-
throw new
|
|
2941
|
+
throw new import_deep_memory6.ProviderError(
|
|
2693
2942
|
"Unpacking Gremlin path: entity referenced by path is missing from the result",
|
|
2694
2943
|
"This indicates a Gremlin response shape mismatch \u2014 inspect compiledQuery."
|
|
2695
2944
|
);
|
|
@@ -2699,7 +2948,7 @@ Verify the ARM/Bicep template that provisioned the container.`
|
|
|
2699
2948
|
relationships: row.relationshipIds.map((id, i) => {
|
|
2700
2949
|
const stored = raw.relationshipMap.get(id);
|
|
2701
2950
|
if (!stored) {
|
|
2702
|
-
throw new
|
|
2951
|
+
throw new import_deep_memory6.ProviderError(
|
|
2703
2952
|
"Unpacking Gremlin path: relationship referenced by path is missing from the result",
|
|
2704
2953
|
"This indicates a Gremlin response shape mismatch \u2014 inspect compiledQuery."
|
|
2705
2954
|
);
|
|
@@ -2708,18 +2957,22 @@ Verify the ARM/Bicep template that provisioned the container.`
|
|
|
2708
2957
|
})
|
|
2709
2958
|
}));
|
|
2710
2959
|
relationships = Array.from(raw.relationshipMap.values()).map(
|
|
2711
|
-
(rel) => projectStoredRelationship(rel, raw.pathRelFirstDirection.get(rel.id) ?? "
|
|
2960
|
+
(rel) => projectStoredRelationship(rel, raw.pathRelFirstDirection.get(rel.id) ?? "out")
|
|
2712
2961
|
);
|
|
2713
2962
|
}
|
|
2714
2963
|
const limit = spec.limit ?? 50;
|
|
2715
2964
|
let total;
|
|
2716
|
-
if (
|
|
2965
|
+
if (projectionEmitted) {
|
|
2966
|
+
total = aggregations.length;
|
|
2967
|
+
} else if (spec.returnMode === "path") {
|
|
2717
2968
|
total = paths?.length ?? 0;
|
|
2718
2969
|
} else if (spec.returnMode === "all") {
|
|
2719
2970
|
total = entities.length + (relationships?.length ?? 0);
|
|
2720
2971
|
} else {
|
|
2721
2972
|
total = entities.length;
|
|
2722
2973
|
}
|
|
2974
|
+
const paginationSignal = spec.returnMode === "all" ? rangeRowCount : total;
|
|
2975
|
+
const truncated = paginationSignal >= limit;
|
|
2723
2976
|
const queryMetadata = {
|
|
2724
2977
|
executionTimeMs: raw.executionTimeMs,
|
|
2725
2978
|
resourceCost: raw.requestCharge != null ? { units: "RU", value: raw.requestCharge } : void 0,
|
|
@@ -2729,16 +2982,17 @@ Verify the ARM/Bicep template that provisioned the container.`
|
|
|
2729
2982
|
maxResults: limit,
|
|
2730
2983
|
maxDepth: spec.steps?.length
|
|
2731
2984
|
},
|
|
2732
|
-
truncated
|
|
2733
|
-
truncationReason:
|
|
2985
|
+
truncated,
|
|
2986
|
+
truncationReason: truncated ? "result_limit" : void 0
|
|
2734
2987
|
};
|
|
2735
2988
|
return {
|
|
2736
2989
|
entities,
|
|
2737
2990
|
relationships,
|
|
2738
2991
|
paths,
|
|
2992
|
+
...aggregations !== void 0 ? { aggregations } : {},
|
|
2739
2993
|
total,
|
|
2740
2994
|
returned: total,
|
|
2741
|
-
hasMore:
|
|
2995
|
+
hasMore: truncated,
|
|
2742
2996
|
queryMetadata
|
|
2743
2997
|
};
|
|
2744
2998
|
}
|
|
@@ -2772,7 +3026,7 @@ Verify the ARM/Bicep template that provisioned the container.`
|
|
|
2772
3026
|
try {
|
|
2773
3027
|
result = await this.conn.submit(scopedQuery, scopedParams);
|
|
2774
3028
|
} catch (err) {
|
|
2775
|
-
throw new
|
|
3029
|
+
throw new import_deep_memory6.ProviderError(
|
|
2776
3030
|
`Gremlin traversal failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2777
3031
|
"Check the traversal spec and ensure the CosmosDB connection is healthy."
|
|
2778
3032
|
);
|
|
@@ -2790,7 +3044,14 @@ Verify the ARM/Bicep template that provisioned the container.`
|
|
|
2790
3044
|
requestCharge: result.requestCharge,
|
|
2791
3045
|
compiledQuery: scopedQuery
|
|
2792
3046
|
};
|
|
2793
|
-
|
|
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") {
|
|
2794
3055
|
for (const item of result.items) {
|
|
2795
3056
|
const stored = entityFromGremlin(item);
|
|
2796
3057
|
raw.terminalEntities.push(stored);
|
|
@@ -2829,7 +3090,7 @@ Verify the ARM/Bicep template that provisioned the container.`
|
|
|
2829
3090
|
const stored = relationshipFromGremlin(props);
|
|
2830
3091
|
raw.relationshipMap.set(stored.id, stored);
|
|
2831
3092
|
pathRelIds.push(stored.id);
|
|
2832
|
-
const direction = lastVertexId === stored.sourceEntityId ? "
|
|
3093
|
+
const direction = lastVertexId === stored.sourceEntityId ? "out" : "in";
|
|
2833
3094
|
pathRelDirections.push(direction);
|
|
2834
3095
|
if (!raw.pathRelFirstDirection.has(stored.id)) {
|
|
2835
3096
|
raw.pathRelFirstDirection.set(stored.id, direction);
|
|
@@ -2878,6 +3139,81 @@ function unwrapGremlinValue(val) {
|
|
|
2878
3139
|
if (Array.isArray(val) && val.length > 0) return val[0];
|
|
2879
3140
|
return val;
|
|
2880
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
|
+
}
|
|
2881
3217
|
function buildExploreSteps(depth, options) {
|
|
2882
3218
|
const base = { direction: "both" };
|
|
2883
3219
|
if (options.relationshipTypes && options.relationshipTypes.length > 0) {
|