@utaba/deep-memory-storage-cosmosdb 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -12
- package/dist/index.cjs +374 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +350 -33
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
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 =
|
|
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
|
|
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
|
|
1360
|
+
const userProps = relationshipUserPropertyParams(relationship.properties ?? {});
|
|
1361
|
+
let query;
|
|
1362
|
+
if (userProps.length === 0) {
|
|
1363
|
+
query = RELATIONSHIP_CREATE_QUERY;
|
|
1364
|
+
} else {
|
|
1365
|
+
let suffix = "";
|
|
1366
|
+
for (let i = 0; i < userProps.length; i++) {
|
|
1367
|
+
const { key, value } = userProps[i];
|
|
1368
|
+
suffix += `.property('${key}', p_user_${i})`;
|
|
1369
|
+
bindings[`p_user_${i}`] = value;
|
|
1370
|
+
}
|
|
1371
|
+
query = `${RELATIONSHIP_CREATE_PREFIX}${suffix})`;
|
|
1372
|
+
}
|
|
1373
|
+
const result = await conn.submit(query, bindings);
|
|
1205
1374
|
if (result.items[0] === DUPLICATE_SENTINEL2) {
|
|
1206
1375
|
throw new DuplicateRelationshipError(relationship.id);
|
|
1207
1376
|
}
|
|
@@ -1754,8 +1923,12 @@ var ENTITY_LADDER_CHAIN = buildEntityPropertyLadder();
|
|
|
1754
1923
|
var RELATIONSHIP_LADDER_CHAIN = buildRelationshipPropertyLadder();
|
|
1755
1924
|
var INSERT_ENTITY_QUERY = `g.addV(vertexLabel).property('id', vid).property('repositoryId', rid)${ENTITY_LADDER_CHAIN}`;
|
|
1756
1925
|
var INSERT_RELATIONSHIP_QUERY = `g.V().has('repositoryId', rid).hasId(srcId).has('entityType').addE(edgeLabel).to(g.V().has('repositoryId', rid).hasId(tgtId).has('entityType')).property('id', relId).property('repositoryId', rid)${RELATIONSHIP_LADDER_CHAIN}`;
|
|
1757
|
-
var
|
|
1758
|
-
var
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2193
|
+
throw new ProviderError2(
|
|
1969
2194
|
`Failed to ensure CosmosDB schema: ${err instanceof Error ? err.message : String(err)}`,
|
|
1970
2195
|
"Verify the CosmosDB REST endpoint is accessible and the Gremlin endpoint is reachable."
|
|
1971
2196
|
);
|
|
@@ -2382,7 +2607,7 @@ Verify the ARM/Bicep template that provisioned the container.`
|
|
|
2382
2607
|
}
|
|
2383
2608
|
const layer = {};
|
|
2384
2609
|
const nextFrontier = /* @__PURE__ */ new Set();
|
|
2385
|
-
const
|
|
2610
|
+
const layerBucketSeen = /* @__PURE__ */ new Map();
|
|
2386
2611
|
for (const fv of frontier) {
|
|
2387
2612
|
const incident = edgesByVertex.get(fv) ?? [];
|
|
2388
2613
|
for (const rel of incident) {
|
|
@@ -2415,10 +2640,14 @@ Verify the ARM/Bicep template that provisioned the container.`
|
|
|
2415
2640
|
if (options.entityTypes && options.entityTypes.length > 0 && !options.entityTypes.includes(connectedEntity.entityType)) {
|
|
2416
2641
|
continue;
|
|
2417
2642
|
}
|
|
2418
|
-
const edgeKey = `${rel.id}|${fv}->${connectedId}`;
|
|
2419
|
-
if (layerEdgeSeen.has(edgeKey)) continue;
|
|
2420
|
-
layerEdgeSeen.add(edgeKey);
|
|
2421
2643
|
const relType = rel.relationshipType;
|
|
2644
|
+
let bucketSeen = layerBucketSeen.get(relType);
|
|
2645
|
+
if (!bucketSeen) {
|
|
2646
|
+
bucketSeen = /* @__PURE__ */ new Set();
|
|
2647
|
+
layerBucketSeen.set(relType, bucketSeen);
|
|
2648
|
+
}
|
|
2649
|
+
if (bucketSeen.has(connectedId)) continue;
|
|
2650
|
+
bucketSeen.add(connectedId);
|
|
2422
2651
|
if (!layer[relType]) {
|
|
2423
2652
|
layer[relType] = { total: 0, entities: [], relationships: [] };
|
|
2424
2653
|
}
|
|
@@ -2653,8 +2882,11 @@ Verify the ARM/Bicep template that provisioned the container.`
|
|
|
2653
2882
|
let entities = [];
|
|
2654
2883
|
let relationships;
|
|
2655
2884
|
let paths;
|
|
2885
|
+
const aggregations = raw.aggregations;
|
|
2886
|
+
const projectionEmitted = aggregations !== void 0;
|
|
2656
2887
|
const rangeRowCount = spec.returnMode === "all" ? raw.allEntities.length + raw.allRelationships.length : 0;
|
|
2657
|
-
if (
|
|
2888
|
+
if (projectionEmitted) {
|
|
2889
|
+
} else if (spec.returnMode === "terminal") {
|
|
2658
2890
|
entities = raw.terminalEntities.map(projectStoredEntity);
|
|
2659
2891
|
relationships = void 0;
|
|
2660
2892
|
} else if (spec.returnMode === "all") {
|
|
@@ -2682,7 +2914,7 @@ Verify the ARM/Bicep template that provisioned the container.`
|
|
|
2682
2914
|
entities: row.entityIds.map((id) => {
|
|
2683
2915
|
const stored = raw.entityMap.get(id);
|
|
2684
2916
|
if (!stored) {
|
|
2685
|
-
throw new
|
|
2917
|
+
throw new ProviderError2(
|
|
2686
2918
|
"Unpacking Gremlin path: entity referenced by path is missing from the result",
|
|
2687
2919
|
"This indicates a Gremlin response shape mismatch \u2014 inspect compiledQuery."
|
|
2688
2920
|
);
|
|
@@ -2692,7 +2924,7 @@ Verify the ARM/Bicep template that provisioned the container.`
|
|
|
2692
2924
|
relationships: row.relationshipIds.map((id, i) => {
|
|
2693
2925
|
const stored = raw.relationshipMap.get(id);
|
|
2694
2926
|
if (!stored) {
|
|
2695
|
-
throw new
|
|
2927
|
+
throw new ProviderError2(
|
|
2696
2928
|
"Unpacking Gremlin path: relationship referenced by path is missing from the result",
|
|
2697
2929
|
"This indicates a Gremlin response shape mismatch \u2014 inspect compiledQuery."
|
|
2698
2930
|
);
|
|
@@ -2706,7 +2938,9 @@ Verify the ARM/Bicep template that provisioned the container.`
|
|
|
2706
2938
|
}
|
|
2707
2939
|
const limit = spec.limit ?? 50;
|
|
2708
2940
|
let total;
|
|
2709
|
-
if (
|
|
2941
|
+
if (projectionEmitted) {
|
|
2942
|
+
total = aggregations.length;
|
|
2943
|
+
} else if (spec.returnMode === "path") {
|
|
2710
2944
|
total = paths?.length ?? 0;
|
|
2711
2945
|
} else if (spec.returnMode === "all") {
|
|
2712
2946
|
total = entities.length + (relationships?.length ?? 0);
|
|
@@ -2731,6 +2965,7 @@ Verify the ARM/Bicep template that provisioned the container.`
|
|
|
2731
2965
|
entities,
|
|
2732
2966
|
relationships,
|
|
2733
2967
|
paths,
|
|
2968
|
+
...aggregations !== void 0 ? { aggregations } : {},
|
|
2734
2969
|
total,
|
|
2735
2970
|
returned: total,
|
|
2736
2971
|
hasMore: truncated,
|
|
@@ -2767,7 +3002,7 @@ Verify the ARM/Bicep template that provisioned the container.`
|
|
|
2767
3002
|
try {
|
|
2768
3003
|
result = await this.conn.submit(scopedQuery, scopedParams);
|
|
2769
3004
|
} catch (err) {
|
|
2770
|
-
throw new
|
|
3005
|
+
throw new ProviderError2(
|
|
2771
3006
|
`Gremlin traversal failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2772
3007
|
"Check the traversal spec and ensure the CosmosDB connection is healthy."
|
|
2773
3008
|
);
|
|
@@ -2785,7 +3020,14 @@ Verify the ARM/Bicep template that provisioned the container.`
|
|
|
2785
3020
|
requestCharge: result.requestCharge,
|
|
2786
3021
|
compiledQuery: scopedQuery
|
|
2787
3022
|
};
|
|
2788
|
-
|
|
3023
|
+
const emitsProjection = spec.projection !== void 0 && spec.returnMode !== "all" && spec.returnMode !== "path";
|
|
3024
|
+
if (emitsProjection) {
|
|
3025
|
+
raw.aggregations = parseProjectionRows(
|
|
3026
|
+
result.items,
|
|
3027
|
+
spec.projection.properties,
|
|
3028
|
+
spec.projection.mode ?? "values"
|
|
3029
|
+
);
|
|
3030
|
+
} else if (spec.returnMode === "terminal") {
|
|
2789
3031
|
for (const item of result.items) {
|
|
2790
3032
|
const stored = entityFromGremlin(item);
|
|
2791
3033
|
raw.terminalEntities.push(stored);
|
|
@@ -2873,6 +3115,81 @@ function unwrapGremlinValue(val) {
|
|
|
2873
3115
|
if (Array.isArray(val) && val.length > 0) return val[0];
|
|
2874
3116
|
return val;
|
|
2875
3117
|
}
|
|
3118
|
+
function parseProjectionRows(items, properties, mode) {
|
|
3119
|
+
const singleProperty = properties.length === 1;
|
|
3120
|
+
const aggregations = [];
|
|
3121
|
+
if (mode === "count") {
|
|
3122
|
+
for (const item of items) {
|
|
3123
|
+
const entry = readEntry(item);
|
|
3124
|
+
if (entry === void 0) continue;
|
|
3125
|
+
const values = singleProperty ? { [properties[0]]: normaliseProjectionScalar(entry.key) } : readProjectedMap(entry.key, properties);
|
|
3126
|
+
aggregations.push({ values, count: coerceCount(entry.value) });
|
|
3127
|
+
}
|
|
3128
|
+
return aggregations;
|
|
3129
|
+
}
|
|
3130
|
+
for (const item of items) {
|
|
3131
|
+
const values = singleProperty ? { [properties[0]]: normaliseProjectionScalar(item) } : readProjectedMap(item, properties);
|
|
3132
|
+
aggregations.push({ values });
|
|
3133
|
+
}
|
|
3134
|
+
return aggregations;
|
|
3135
|
+
}
|
|
3136
|
+
function readEntry(item) {
|
|
3137
|
+
if (item instanceof Map) {
|
|
3138
|
+
if (item.has("key") && item.has("value")) {
|
|
3139
|
+
return { key: item.get("key"), value: item.get("value") };
|
|
3140
|
+
}
|
|
3141
|
+
const first = item.entries().next();
|
|
3142
|
+
if (!first.done) {
|
|
3143
|
+
const [key, value] = first.value;
|
|
3144
|
+
return { key, value };
|
|
3145
|
+
}
|
|
3146
|
+
return void 0;
|
|
3147
|
+
}
|
|
3148
|
+
if (typeof item !== "object" || item === null) return void 0;
|
|
3149
|
+
const obj = item;
|
|
3150
|
+
if ("key" in obj && "value" in obj) {
|
|
3151
|
+
return { key: obj["key"], value: obj["value"] };
|
|
3152
|
+
}
|
|
3153
|
+
const keys = Object.keys(obj);
|
|
3154
|
+
if (keys.length === 1) {
|
|
3155
|
+
const k = keys[0];
|
|
3156
|
+
return { key: k, value: obj[k] };
|
|
3157
|
+
}
|
|
3158
|
+
return void 0;
|
|
3159
|
+
}
|
|
3160
|
+
function readProjectedMap(row, properties) {
|
|
3161
|
+
const out = {};
|
|
3162
|
+
if (row instanceof Map) {
|
|
3163
|
+
for (const prop of properties) {
|
|
3164
|
+
out[prop] = normaliseProjectionScalar(row.get(prop));
|
|
3165
|
+
}
|
|
3166
|
+
return out;
|
|
3167
|
+
}
|
|
3168
|
+
if (typeof row === "object" && row !== null) {
|
|
3169
|
+
const obj = row;
|
|
3170
|
+
for (const prop of properties) {
|
|
3171
|
+
out[prop] = normaliseProjectionScalar(obj[prop]);
|
|
3172
|
+
}
|
|
3173
|
+
return out;
|
|
3174
|
+
}
|
|
3175
|
+
for (const prop of properties) {
|
|
3176
|
+
out[prop] = void 0;
|
|
3177
|
+
}
|
|
3178
|
+
return out;
|
|
3179
|
+
}
|
|
3180
|
+
function normaliseProjectionScalar(value) {
|
|
3181
|
+
if (typeof value === "bigint") return Number(value);
|
|
3182
|
+
return value;
|
|
3183
|
+
}
|
|
3184
|
+
function coerceCount(value) {
|
|
3185
|
+
if (typeof value === "number") return value;
|
|
3186
|
+
if (typeof value === "bigint") return Number(value);
|
|
3187
|
+
if (typeof value === "string") {
|
|
3188
|
+
const n = Number(value);
|
|
3189
|
+
return Number.isFinite(n) ? n : 0;
|
|
3190
|
+
}
|
|
3191
|
+
return 0;
|
|
3192
|
+
}
|
|
2876
3193
|
function buildExploreSteps(depth, options) {
|
|
2877
3194
|
const base = { direction: "both" };
|
|
2878
3195
|
if (options.relationshipTypes && options.relationshipTypes.length > 0) {
|