@utaba/deep-memory-storage-cosmosdb 0.16.0 → 0.17.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/LICENSE +15 -4
- package/README.md +262 -0
- package/dist/index.cjs +1269 -671
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +136 -7
- package/dist/index.d.ts +136 -7
- package/dist/index.js +1256 -645
- package/dist/index.js.map +1 -1
- package/package.json +4 -5
package/dist/index.cjs
CHANGED
|
@@ -36,13 +36,16 @@ __export(index_exports, {
|
|
|
36
36
|
module.exports = __toCommonJS(index_exports);
|
|
37
37
|
|
|
38
38
|
// src/CosmosDbProvider.ts
|
|
39
|
-
var
|
|
40
|
-
var import_deep_memory6 = require("@utaba/deep-memory");
|
|
39
|
+
var import_deep_memory5 = require("@utaba/deep-memory");
|
|
41
40
|
|
|
42
41
|
// src/CosmosDbConnection.ts
|
|
43
42
|
var import_gremlin = __toESM(require("gremlin"), 1);
|
|
43
|
+
|
|
44
|
+
// src/usage.ts
|
|
44
45
|
var import_node_async_hooks = require("async_hooks");
|
|
45
46
|
var usageScope = new import_node_async_hooks.AsyncLocalStorage();
|
|
47
|
+
|
|
48
|
+
// src/CosmosDbConnection.ts
|
|
46
49
|
var CosmosDbConnection = class {
|
|
47
50
|
client = null;
|
|
48
51
|
config;
|
|
@@ -138,14 +141,220 @@ function getRetryAfterMs(err, attempt) {
|
|
|
138
141
|
function extractRequestCharge(resultSet) {
|
|
139
142
|
const attrs = resultSet.attributes;
|
|
140
143
|
if (!attrs) return void 0;
|
|
141
|
-
const
|
|
142
|
-
|
|
144
|
+
const total = attrs["x-ms-total-request-charge"];
|
|
145
|
+
if (typeof total === "number") return total;
|
|
146
|
+
const single = attrs["x-ms-request-charge"];
|
|
147
|
+
if (typeof single === "number") return single;
|
|
148
|
+
return void 0;
|
|
143
149
|
}
|
|
144
150
|
function sleep(ms) {
|
|
145
151
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
146
152
|
}
|
|
147
153
|
|
|
154
|
+
// src/cosmos-rest-auth.ts
|
|
155
|
+
var import_node_crypto = __toESM(require("crypto"), 1);
|
|
156
|
+
function cosmosAuthToken(verb, resourceType, resourceLink, date, key) {
|
|
157
|
+
const payload = `${verb.toLowerCase()}
|
|
158
|
+
${resourceType.toLowerCase()}
|
|
159
|
+
${resourceLink}
|
|
160
|
+
${date.toLowerCase()}
|
|
161
|
+
|
|
162
|
+
`;
|
|
163
|
+
const keyBuffer = Buffer.from(key, "base64");
|
|
164
|
+
const hmac = import_node_crypto.default.createHmac("sha256", keyBuffer);
|
|
165
|
+
hmac.update(payload);
|
|
166
|
+
const signature = hmac.digest("base64");
|
|
167
|
+
return encodeURIComponent(`type=master&ver=1.0&sig=${signature}`);
|
|
168
|
+
}
|
|
169
|
+
async function cosmosRestPut(restBase, key, urlPath, resourceLink, resourceType, body, rejectUnauthorized) {
|
|
170
|
+
const date = (/* @__PURE__ */ new Date()).toUTCString();
|
|
171
|
+
const token = cosmosAuthToken("post", resourceType, resourceLink, date, key);
|
|
172
|
+
const url = `${restBase}/${urlPath}`;
|
|
173
|
+
const options = {
|
|
174
|
+
method: "POST",
|
|
175
|
+
headers: {
|
|
176
|
+
"Authorization": token,
|
|
177
|
+
"x-ms-version": "2018-12-31",
|
|
178
|
+
"x-ms-date": date,
|
|
179
|
+
"Content-Type": "application/json"
|
|
180
|
+
},
|
|
181
|
+
body: JSON.stringify(body)
|
|
182
|
+
};
|
|
183
|
+
if (!rejectUnauthorized) {
|
|
184
|
+
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
|
|
185
|
+
}
|
|
186
|
+
const response = await fetch(url, options);
|
|
187
|
+
if (response.status === 201) return true;
|
|
188
|
+
if (response.status === 409) return false;
|
|
189
|
+
const text = await response.text();
|
|
190
|
+
throw new Error(`CosmosDB REST ${response.status}: ${text}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/CosmosDocumentClient.ts
|
|
194
|
+
var CosmosDocumentClient = class {
|
|
195
|
+
config;
|
|
196
|
+
fetchImpl;
|
|
197
|
+
/**
|
|
198
|
+
* `fetchImpl` is for tests only — production code should pass nothing and
|
|
199
|
+
* inherit `globalThis.fetch`. Keeping it on the constructor (rather than
|
|
200
|
+
* stubbing globals) means each test instance is hermetic.
|
|
201
|
+
*/
|
|
202
|
+
constructor(config, fetchImpl) {
|
|
203
|
+
this.config = {
|
|
204
|
+
...config,
|
|
205
|
+
maxRetries: config.maxRetries ?? 3,
|
|
206
|
+
defaultTimeoutMs: config.defaultTimeoutMs ?? 3e4
|
|
207
|
+
};
|
|
208
|
+
this.fetchImpl = fetchImpl ?? fetch;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Execute a parameterised Cosmos SQL query against the container's `docs`
|
|
212
|
+
* resource. Retries on 429/503 with the response's `x-ms-retry-after-ms`
|
|
213
|
+
* when present, otherwise exponential backoff. RU is accumulated into the
|
|
214
|
+
* active {@link usageScope}.
|
|
215
|
+
*/
|
|
216
|
+
async query(sql, parameters, options) {
|
|
217
|
+
const resourceLink = `dbs/${this.config.database}/colls/${this.config.container}`;
|
|
218
|
+
const url = `${this.restBase()}/${resourceLink}/docs`;
|
|
219
|
+
if (!this.config.rejectUnauthorized) {
|
|
220
|
+
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
|
|
221
|
+
}
|
|
222
|
+
const body = JSON.stringify({ query: sql, parameters });
|
|
223
|
+
for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
|
|
224
|
+
const date = (/* @__PURE__ */ new Date()).toUTCString();
|
|
225
|
+
const token = cosmosAuthToken("post", "docs", resourceLink, date, this.config.key);
|
|
226
|
+
const headers = {
|
|
227
|
+
"Authorization": token,
|
|
228
|
+
"x-ms-version": "2018-12-31",
|
|
229
|
+
"x-ms-date": date,
|
|
230
|
+
"Content-Type": "application/query+json",
|
|
231
|
+
"x-ms-documentdb-isquery": "true"
|
|
232
|
+
};
|
|
233
|
+
if (options.partitionKey != null) {
|
|
234
|
+
headers["x-ms-documentdb-partitionkey"] = JSON.stringify([options.partitionKey]);
|
|
235
|
+
} else {
|
|
236
|
+
headers["x-ms-documentdb-query-enablecrosspartition"] = "true";
|
|
237
|
+
}
|
|
238
|
+
if (options.populateMetrics) {
|
|
239
|
+
headers["x-ms-documentdb-populatequerymetrics"] = "true";
|
|
240
|
+
}
|
|
241
|
+
if (options.continuationToken) {
|
|
242
|
+
headers["x-ms-continuation"] = options.continuationToken;
|
|
243
|
+
}
|
|
244
|
+
const response = await this.fetchImpl(url, { method: "POST", headers, body });
|
|
245
|
+
if (response.status === 429 || response.status === 503) {
|
|
246
|
+
if (attempt < this.config.maxRetries) {
|
|
247
|
+
const waitMs = parseRetryAfterMs(response, attempt);
|
|
248
|
+
const acc2 = usageScope.getStore();
|
|
249
|
+
if (acc2) acc2.retries++;
|
|
250
|
+
await sleep2(waitMs);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const text = await response.text();
|
|
254
|
+
throw new Error(`CosmosDB Document query ${response.status}: ${text}`);
|
|
255
|
+
}
|
|
256
|
+
if (!response.ok) {
|
|
257
|
+
const text = await response.text();
|
|
258
|
+
throw new Error(`CosmosDB Document query ${response.status}: ${text}`);
|
|
259
|
+
}
|
|
260
|
+
const json = await response.json();
|
|
261
|
+
const requestCharge = Number(response.headers.get("x-ms-request-charge") ?? "0") || 0;
|
|
262
|
+
const queryMetrics = response.headers.get("x-ms-documentdb-query-metrics");
|
|
263
|
+
const continuationToken = response.headers.get("x-ms-continuation");
|
|
264
|
+
const acc = usageScope.getStore();
|
|
265
|
+
if (acc) {
|
|
266
|
+
acc.calls++;
|
|
267
|
+
acc.ru += requestCharge;
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
documents: json.Documents ?? [],
|
|
271
|
+
requestCharge,
|
|
272
|
+
queryMetrics,
|
|
273
|
+
continuationToken
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
throw new Error("CosmosDocumentClient.query: retry loop exhausted without resolution");
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Read container metadata (id, partition key, indexing policy). Used by
|
|
280
|
+
* `ensureSchema()` to warn when externally-provisioned containers have
|
|
281
|
+
* excluded paths the findEntities SQL needs.
|
|
282
|
+
*/
|
|
283
|
+
async getContainerProperties() {
|
|
284
|
+
const resourceLink = `dbs/${this.config.database}/colls/${this.config.container}`;
|
|
285
|
+
const url = `${this.restBase()}/${resourceLink}`;
|
|
286
|
+
if (!this.config.rejectUnauthorized) {
|
|
287
|
+
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
|
|
288
|
+
}
|
|
289
|
+
const date = (/* @__PURE__ */ new Date()).toUTCString();
|
|
290
|
+
const token = cosmosAuthToken("get", "colls", resourceLink, date, this.config.key);
|
|
291
|
+
const response = await this.fetchImpl(url, {
|
|
292
|
+
method: "GET",
|
|
293
|
+
headers: {
|
|
294
|
+
"Authorization": token,
|
|
295
|
+
"x-ms-version": "2018-12-31",
|
|
296
|
+
"x-ms-date": date
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
if (!response.ok) {
|
|
300
|
+
const text = await response.text();
|
|
301
|
+
throw new Error(`CosmosDB Document getContainerProperties ${response.status}: ${text}`);
|
|
302
|
+
}
|
|
303
|
+
return await response.json();
|
|
304
|
+
}
|
|
305
|
+
restBase() {
|
|
306
|
+
return this.config.restEndpoint.replace(/\/+$/, "");
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
function parseRetryAfterMs(response, attempt) {
|
|
310
|
+
const header = response.headers.get("x-ms-retry-after-ms");
|
|
311
|
+
if (header) {
|
|
312
|
+
const n = Number(header);
|
|
313
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
314
|
+
}
|
|
315
|
+
return Math.min(500 * Math.pow(2, attempt), 1e4);
|
|
316
|
+
}
|
|
317
|
+
function sleep2(ms) {
|
|
318
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
319
|
+
}
|
|
320
|
+
|
|
148
321
|
// src/mapping.ts
|
|
322
|
+
var STORED_ENTITY_FIELDS = [
|
|
323
|
+
"id",
|
|
324
|
+
"entityType",
|
|
325
|
+
"entityLabel",
|
|
326
|
+
"slug",
|
|
327
|
+
"summary",
|
|
328
|
+
"properties",
|
|
329
|
+
"data",
|
|
330
|
+
"dataFormat",
|
|
331
|
+
"createdBy",
|
|
332
|
+
"createdByType",
|
|
333
|
+
"createdAt",
|
|
334
|
+
"createdInConversation",
|
|
335
|
+
"createdFromMessage",
|
|
336
|
+
"modifiedBy",
|
|
337
|
+
"modifiedByType",
|
|
338
|
+
"modifiedAt",
|
|
339
|
+
"modifiedInConversation",
|
|
340
|
+
"modifiedFromMessage"
|
|
341
|
+
];
|
|
342
|
+
function buildRepositoryProjectChain() {
|
|
343
|
+
return [
|
|
344
|
+
`project('id','repositoryId','repoLabel','description','type','legal','owner','governanceConfig','metadata','createdAt','createdBy')`,
|
|
345
|
+
`.by(id)`,
|
|
346
|
+
`.by('repositoryId')`,
|
|
347
|
+
`.by('repoLabel')`,
|
|
348
|
+
`.by(coalesce(values('description'), constant('')))`,
|
|
349
|
+
`.by(coalesce(values('type'), constant('')))`,
|
|
350
|
+
`.by(coalesce(values('legal'), constant('')))`,
|
|
351
|
+
`.by(coalesce(values('owner'), constant('')))`,
|
|
352
|
+
`.by('governanceConfig')`,
|
|
353
|
+
`.by(coalesce(values('metadata'), constant('')))`,
|
|
354
|
+
`.by('createdAt')`,
|
|
355
|
+
`.by('createdBy')`
|
|
356
|
+
].join("");
|
|
357
|
+
}
|
|
149
358
|
function unwrap(val) {
|
|
150
359
|
if (Array.isArray(val) && val.length > 0) return val[0];
|
|
151
360
|
return val;
|
|
@@ -197,6 +406,54 @@ function entityFromGremlin(props) {
|
|
|
197
406
|
embedding: embeddingStr ? safeParseJson(embeddingStr, void 0) : void 0
|
|
198
407
|
};
|
|
199
408
|
}
|
|
409
|
+
function pluckDocValue(doc, key) {
|
|
410
|
+
const arr = doc[key];
|
|
411
|
+
if (Array.isArray(arr) && arr.length > 0) {
|
|
412
|
+
const entry = arr[0];
|
|
413
|
+
if (entry && typeof entry === "object") {
|
|
414
|
+
return entry["_value"];
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return void 0;
|
|
418
|
+
}
|
|
419
|
+
function pluckDocStr(doc, key) {
|
|
420
|
+
const v = pluckDocValue(doc, key);
|
|
421
|
+
return typeof v === "string" ? v : String(v ?? "");
|
|
422
|
+
}
|
|
423
|
+
function pluckDocOptStr(doc, key) {
|
|
424
|
+
const v = pluckDocValue(doc, key);
|
|
425
|
+
return v != null && v !== "" ? String(v) : void 0;
|
|
426
|
+
}
|
|
427
|
+
function provenanceFromDocument(doc) {
|
|
428
|
+
return {
|
|
429
|
+
createdBy: pluckDocStr(doc, "createdBy"),
|
|
430
|
+
createdByType: pluckDocStr(doc, "createdByType") || "agent",
|
|
431
|
+
createdAt: pluckDocStr(doc, "createdAt"),
|
|
432
|
+
createdInConversation: pluckDocOptStr(doc, "createdInConversation"),
|
|
433
|
+
createdFromMessage: pluckDocOptStr(doc, "createdFromMessage"),
|
|
434
|
+
modifiedBy: pluckDocStr(doc, "modifiedBy"),
|
|
435
|
+
modifiedByType: pluckDocStr(doc, "modifiedByType") || "agent",
|
|
436
|
+
modifiedAt: pluckDocStr(doc, "modifiedAt"),
|
|
437
|
+
modifiedInConversation: pluckDocOptStr(doc, "modifiedInConversation"),
|
|
438
|
+
modifiedFromMessage: pluckDocOptStr(doc, "modifiedFromMessage")
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function entityFromDocument(doc) {
|
|
442
|
+
const id = typeof doc["id"] === "string" ? doc["id"] : String(doc["id"] ?? "");
|
|
443
|
+
const embeddingStr = pluckDocOptStr(doc, "embedding");
|
|
444
|
+
return {
|
|
445
|
+
id,
|
|
446
|
+
slug: pluckDocStr(doc, "slug"),
|
|
447
|
+
entityType: pluckDocStr(doc, "entityType"),
|
|
448
|
+
label: pluckDocStr(doc, "entityLabel"),
|
|
449
|
+
summary: pluckDocOptStr(doc, "summary"),
|
|
450
|
+
properties: safeParseJson(pluckDocValue(doc, "properties"), {}),
|
|
451
|
+
data: pluckDocOptStr(doc, "data"),
|
|
452
|
+
dataFormat: pluckDocOptStr(doc, "dataFormat"),
|
|
453
|
+
provenance: provenanceFromDocument(doc),
|
|
454
|
+
embedding: embeddingStr ? safeParseJson(embeddingStr, void 0) : void 0
|
|
455
|
+
};
|
|
456
|
+
}
|
|
200
457
|
function relationshipFromGremlin(props) {
|
|
201
458
|
const bidir = unwrap(props["bidirectional"]);
|
|
202
459
|
return {
|
|
@@ -223,24 +480,6 @@ function repositoryFromGremlin(props) {
|
|
|
223
480
|
createdBy: unwrapStr(props["createdBy"])
|
|
224
481
|
};
|
|
225
482
|
}
|
|
226
|
-
function repositorySummaryFromGremlin(props) {
|
|
227
|
-
return {
|
|
228
|
-
repositoryId: unwrapStr(props["repositoryId"]),
|
|
229
|
-
type: unwrapOptStr(props["type"]),
|
|
230
|
-
label: unwrapStr(props["repoLabel"]),
|
|
231
|
-
description: unwrapOptStr(props["description"]),
|
|
232
|
-
governanceConfig: safeParseJson(unwrap(props["governanceConfig"]), { mode: "open" })
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
function vocabularyFromGremlin(props) {
|
|
236
|
-
return safeParseJson(unwrap(props["vocabulary"]), {
|
|
237
|
-
version: "0.0.0",
|
|
238
|
-
lastModified: (/* @__PURE__ */ new Date()).toISOString(),
|
|
239
|
-
modifiedBy: "system",
|
|
240
|
-
entityTypes: [],
|
|
241
|
-
relationshipTypes: []
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
483
|
function changeRecordFromGremlin(props) {
|
|
245
484
|
return {
|
|
246
485
|
changeId: unwrapStr(props["changeId"]),
|
|
@@ -255,9 +494,97 @@ function changeRecordFromGremlin(props) {
|
|
|
255
494
|
reason: unwrapStr(props["reason"])
|
|
256
495
|
};
|
|
257
496
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
497
|
+
var ABSENT_STRING_SENTINEL = "";
|
|
498
|
+
var SENTINEL_BINDING = "absentSentinel";
|
|
499
|
+
var ENTITY_REQUIRED_SLOTS = [
|
|
500
|
+
"entityType",
|
|
501
|
+
"entityLabel",
|
|
502
|
+
"slug",
|
|
503
|
+
"properties",
|
|
504
|
+
"createdBy",
|
|
505
|
+
"createdByType",
|
|
506
|
+
"createdAt",
|
|
507
|
+
"modifiedBy",
|
|
508
|
+
"modifiedByType",
|
|
509
|
+
"modifiedAt"
|
|
510
|
+
];
|
|
511
|
+
var ENTITY_OPTIONAL_SLOTS = [
|
|
512
|
+
"summary",
|
|
513
|
+
"data",
|
|
514
|
+
"dataFormat",
|
|
515
|
+
"embedding",
|
|
516
|
+
"createdInConversation",
|
|
517
|
+
"createdFromMessage",
|
|
518
|
+
"modifiedInConversation",
|
|
519
|
+
"modifiedFromMessage"
|
|
520
|
+
];
|
|
521
|
+
var RELATIONSHIP_REQUIRED_SLOTS = [
|
|
522
|
+
"relationshipType",
|
|
523
|
+
"sourceEntityId",
|
|
524
|
+
"targetEntityId",
|
|
525
|
+
"bidirectional",
|
|
526
|
+
"properties",
|
|
527
|
+
"createdBy",
|
|
528
|
+
"createdByType",
|
|
529
|
+
"createdAt",
|
|
530
|
+
"modifiedBy",
|
|
531
|
+
"modifiedByType",
|
|
532
|
+
"modifiedAt"
|
|
533
|
+
];
|
|
534
|
+
var RELATIONSHIP_OPTIONAL_SLOTS = [
|
|
535
|
+
"createdInConversation",
|
|
536
|
+
"createdFromMessage",
|
|
537
|
+
"modifiedInConversation",
|
|
538
|
+
"modifiedFromMessage"
|
|
539
|
+
];
|
|
540
|
+
var REPOSITORY_REQUIRED_SLOTS = [
|
|
541
|
+
"repoLabel",
|
|
542
|
+
"governanceConfig",
|
|
543
|
+
"createdAt",
|
|
544
|
+
"createdBy"
|
|
545
|
+
];
|
|
546
|
+
var REPOSITORY_OPTIONAL_SLOTS = [
|
|
547
|
+
"description",
|
|
548
|
+
"type",
|
|
549
|
+
"legal",
|
|
550
|
+
"owner",
|
|
551
|
+
"metadata"
|
|
552
|
+
];
|
|
553
|
+
function buildLadder(required, optional, paramPrefix) {
|
|
554
|
+
const parts = [];
|
|
555
|
+
let i = 0;
|
|
556
|
+
for (const slot of required) {
|
|
557
|
+
parts.push(`.property('${slot}', ${paramPrefix}${i++})`);
|
|
558
|
+
}
|
|
559
|
+
for (const slot of optional) {
|
|
560
|
+
parts.push(
|
|
561
|
+
`.choose(__.constant(${paramPrefix}${i}).is(neq(${SENTINEL_BINDING})), __.property('${slot}', ${paramPrefix}${i}), __.identity())`
|
|
562
|
+
);
|
|
563
|
+
i++;
|
|
564
|
+
}
|
|
565
|
+
return parts.join("");
|
|
566
|
+
}
|
|
567
|
+
function buildLadderBindings(required, optional, paramPrefix, values) {
|
|
568
|
+
const bindings = {};
|
|
569
|
+
let i = 0;
|
|
570
|
+
for (const slot of required) {
|
|
571
|
+
const v = values[slot];
|
|
572
|
+
if (v == null) {
|
|
573
|
+
throw new Error(`Fixed-shape ladder: required slot '${slot}' is null/undefined`);
|
|
574
|
+
}
|
|
575
|
+
bindings[`${paramPrefix}${i++}`] = v;
|
|
576
|
+
}
|
|
577
|
+
for (const slot of optional) {
|
|
578
|
+
const v = values[slot];
|
|
579
|
+
bindings[`${paramPrefix}${i++}`] = v ?? ABSENT_STRING_SENTINEL;
|
|
580
|
+
}
|
|
581
|
+
return bindings;
|
|
582
|
+
}
|
|
583
|
+
function buildEntityPropertyLadder() {
|
|
584
|
+
return buildLadder(ENTITY_REQUIRED_SLOTS, ENTITY_OPTIONAL_SLOTS, "p");
|
|
585
|
+
}
|
|
586
|
+
function entityToLadderBindings(entity) {
|
|
587
|
+
const bindings = buildLadderBindings(ENTITY_REQUIRED_SLOTS, ENTITY_OPTIONAL_SLOTS, "p", {
|
|
261
588
|
entityType: entity.entityType,
|
|
262
589
|
entityLabel: entity.label,
|
|
263
590
|
slug: entity.slug,
|
|
@@ -267,46 +594,120 @@ function entityToGremlinProps(repositoryId, entity) {
|
|
|
267
594
|
createdAt: entity.provenance.createdAt,
|
|
268
595
|
modifiedBy: entity.provenance.modifiedBy,
|
|
269
596
|
modifiedByType: entity.provenance.modifiedByType,
|
|
270
|
-
modifiedAt: entity.provenance.modifiedAt
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
597
|
+
modifiedAt: entity.provenance.modifiedAt,
|
|
598
|
+
summary: entity.summary,
|
|
599
|
+
data: entity.data,
|
|
600
|
+
dataFormat: entity.dataFormat,
|
|
601
|
+
embedding: entity.embedding != null ? JSON.stringify(entity.embedding) : void 0,
|
|
602
|
+
createdInConversation: entity.provenance.createdInConversation,
|
|
603
|
+
createdFromMessage: entity.provenance.createdFromMessage,
|
|
604
|
+
modifiedInConversation: entity.provenance.modifiedInConversation,
|
|
605
|
+
modifiedFromMessage: entity.provenance.modifiedFromMessage
|
|
606
|
+
});
|
|
607
|
+
bindings[SENTINEL_BINDING] = ABSENT_STRING_SENTINEL;
|
|
608
|
+
return bindings;
|
|
609
|
+
}
|
|
610
|
+
function buildRelationshipPropertyLadder() {
|
|
611
|
+
return buildLadder(RELATIONSHIP_REQUIRED_SLOTS, RELATIONSHIP_OPTIONAL_SLOTS, "p");
|
|
612
|
+
}
|
|
613
|
+
function relationshipToLadderBindings(rel) {
|
|
614
|
+
const bindings = buildLadderBindings(
|
|
615
|
+
RELATIONSHIP_REQUIRED_SLOTS,
|
|
616
|
+
RELATIONSHIP_OPTIONAL_SLOTS,
|
|
617
|
+
"p",
|
|
618
|
+
{
|
|
619
|
+
relationshipType: rel.relationshipType,
|
|
620
|
+
sourceEntityId: rel.sourceEntityId,
|
|
621
|
+
targetEntityId: rel.targetEntityId,
|
|
622
|
+
bidirectional: rel.bidirectional,
|
|
623
|
+
properties: JSON.stringify(rel.properties ?? {}),
|
|
624
|
+
createdBy: rel.provenance.createdBy,
|
|
625
|
+
createdByType: rel.provenance.createdByType,
|
|
626
|
+
createdAt: rel.provenance.createdAt,
|
|
627
|
+
modifiedBy: rel.provenance.modifiedBy,
|
|
628
|
+
modifiedByType: rel.provenance.modifiedByType,
|
|
629
|
+
modifiedAt: rel.provenance.modifiedAt,
|
|
630
|
+
createdInConversation: rel.provenance.createdInConversation,
|
|
631
|
+
createdFromMessage: rel.provenance.createdFromMessage,
|
|
632
|
+
modifiedInConversation: rel.provenance.modifiedInConversation,
|
|
633
|
+
modifiedFromMessage: rel.provenance.modifiedFromMessage
|
|
634
|
+
}
|
|
635
|
+
);
|
|
636
|
+
bindings[SENTINEL_BINDING] = ABSENT_STRING_SENTINEL;
|
|
637
|
+
return bindings;
|
|
638
|
+
}
|
|
639
|
+
function buildRepositoryPropertyLadder() {
|
|
640
|
+
return buildLadder(REPOSITORY_REQUIRED_SLOTS, REPOSITORY_OPTIONAL_SLOTS, "p");
|
|
641
|
+
}
|
|
642
|
+
function repositoryConfigToLadderBindings(config) {
|
|
643
|
+
const bindings = buildLadderBindings(
|
|
644
|
+
REPOSITORY_REQUIRED_SLOTS,
|
|
645
|
+
REPOSITORY_OPTIONAL_SLOTS,
|
|
646
|
+
"p",
|
|
647
|
+
{
|
|
648
|
+
repoLabel: config.label,
|
|
649
|
+
governanceConfig: JSON.stringify(config.governanceConfig),
|
|
650
|
+
createdAt: config.createdAt,
|
|
651
|
+
createdBy: config.createdBy,
|
|
652
|
+
description: config.description,
|
|
653
|
+
type: config.type,
|
|
654
|
+
legal: config.legal,
|
|
655
|
+
owner: config.owner,
|
|
656
|
+
metadata: config.metadata != null ? JSON.stringify(config.metadata) : void 0
|
|
657
|
+
}
|
|
658
|
+
);
|
|
659
|
+
bindings[SENTINEL_BINDING] = ABSENT_STRING_SENTINEL;
|
|
660
|
+
return bindings;
|
|
302
661
|
}
|
|
303
662
|
|
|
304
663
|
// src/queries/repository.ts
|
|
305
664
|
var import_deep_memory = require("@utaba/deep-memory");
|
|
306
665
|
var REPO_LABEL = "_repository";
|
|
666
|
+
var REPOSITORY_INDEX_VERTEX_ID = "_repository_index";
|
|
667
|
+
var REPOSITORY_INDEX_PARTITION = "_index";
|
|
668
|
+
var REPOSITORY_INDEX_LABEL = "_repository_index";
|
|
307
669
|
function repoVertexId(repositoryId) {
|
|
308
670
|
return `repo:${repositoryId}`;
|
|
309
671
|
}
|
|
672
|
+
async function ensureRepositoryIndex(conn) {
|
|
673
|
+
const existing = await conn.submit(
|
|
674
|
+
"g.V().has('repositoryId', pk).hasId(sid).count()",
|
|
675
|
+
{ pk: REPOSITORY_INDEX_PARTITION, sid: REPOSITORY_INDEX_VERTEX_ID }
|
|
676
|
+
);
|
|
677
|
+
if (Number(existing.items[0] ?? 0) > 0) {
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
const scan = await conn.submit(
|
|
681
|
+
"g.V().hasLabel('_repository').values('repositoryId')",
|
|
682
|
+
{}
|
|
683
|
+
);
|
|
684
|
+
const ids = scan.items.map((item) => typeof item === "string" ? item : String(item ?? "")).filter((id) => id.length > 0);
|
|
685
|
+
await conn.submit(
|
|
686
|
+
"g.addV('" + REPOSITORY_INDEX_LABEL + "').property('id', sid).property('repositoryId', pk).property('repositoryIds', initial)",
|
|
687
|
+
{
|
|
688
|
+
pk: REPOSITORY_INDEX_PARTITION,
|
|
689
|
+
sid: REPOSITORY_INDEX_VERTEX_ID,
|
|
690
|
+
initial: JSON.stringify(ids)
|
|
691
|
+
}
|
|
692
|
+
);
|
|
693
|
+
return ids.length;
|
|
694
|
+
}
|
|
695
|
+
async function readRepositoryIndex(conn) {
|
|
696
|
+
const result = await conn.submit(
|
|
697
|
+
"g.V().has('repositoryId', pk).hasId(sid).values('repositoryIds')",
|
|
698
|
+
{ pk: REPOSITORY_INDEX_PARTITION, sid: REPOSITORY_INDEX_VERTEX_ID }
|
|
699
|
+
);
|
|
700
|
+
if (result.items.length === 0) return [];
|
|
701
|
+
const raw = result.items[0];
|
|
702
|
+
const json = typeof raw === "string" ? raw : String(raw ?? "");
|
|
703
|
+
if (!json) return [];
|
|
704
|
+
try {
|
|
705
|
+
const parsed = JSON.parse(json);
|
|
706
|
+
return Array.isArray(parsed) ? parsed.filter((id) => typeof id === "string") : [];
|
|
707
|
+
} catch {
|
|
708
|
+
return [];
|
|
709
|
+
}
|
|
710
|
+
}
|
|
310
711
|
function propertyChain(bindings, props, startIndex) {
|
|
311
712
|
const parts = [];
|
|
312
713
|
let idx = startIndex;
|
|
@@ -318,33 +719,27 @@ function propertyChain(bindings, props, startIndex) {
|
|
|
318
719
|
}
|
|
319
720
|
return { chain: parts.join(""), nextIndex: idx };
|
|
320
721
|
}
|
|
722
|
+
var REPOSITORY_CREATE_QUERY = `g.addV('${REPO_LABEL}').property('id', vid).property('repositoryId', rid)${buildRepositoryPropertyLadder()}.sideEffect(__.V().has('repositoryId', pk).hasId(sid).property('repositoryIds', updatedIndex))`;
|
|
321
723
|
async function createRepository(conn, config) {
|
|
322
724
|
const vertexId = repoVertexId(config.repositoryId);
|
|
323
725
|
const existing = await conn.submit(
|
|
324
|
-
"g.V().has('
|
|
325
|
-
{ vid: vertexId, lbl: REPO_LABEL }
|
|
726
|
+
"g.V().has('repositoryId', rid).hasId(vid).has('label', lbl).count()",
|
|
727
|
+
{ vid: vertexId, rid: config.repositoryId, lbl: REPO_LABEL }
|
|
326
728
|
);
|
|
327
729
|
if (existing.items.length > 0 && Number(existing.items[0]) > 0) {
|
|
328
730
|
throw new import_deep_memory.DuplicateRepositoryError(config.repositoryId);
|
|
329
731
|
}
|
|
732
|
+
const currentIds = await readRepositoryIndex(conn);
|
|
733
|
+
const updatedIds = currentIds.includes(config.repositoryId) ? currentIds : [...currentIds, config.repositoryId];
|
|
330
734
|
const bindings = {
|
|
331
735
|
vid: vertexId,
|
|
332
|
-
rid: config.repositoryId
|
|
736
|
+
rid: config.repositoryId,
|
|
737
|
+
pk: REPOSITORY_INDEX_PARTITION,
|
|
738
|
+
sid: REPOSITORY_INDEX_VERTEX_ID,
|
|
739
|
+
updatedIndex: JSON.stringify(updatedIds),
|
|
740
|
+
...repositoryConfigToLadderBindings(config)
|
|
333
741
|
};
|
|
334
|
-
|
|
335
|
-
repoLabel: config.label,
|
|
336
|
-
description: config.description,
|
|
337
|
-
type: config.type,
|
|
338
|
-
legal: config.legal,
|
|
339
|
-
owner: config.owner,
|
|
340
|
-
governanceConfig: JSON.stringify(config.governanceConfig),
|
|
341
|
-
metadata: config.metadata ? JSON.stringify(config.metadata) : void 0,
|
|
342
|
-
createdAt: config.createdAt,
|
|
343
|
-
createdBy: config.createdBy
|
|
344
|
-
};
|
|
345
|
-
const { chain } = propertyChain(bindings, props, 0);
|
|
346
|
-
const query = `g.addV('${REPO_LABEL}').property('id', vid).property('repositoryId', rid)${chain}`;
|
|
347
|
-
await conn.submit(query, bindings);
|
|
742
|
+
await conn.submit(REPOSITORY_CREATE_QUERY, bindings);
|
|
348
743
|
return {
|
|
349
744
|
repositoryId: config.repositoryId,
|
|
350
745
|
type: config.type,
|
|
@@ -359,9 +754,10 @@ async function createRepository(conn, config) {
|
|
|
359
754
|
};
|
|
360
755
|
}
|
|
361
756
|
async function getRepository(conn, repositoryId) {
|
|
757
|
+
const projection = buildRepositoryProjectChain();
|
|
362
758
|
const result = await conn.submit(
|
|
363
|
-
|
|
364
|
-
{ vid: repoVertexId(repositoryId) }
|
|
759
|
+
`g.V().has('repositoryId', rid).hasId(vid).hasLabel('_repository').${projection}`,
|
|
760
|
+
{ vid: repoVertexId(repositoryId), rid: repositoryId }
|
|
365
761
|
);
|
|
366
762
|
if (result.items.length === 0) return null;
|
|
367
763
|
return repositoryFromGremlin(result.items[0]);
|
|
@@ -369,27 +765,32 @@ async function getRepository(conn, repositoryId) {
|
|
|
369
765
|
async function listRepositories(conn, filter) {
|
|
370
766
|
const limit = filter?.limit ?? 20;
|
|
371
767
|
const offset = filter?.offset ?? 0;
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
if (filter?.type) {
|
|
376
|
-
countQuery += ".has('type', filterType)";
|
|
377
|
-
dataQuery += ".has('type', filterType)";
|
|
378
|
-
bindings["filterType"] = filter.type;
|
|
768
|
+
const repositoryIds = await readRepositoryIndex(conn);
|
|
769
|
+
if (repositoryIds.length === 0) {
|
|
770
|
+
return { items: [], total: 0, hasMore: false, limit, offset };
|
|
379
771
|
}
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
bindings["rangeStart"] = offset;
|
|
383
|
-
bindings["rangeEnd"] = offset + limit;
|
|
384
|
-
const dataResult = await conn.submit(
|
|
385
|
-
`${dataQuery}.range(rangeStart, rangeEnd).valueMap(true)`,
|
|
386
|
-
bindings
|
|
772
|
+
const hydrated = await Promise.all(
|
|
773
|
+
repositoryIds.map((rid) => getRepository(conn, rid))
|
|
387
774
|
);
|
|
388
|
-
|
|
775
|
+
let summaries = hydrated.filter((r) => r != null).map((r) => {
|
|
776
|
+
const summary = {
|
|
777
|
+
repositoryId: r.repositoryId,
|
|
778
|
+
label: r.label,
|
|
779
|
+
governanceConfig: r.governanceConfig
|
|
780
|
+
};
|
|
781
|
+
if (r.type !== void 0) summary.type = r.type;
|
|
782
|
+
if (r.description !== void 0) summary.description = r.description;
|
|
783
|
+
return summary;
|
|
784
|
+
});
|
|
785
|
+
if (filter?.type) {
|
|
786
|
+
summaries = summaries.filter((s) => s.type === filter.type);
|
|
787
|
+
}
|
|
788
|
+
const total = summaries.length;
|
|
789
|
+
const items = summaries.slice(offset, offset + limit);
|
|
389
790
|
return {
|
|
390
791
|
items,
|
|
391
792
|
total,
|
|
392
|
-
hasMore: offset +
|
|
793
|
+
hasMore: offset + items.length < total,
|
|
393
794
|
limit,
|
|
394
795
|
offset
|
|
395
796
|
};
|
|
@@ -398,7 +799,7 @@ async function updateRepository(conn, repositoryId, updates) {
|
|
|
398
799
|
const vertexId = repoVertexId(repositoryId);
|
|
399
800
|
const existing = await getRepository(conn, repositoryId);
|
|
400
801
|
if (!existing) throw new import_deep_memory.RepositoryNotFoundError(repositoryId);
|
|
401
|
-
const bindings = { vid: vertexId };
|
|
802
|
+
const bindings = { vid: vertexId, rid: repositoryId };
|
|
402
803
|
const props = {};
|
|
403
804
|
if (updates.label !== void 0) props["repoLabel"] = updates.label;
|
|
404
805
|
if (updates.description !== void 0) props["description"] = updates.description;
|
|
@@ -412,7 +813,7 @@ async function updateRepository(conn, repositoryId, updates) {
|
|
|
412
813
|
}
|
|
413
814
|
if (Object.keys(props).length === 0) return existing;
|
|
414
815
|
const { chain } = propertyChain(bindings, props, 0);
|
|
415
|
-
const query = `g.V().has('
|
|
816
|
+
const query = `g.V().has('repositoryId', rid).hasId(vid).hasLabel('_repository')${chain}`;
|
|
416
817
|
await conn.submit(query, bindings);
|
|
417
818
|
return await getRepository(conn, repositoryId);
|
|
418
819
|
}
|
|
@@ -458,6 +859,18 @@ async function deleteRepository(conn, repositoryId, onProgress) {
|
|
|
458
859
|
await onProgress?.({ entitiesDeleted, relationshipsDeleted, totalEntities, totalRelationships });
|
|
459
860
|
if (remainingCount === 0) break;
|
|
460
861
|
}
|
|
862
|
+
const currentIds = await readRepositoryIndex(conn);
|
|
863
|
+
const updatedIds = currentIds.filter((id) => id !== repositoryId);
|
|
864
|
+
if (updatedIds.length !== currentIds.length) {
|
|
865
|
+
await conn.submit(
|
|
866
|
+
"g.V().has('repositoryId', pk).hasId(sid).property('repositoryIds', updatedIndex)",
|
|
867
|
+
{
|
|
868
|
+
pk: REPOSITORY_INDEX_PARTITION,
|
|
869
|
+
sid: REPOSITORY_INDEX_VERTEX_ID,
|
|
870
|
+
updatedIndex: JSON.stringify(updatedIds)
|
|
871
|
+
}
|
|
872
|
+
);
|
|
873
|
+
}
|
|
461
874
|
}
|
|
462
875
|
async function deleteAllContents(conn, repositoryId, onProgress) {
|
|
463
876
|
const entityCountResult = await conn.submit(
|
|
@@ -554,33 +967,39 @@ async function getRepositoryStats(conn, repositoryId) {
|
|
|
554
967
|
function vocabVertexId(repositoryId) {
|
|
555
968
|
return `vocab:${repositoryId}`;
|
|
556
969
|
}
|
|
970
|
+
var EMPTY_VOCABULARY = () => ({
|
|
971
|
+
version: "0.0.0",
|
|
972
|
+
lastModified: (/* @__PURE__ */ new Date()).toISOString(),
|
|
973
|
+
modifiedBy: "system",
|
|
974
|
+
entityTypes: [],
|
|
975
|
+
relationshipTypes: []
|
|
976
|
+
});
|
|
557
977
|
async function getVocabulary(conn, repositoryId) {
|
|
558
978
|
const result = await conn.submit(
|
|
559
|
-
"g.V().has('
|
|
560
|
-
{ vid: vocabVertexId(repositoryId) }
|
|
979
|
+
"g.V().has('repositoryId', rid).hasId(vid).hasLabel('_vocabulary').values('vocabulary').limit(1)",
|
|
980
|
+
{ vid: vocabVertexId(repositoryId), rid: repositoryId }
|
|
561
981
|
);
|
|
562
|
-
if (result.items.length === 0)
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
982
|
+
if (result.items.length === 0) return EMPTY_VOCABULARY();
|
|
983
|
+
const raw = result.items[0];
|
|
984
|
+
const json = typeof raw === "string" ? raw : String(raw ?? "");
|
|
985
|
+
if (!json) return EMPTY_VOCABULARY();
|
|
986
|
+
try {
|
|
987
|
+
return JSON.parse(json);
|
|
988
|
+
} catch {
|
|
989
|
+
return EMPTY_VOCABULARY();
|
|
570
990
|
}
|
|
571
|
-
return vocabularyFromGremlin(result.items[0]);
|
|
572
991
|
}
|
|
573
992
|
async function saveVocabulary(conn, repositoryId, vocabulary) {
|
|
574
993
|
const vid = vocabVertexId(repositoryId);
|
|
575
994
|
const vocabJson = JSON.stringify(vocabulary);
|
|
576
995
|
const existing = await conn.submit(
|
|
577
|
-
"g.V().has('
|
|
578
|
-
{ vid }
|
|
996
|
+
"g.V().has('repositoryId', rid).hasId(vid).hasLabel('_vocabulary').count()",
|
|
997
|
+
{ vid, rid: repositoryId }
|
|
579
998
|
);
|
|
580
999
|
if (Number(existing.items[0] ?? 0) > 0) {
|
|
581
1000
|
await conn.submit(
|
|
582
|
-
"g.V().has('
|
|
583
|
-
{ vid, vocabJson }
|
|
1001
|
+
"g.V().has('repositoryId', rid).hasId(vid).hasLabel('_vocabulary').property('vocabulary', vocabJson)",
|
|
1002
|
+
{ vid, rid: repositoryId, vocabJson }
|
|
584
1003
|
);
|
|
585
1004
|
} else {
|
|
586
1005
|
await conn.submit(
|
|
@@ -592,20 +1011,22 @@ async function saveVocabulary(conn, repositoryId, vocabulary) {
|
|
|
592
1011
|
async function getVocabularyChangeLog(conn, repositoryId, options) {
|
|
593
1012
|
const limit = options?.limit ?? 10;
|
|
594
1013
|
const offset = options?.offset ?? 0;
|
|
595
|
-
const countResult = await
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
1014
|
+
const [countResult, dataResult] = await Promise.all([
|
|
1015
|
+
conn.submit(
|
|
1016
|
+
"g.V().has('repositoryId', rid).hasLabel('_vocabularyChangeLog').count()",
|
|
1017
|
+
{ rid: repositoryId }
|
|
1018
|
+
),
|
|
1019
|
+
conn.submit(
|
|
1020
|
+
"g.V().has('repositoryId', rid).hasLabel('_vocabularyChangeLog').order().by('proposedAt', decr).range(rangeStart, rangeEnd).valueMap(true)",
|
|
1021
|
+
{ rid: repositoryId, rangeStart: offset, rangeEnd: offset + limit }
|
|
1022
|
+
)
|
|
1023
|
+
]);
|
|
599
1024
|
const total = Number(countResult.items[0] ?? 0);
|
|
600
|
-
const dataResult = await conn.submit(
|
|
601
|
-
"g.V().has('repositoryId', rid).hasLabel('_vocabularyChangeLog').order().by('proposedAt', decr).range(rangeStart, rangeEnd).valueMap(true)",
|
|
602
|
-
{ rid: repositoryId, rangeStart: offset, rangeEnd: offset + limit }
|
|
603
|
-
);
|
|
604
1025
|
const items = dataResult.items.map(changeRecordFromGremlin);
|
|
605
1026
|
return {
|
|
606
1027
|
items,
|
|
607
1028
|
total,
|
|
608
|
-
hasMore: offset +
|
|
1029
|
+
hasMore: offset + items.length < total,
|
|
609
1030
|
limit,
|
|
610
1031
|
offset
|
|
611
1032
|
};
|
|
@@ -613,45 +1034,40 @@ async function getVocabularyChangeLog(conn, repositoryId, options) {
|
|
|
613
1034
|
|
|
614
1035
|
// src/queries/entity.ts
|
|
615
1036
|
var import_deep_memory2 = require("@utaba/deep-memory");
|
|
1037
|
+
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()})`;
|
|
616
1039
|
async function createEntity(conn, repositoryId, entity) {
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
1040
|
+
const bindings = {
|
|
1041
|
+
rid: repositoryId,
|
|
1042
|
+
vid: entity.id,
|
|
1043
|
+
vertexLabel: entity.entityType,
|
|
1044
|
+
...entityToLadderBindings(entity)
|
|
1045
|
+
};
|
|
1046
|
+
const result = await conn.submit(ENTITY_CREATE_QUERY, bindings);
|
|
1047
|
+
if (result.items[0] === DUPLICATE_SENTINEL) {
|
|
622
1048
|
throw new import_deep_memory2.DuplicateEntityError(entity.id);
|
|
623
1049
|
}
|
|
624
|
-
const props = entityToGremlinProps(repositoryId, entity);
|
|
625
|
-
const bindings = { vid: entity.id };
|
|
626
|
-
const propParts = [];
|
|
627
|
-
let idx = 0;
|
|
628
|
-
for (const [key, value] of Object.entries(props)) {
|
|
629
|
-
const paramName = `p${idx++}`;
|
|
630
|
-
bindings[paramName] = value;
|
|
631
|
-
propParts.push(`.property('${key}', ${paramName})`);
|
|
632
|
-
}
|
|
633
|
-
bindings["vertexLabel"] = entity.entityType;
|
|
634
|
-
const query = `g.addV(vertexLabel).property('id', vid)${propParts.join("")}`;
|
|
635
|
-
await conn.submit(query, bindings);
|
|
636
1050
|
return entity;
|
|
637
1051
|
}
|
|
638
|
-
async function getEntity(conn, repositoryId, entityId) {
|
|
1052
|
+
async function getEntity(conn, repositoryId, entityId, options) {
|
|
1053
|
+
const projection = (0, import_deep_memory2.buildVertexProjectChain)({ withEmbedding: options?.loadEmbeddings });
|
|
639
1054
|
const result = await conn.submit(
|
|
640
|
-
|
|
1055
|
+
`g.V().has('repositoryId', rid).hasId(eid).has('entityType').${projection}`,
|
|
641
1056
|
{ rid: repositoryId, eid: entityId }
|
|
642
1057
|
);
|
|
643
1058
|
if (result.items.length === 0) return null;
|
|
644
1059
|
return entityFromGremlin(result.items[0]);
|
|
645
1060
|
}
|
|
646
|
-
async function getEntityBySlug(conn, repositoryId, slug) {
|
|
1061
|
+
async function getEntityBySlug(conn, repositoryId, slug, options) {
|
|
1062
|
+
const projection = (0, import_deep_memory2.buildVertexProjectChain)({ withEmbedding: options?.loadEmbeddings });
|
|
647
1063
|
const result = await conn.submit(
|
|
648
|
-
|
|
1064
|
+
`g.V().has('repositoryId', rid).has('slug', slugVal).has('entityType').${projection}`,
|
|
649
1065
|
{ rid: repositoryId, slugVal: slug }
|
|
650
1066
|
);
|
|
651
1067
|
if (result.items.length === 0) return null;
|
|
652
1068
|
return entityFromGremlin(result.items[0]);
|
|
653
1069
|
}
|
|
654
|
-
async function getEntities(conn, repositoryId, entityIds) {
|
|
1070
|
+
async function getEntities(conn, repositoryId, entityIds, options) {
|
|
655
1071
|
if (entityIds.length === 0) return /* @__PURE__ */ new Map();
|
|
656
1072
|
const bindings = { rid: repositoryId };
|
|
657
1073
|
const idParams = [];
|
|
@@ -661,8 +1077,9 @@ async function getEntities(conn, repositoryId, entityIds) {
|
|
|
661
1077
|
idParams.push(paramName);
|
|
662
1078
|
});
|
|
663
1079
|
const withinClause = `within(${idParams.join(", ")})`;
|
|
1080
|
+
const projection = (0, import_deep_memory2.buildVertexProjectChain)({ withEmbedding: options?.loadEmbeddings });
|
|
664
1081
|
const result = await conn.submit(
|
|
665
|
-
`g.V().has('repositoryId', rid).
|
|
1082
|
+
`g.V().has('repositoryId', rid).hasId(${withinClause}).has('entityType').${projection}`,
|
|
666
1083
|
bindings
|
|
667
1084
|
);
|
|
668
1085
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -700,95 +1117,96 @@ async function updateEntity(conn, repositoryId, entityId, updates) {
|
|
|
700
1117
|
addProp("modifiedAt", updates.provenance.modifiedAt);
|
|
701
1118
|
if (updates.provenance.modifiedInConversation != null) addProp("modifiedInConversation", updates.provenance.modifiedInConversation);
|
|
702
1119
|
if (updates.provenance.modifiedFromMessage != null) addProp("modifiedFromMessage", updates.provenance.modifiedFromMessage);
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
|
|
1120
|
+
const projection = (0, import_deep_memory2.buildVertexProjectChain)();
|
|
1121
|
+
const query = `g.V().has('repositoryId', rid).hasId(eid).has('entityType')${propParts.join("")}.${projection}`;
|
|
1122
|
+
const result = await conn.submit(query, bindings);
|
|
1123
|
+
if (result.items.length === 0) {
|
|
1124
|
+
throw new import_deep_memory2.EntityNotFoundError(entityId);
|
|
1125
|
+
}
|
|
1126
|
+
return entityFromGremlin(result.items[0]);
|
|
706
1127
|
}
|
|
707
1128
|
async function deleteEntity(conn, repositoryId, entityId) {
|
|
708
1129
|
await conn.submit(
|
|
709
|
-
"g.V().has('repositoryId', rid).
|
|
1130
|
+
"g.V().has('repositoryId', rid).hasId(eid).has('entityType').drop()",
|
|
710
1131
|
{ rid: repositoryId, eid: entityId }
|
|
711
1132
|
);
|
|
712
1133
|
}
|
|
713
1134
|
async function deleteEntitiesByType(conn, repositoryId, entityType) {
|
|
714
|
-
const
|
|
715
|
-
"g.V().has('repositoryId', rid).has('entityType', etype).
|
|
716
|
-
{ rid: repositoryId, etype: entityType }
|
|
717
|
-
);
|
|
718
|
-
const deletedEntities = Number(entityCountResult.items[0] ?? 0);
|
|
719
|
-
const relCountResult = await conn.submit(
|
|
720
|
-
"g.V().has('repositoryId', rid).has('entityType', etype).bothE().dedup().count()",
|
|
1135
|
+
const result = await conn.submit(
|
|
1136
|
+
"g.V().has('repositoryId', rid).has('entityType', etype).aggregate('found').by('id').drop().cap('found')",
|
|
721
1137
|
{ rid: repositoryId, etype: entityType }
|
|
722
1138
|
);
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
"g.V().has('repositoryId', rid).has('entityType', etype).drop()",
|
|
727
|
-
{ rid: repositoryId, etype: entityType }
|
|
728
|
-
);
|
|
729
|
-
}
|
|
730
|
-
return { deletedEntities, deletedRelationships };
|
|
1139
|
+
const bucket = result.items[0];
|
|
1140
|
+
const deletedEntities = Array.isArray(bucket) ? bucket.length : 0;
|
|
1141
|
+
return { deletedEntities, deletedRelationships: void 0 };
|
|
731
1142
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
1143
|
+
function sqlPath(key) {
|
|
1144
|
+
return `c.${key}[0]._value`;
|
|
1145
|
+
}
|
|
1146
|
+
function buildWhereClause(query, repositoryId) {
|
|
1147
|
+
const params = [{ name: "@rid", value: repositoryId }];
|
|
1148
|
+
const predicates = ["c.repositoryId = @rid", "IS_DEFINED(c.entityType)"];
|
|
735
1149
|
if (query.entityTypes && query.entityTypes.length > 0) {
|
|
736
|
-
const
|
|
1150
|
+
const typeParamNames = [];
|
|
737
1151
|
query.entityTypes.forEach((t, i) => {
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
1152
|
+
const name = `@etype${i}`;
|
|
1153
|
+
params.push({ name, value: t });
|
|
1154
|
+
typeParamNames.push(name);
|
|
741
1155
|
});
|
|
742
|
-
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
`g.V()${filterClause}.valueMap(true)`,
|
|
749
|
-
bindings
|
|
1156
|
+
predicates.push(`${sqlPath("entityType")} IN (${typeParamNames.join(", ")})`);
|
|
1157
|
+
}
|
|
1158
|
+
if (query.searchTerm) {
|
|
1159
|
+
params.push({ name: "@term", value: query.searchTerm });
|
|
1160
|
+
predicates.push(
|
|
1161
|
+
`(CONTAINS(${sqlPath("entityLabel")}, @term, true) OR CONTAINS(${sqlPath("slug")}, @term, true) OR CONTAINS(${sqlPath("summary")}, @term, true))`
|
|
750
1162
|
);
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
items2 = items2.filter((entity) => {
|
|
760
|
-
for (const [key, value] of Object.entries(query.properties)) {
|
|
761
|
-
if (entity.properties[key] !== value) return false;
|
|
762
|
-
}
|
|
763
|
-
return true;
|
|
764
|
-
});
|
|
1163
|
+
}
|
|
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)`);
|
|
765
1171
|
}
|
|
766
|
-
const total2 = items2.length;
|
|
767
|
-
const paged = items2.slice(query.offset, query.offset + query.limit);
|
|
768
|
-
return {
|
|
769
|
-
items: paged,
|
|
770
|
-
total: total2,
|
|
771
|
-
hasMore: query.offset + query.limit < total2,
|
|
772
|
-
limit: query.limit,
|
|
773
|
-
offset: query.offset
|
|
774
|
-
};
|
|
775
1172
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
);
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
1173
|
+
return { sqlWhere: `WHERE ${predicates.join(" AND ")}`, params };
|
|
1174
|
+
}
|
|
1175
|
+
function buildSelectClause(loadEmbeddings) {
|
|
1176
|
+
const fields = ["c.id", ...STORED_ENTITY_FIELDS.filter((f) => f !== "id").map((f) => `c.${f}`)];
|
|
1177
|
+
if (loadEmbeddings) fields.push("c.embedding");
|
|
1178
|
+
return `SELECT ${fields.join(", ")}`;
|
|
1179
|
+
}
|
|
1180
|
+
async function findEntities(docClient, repositoryId, query, options) {
|
|
1181
|
+
const { sqlWhere, params } = buildWhereClause(query, repositoryId);
|
|
1182
|
+
const dataParams = [
|
|
1183
|
+
...params,
|
|
1184
|
+
{ name: "@off", value: query.offset },
|
|
1185
|
+
{ name: "@lim", value: query.limit }
|
|
1186
|
+
];
|
|
1187
|
+
const selectClause = buildSelectClause(options?.loadEmbeddings === true);
|
|
1188
|
+
const dataSql = `${selectClause} FROM c ${sqlWhere} ORDER BY c.id OFFSET @off LIMIT @lim`;
|
|
1189
|
+
const countSql = `SELECT VALUE COUNT(1) FROM c ${sqlWhere}`;
|
|
1190
|
+
const skipCount = query.properties != null && Object.keys(query.properties).length > 0;
|
|
1191
|
+
const [dataResult, countResult] = await Promise.all([
|
|
1192
|
+
docClient.query(dataSql, dataParams, {
|
|
1193
|
+
partitionKey: repositoryId
|
|
1194
|
+
}),
|
|
1195
|
+
skipCount ? Promise.resolve(null) : docClient.query(countSql, params, { partitionKey: repositoryId })
|
|
1196
|
+
]);
|
|
1197
|
+
let items = dataResult.documents.map(entityFromDocument);
|
|
1198
|
+
if (query.properties != null && Object.keys(query.properties).length > 0) {
|
|
1199
|
+
const filters = Object.entries(query.properties).map(
|
|
1200
|
+
([key, value]) => ({ key, operator: "eq", value })
|
|
1201
|
+
);
|
|
1202
|
+
items = items.filter((entity) => (0, import_deep_memory2.matchesPropertyFilters)(entity.properties, filters));
|
|
1203
|
+
}
|
|
1204
|
+
const total = countResult && countResult.documents.length > 0 ? Number(countResult.documents[0]) : void 0;
|
|
1205
|
+
const hasMore = total != null ? query.offset + items.length < total : items.length === query.limit;
|
|
788
1206
|
return {
|
|
789
1207
|
items,
|
|
790
1208
|
total,
|
|
791
|
-
hasMore
|
|
1209
|
+
hasMore,
|
|
792
1210
|
limit: query.limit,
|
|
793
1211
|
offset: query.offset
|
|
794
1212
|
};
|
|
@@ -796,36 +1214,27 @@ async function findEntities(conn, repositoryId, query) {
|
|
|
796
1214
|
|
|
797
1215
|
// src/queries/relationship.ts
|
|
798
1216
|
var import_deep_memory3 = require("@utaba/deep-memory");
|
|
1217
|
+
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()})`;
|
|
799
1219
|
async function createRelationship(conn, repositoryId, relationship) {
|
|
800
|
-
const existing = await conn.submit(
|
|
801
|
-
"g.E().has('repositoryId', rid).has('id', relId).count()",
|
|
802
|
-
{ rid: repositoryId, relId: relationship.id }
|
|
803
|
-
);
|
|
804
|
-
if (Number(existing.items[0] ?? 0) > 0) {
|
|
805
|
-
throw new import_deep_memory3.DuplicateRelationshipError(relationship.id);
|
|
806
|
-
}
|
|
807
|
-
const props = relationshipToGremlinProps(repositoryId, relationship);
|
|
808
1220
|
const bindings = {
|
|
1221
|
+
rid: repositoryId,
|
|
809
1222
|
relId: relationship.id,
|
|
810
1223
|
srcId: relationship.sourceEntityId,
|
|
811
1224
|
tgtId: relationship.targetEntityId,
|
|
812
|
-
|
|
813
|
-
|
|
1225
|
+
edgeLabel: relationship.relationshipType,
|
|
1226
|
+
...relationshipToLadderBindings(relationship)
|
|
814
1227
|
};
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
const paramName = `p${idx++}`;
|
|
819
|
-
bindings[paramName] = value;
|
|
820
|
-
propParts.push(`.property('${key}', ${paramName})`);
|
|
1228
|
+
const result = await conn.submit(RELATIONSHIP_CREATE_QUERY, bindings);
|
|
1229
|
+
if (result.items[0] === DUPLICATE_SENTINEL2) {
|
|
1230
|
+
throw new import_deep_memory3.DuplicateRelationshipError(relationship.id);
|
|
821
1231
|
}
|
|
822
|
-
const query = `g.V().has('repositoryId', rid).has('id', srcId).has('entityType').addE(edgeLabel).to(g.V().has('repositoryId', rid).has('id', tgtId).has('entityType')).property('id', relId)${propParts.join("")}`;
|
|
823
|
-
await conn.submit(query, bindings);
|
|
824
1232
|
return relationship;
|
|
825
1233
|
}
|
|
826
1234
|
async function getRelationship(conn, repositoryId, relationshipId) {
|
|
1235
|
+
const projection = (0, import_deep_memory3.buildEdgeProjectChain)();
|
|
827
1236
|
const result = await conn.submit(
|
|
828
|
-
|
|
1237
|
+
`g.E().hasId(relId).has('repositoryId', rid).${projection}`,
|
|
829
1238
|
{ relId: relationshipId, rid: repositoryId }
|
|
830
1239
|
);
|
|
831
1240
|
if (result.items.length === 0) return null;
|
|
@@ -835,240 +1244,86 @@ async function getEntityRelationships(conn, repositoryId, entityId, options) {
|
|
|
835
1244
|
const limit = options?.limit ?? 50;
|
|
836
1245
|
const offset = options?.offset ?? 0;
|
|
837
1246
|
const direction = options?.direction ?? "both";
|
|
838
|
-
const
|
|
1247
|
+
const hasPropertyFilters = options?.propertyFilters != null && options.propertyFilters.length > 0;
|
|
1248
|
+
const baseBindings = {
|
|
839
1249
|
rid: repositoryId,
|
|
840
1250
|
eid: entityId
|
|
841
|
-
};
|
|
842
|
-
let edgeTraversal;
|
|
843
|
-
switch (direction) {
|
|
844
|
-
case "outbound":
|
|
845
|
-
edgeTraversal = "g.V().has('repositoryId', rid).has('id', eid).has('entityType').outE()";
|
|
846
|
-
break;
|
|
847
|
-
case "inbound":
|
|
848
|
-
edgeTraversal = "g.V().has('repositoryId', rid).has('id', eid).has('entityType').inE()";
|
|
849
|
-
break;
|
|
850
|
-
case "both":
|
|
851
|
-
default:
|
|
852
|
-
edgeTraversal = "g.V().has('repositoryId', rid).has('id', eid).has('entityType').bothE()";
|
|
853
|
-
break;
|
|
854
|
-
}
|
|
855
|
-
let typeFilter = "";
|
|
856
|
-
if (options?.relationshipTypes && options.relationshipTypes.length > 0) {
|
|
857
|
-
const typeParams = [];
|
|
858
|
-
options.relationshipTypes.forEach((t, i) => {
|
|
859
|
-
const paramName = `rtype${i}`;
|
|
860
|
-
bindings[paramName] = t;
|
|
861
|
-
typeParams.push(paramName);
|
|
862
|
-
});
|
|
863
|
-
typeFilter = `.hasLabel(${typeParams.join(", ")})`;
|
|
864
|
-
}
|
|
865
|
-
let unionQuery = null;
|
|
866
|
-
if (direction === "outbound") {
|
|
867
|
-
unionQuery = `g.V().has('repositoryId', rid).has('id', eid).has('entityType').union(outE()${typeFilter}, inE()${typeFilter}.has('bidirectional', true))`;
|
|
868
|
-
} else if (direction === "inbound") {
|
|
869
|
-
unionQuery = `g.V().has('repositoryId', rid).has('id', eid).has('entityType').union(inE()${typeFilter}, outE()${typeFilter}.has('bidirectional', true))`;
|
|
870
|
-
}
|
|
871
|
-
const baseQuery = unionQuery ?? `${edgeTraversal}${typeFilter}`;
|
|
872
|
-
const countResult = await conn.submit(`${baseQuery}.dedup().count()`, bindings);
|
|
873
|
-
const total = Number(countResult.items[0] ?? 0);
|
|
874
|
-
bindings["rangeStart"] = offset;
|
|
875
|
-
bindings["rangeEnd"] = offset + limit;
|
|
876
|
-
const dataResult = await conn.submit(
|
|
877
|
-
`${baseQuery}.dedup().range(rangeStart, rangeEnd).valueMap(true)`,
|
|
878
|
-
bindings
|
|
879
|
-
);
|
|
880
|
-
let items = dataResult.items.map(relationshipFromGremlin);
|
|
881
|
-
if (options?.propertyFilters && options.propertyFilters.length > 0) {
|
|
882
|
-
items = items.filter((rel) => (0, import_deep_memory3.matchesPropertyFilters)(rel.properties, options.propertyFilters));
|
|
883
|
-
}
|
|
884
|
-
return {
|
|
885
|
-
items,
|
|
886
|
-
total,
|
|
887
|
-
hasMore: offset + limit < total,
|
|
888
|
-
limit,
|
|
889
|
-
offset
|
|
890
|
-
};
|
|
891
|
-
}
|
|
892
|
-
async function deleteRelationship(conn, repositoryId, relationshipId) {
|
|
893
|
-
await conn.submit(
|
|
894
|
-
"g.E().has('id', relId).has('repositoryId', rid).drop()",
|
|
895
|
-
{ relId: relationshipId, rid: repositoryId }
|
|
896
|
-
);
|
|
897
|
-
}
|
|
898
|
-
async function deleteRelationshipsByType(conn, repositoryId, relationshipType) {
|
|
899
|
-
const countResult = await conn.submit(
|
|
900
|
-
"g.E().has('repositoryId', rid).hasLabel(rtype).count()",
|
|
901
|
-
{ rid: repositoryId, rtype: relationshipType }
|
|
902
|
-
);
|
|
903
|
-
const deletedRelationships = Number(countResult.items[0] ?? 0);
|
|
904
|
-
if (deletedRelationships > 0) {
|
|
905
|
-
await conn.submit(
|
|
906
|
-
"g.E().has('repositoryId', rid).hasLabel(rtype).drop()",
|
|
907
|
-
{ rid: repositoryId, rtype: relationshipType }
|
|
908
|
-
);
|
|
909
|
-
}
|
|
910
|
-
return { deletedRelationships };
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
// src/queries/traversal.ts
|
|
914
|
-
var import_deep_memory4 = require("@utaba/deep-memory");
|
|
915
|
-
async function exploreNeighborhood(conn, repositoryId, entityId, options) {
|
|
916
|
-
const layers = [];
|
|
917
|
-
let frontier = /* @__PURE__ */ new Set([entityId]);
|
|
918
|
-
const visited = /* @__PURE__ */ new Set([entityId]);
|
|
919
|
-
for (let depth = 0; depth < options.depth; depth++) {
|
|
920
|
-
const layer = {};
|
|
921
|
-
const nextFrontier = /* @__PURE__ */ new Set();
|
|
922
|
-
const layerCache = /* @__PURE__ */ new Map();
|
|
923
|
-
for (const currentId of frontier) {
|
|
924
|
-
const rels = await getRelationshipsForEntity(conn, repositoryId, currentId, options);
|
|
925
|
-
for (const rel of rels) {
|
|
926
|
-
const relType = rel.relationshipType;
|
|
927
|
-
if (!layer[relType]) {
|
|
928
|
-
layer[relType] = { total: 0, entities: [], relationships: [] };
|
|
929
|
-
}
|
|
930
|
-
let connectedId;
|
|
931
|
-
if (rel.sourceEntityId === currentId) {
|
|
932
|
-
connectedId = rel.targetEntityId;
|
|
933
|
-
} else {
|
|
934
|
-
connectedId = rel.sourceEntityId;
|
|
935
|
-
}
|
|
936
|
-
if (visited.has(connectedId)) continue;
|
|
937
|
-
let entity = layerCache.get(connectedId);
|
|
938
|
-
if (entity === void 0) {
|
|
939
|
-
const loaded = await getEntityLight(conn, repositoryId, connectedId);
|
|
940
|
-
if (loaded && (!options.entityTypes || options.entityTypes.length === 0 || options.entityTypes.includes(loaded.entityType))) {
|
|
941
|
-
entity = loaded;
|
|
942
|
-
} else {
|
|
943
|
-
entity = null;
|
|
944
|
-
}
|
|
945
|
-
layerCache.set(connectedId, entity);
|
|
946
|
-
if (entity) nextFrontier.add(connectedId);
|
|
947
|
-
}
|
|
948
|
-
if (!entity) continue;
|
|
949
|
-
layer[relType].entities.push(entity);
|
|
950
|
-
layer[relType].relationships.push(rel);
|
|
951
|
-
layer[relType].total = layer[relType].entities.length;
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
for (const relType of Object.keys(layer)) {
|
|
955
|
-
const group = layer[relType];
|
|
956
|
-
const start = options.offsetPerType;
|
|
957
|
-
const end = start + options.limitPerType;
|
|
958
|
-
group.entities = group.entities.slice(start, end);
|
|
959
|
-
group.relationships = group.relationships.slice(start, end);
|
|
960
|
-
}
|
|
961
|
-
if (Object.keys(layer).length > 0) {
|
|
962
|
-
layers.push(layer);
|
|
963
|
-
}
|
|
964
|
-
for (const id of nextFrontier) {
|
|
965
|
-
visited.add(id);
|
|
966
|
-
}
|
|
967
|
-
frontier = nextFrontier;
|
|
968
|
-
if (frontier.size === 0) break;
|
|
969
|
-
}
|
|
970
|
-
return { centerId: entityId, layers };
|
|
971
|
-
}
|
|
972
|
-
async function getRelationshipsForEntity(conn, repositoryId, entityId, options) {
|
|
973
|
-
const bindings = { rid: repositoryId, eid: entityId };
|
|
1251
|
+
};
|
|
974
1252
|
let edgeTraversal;
|
|
975
|
-
switch (
|
|
1253
|
+
switch (direction) {
|
|
976
1254
|
case "outbound":
|
|
977
|
-
edgeTraversal = "g.V().has('repositoryId', rid).
|
|
1255
|
+
edgeTraversal = "g.V().has('repositoryId', rid).hasId(eid).has('entityType').outE()";
|
|
978
1256
|
break;
|
|
979
1257
|
case "inbound":
|
|
980
|
-
edgeTraversal = "g.V().has('repositoryId', rid).
|
|
1258
|
+
edgeTraversal = "g.V().has('repositoryId', rid).hasId(eid).has('entityType').inE()";
|
|
981
1259
|
break;
|
|
982
1260
|
case "both":
|
|
983
1261
|
default:
|
|
984
|
-
edgeTraversal = "g.V().has('repositoryId', rid).
|
|
1262
|
+
edgeTraversal = "g.V().has('repositoryId', rid).hasId(eid).has('entityType').bothE()";
|
|
985
1263
|
break;
|
|
986
1264
|
}
|
|
987
1265
|
let typeFilter = "";
|
|
988
|
-
if (options
|
|
1266
|
+
if (options?.relationshipTypes && options.relationshipTypes.length > 0) {
|
|
989
1267
|
const typeParams = [];
|
|
990
1268
|
options.relationshipTypes.forEach((t, i) => {
|
|
991
1269
|
const paramName = `rtype${i}`;
|
|
992
|
-
|
|
1270
|
+
baseBindings[paramName] = t;
|
|
993
1271
|
typeParams.push(paramName);
|
|
994
1272
|
});
|
|
995
1273
|
typeFilter = `.hasLabel(${typeParams.join(", ")})`;
|
|
996
1274
|
}
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
)
|
|
1001
|
-
|
|
1002
|
-
if (options.relationshipPropertyFilters && options.relationshipPropertyFilters.length > 0) {
|
|
1003
|
-
rels = rels.filter((rel) => (0, import_deep_memory4.matchesPropertyFilters)(rel.properties, options.relationshipPropertyFilters));
|
|
1275
|
+
let unionQuery = null;
|
|
1276
|
+
if (direction === "outbound") {
|
|
1277
|
+
unionQuery = `g.V().has('repositoryId', rid).hasId(eid).has('entityType').union(outE()${typeFilter}, inE()${typeFilter}.has('bidirectional', true))`;
|
|
1278
|
+
} else if (direction === "inbound") {
|
|
1279
|
+
unionQuery = `g.V().has('repositoryId', rid).hasId(eid).has('entityType').union(inE()${typeFilter}, outE()${typeFilter}.has('bidirectional', true))`;
|
|
1004
1280
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
const
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
let queue = [{ entityId: sourceId, path: [sourceId], relationshipIds: [] }];
|
|
1020
|
-
for (let depth = 0; depth < maxDepth && paths.length < limit; depth++) {
|
|
1021
|
-
const nextQueue = [];
|
|
1022
|
-
for (const state of queue) {
|
|
1023
|
-
if (paths.length >= limit) break;
|
|
1024
|
-
const bindings = { rid: repositoryId, eid: state.entityId };
|
|
1025
|
-
let edgeQuery = "g.V().has('repositoryId', rid).has('id', eid).has('entityType').bothE()";
|
|
1026
|
-
if (options.relationshipTypes && options.relationshipTypes.length > 0) {
|
|
1027
|
-
const typeParams = [];
|
|
1028
|
-
options.relationshipTypes.forEach((t, i) => {
|
|
1029
|
-
const paramName = `rtype${i}`;
|
|
1030
|
-
bindings[paramName] = t;
|
|
1031
|
-
typeParams.push(paramName);
|
|
1032
|
-
});
|
|
1033
|
-
edgeQuery += `.hasLabel(${typeParams.join(", ")})`;
|
|
1034
|
-
}
|
|
1035
|
-
const relResult = await conn.submit(`${edgeQuery}.valueMap(true)`, bindings);
|
|
1036
|
-
const rels = relResult.items.map(relationshipFromGremlin);
|
|
1037
|
-
let filteredRels = rels;
|
|
1038
|
-
if (options.relationshipPropertyFilters && options.relationshipPropertyFilters.length > 0) {
|
|
1039
|
-
filteredRels = rels.filter((r) => (0, import_deep_memory4.matchesPropertyFilters)(r.properties, options.relationshipPropertyFilters));
|
|
1040
|
-
}
|
|
1041
|
-
for (const rel of filteredRels) {
|
|
1042
|
-
const nextId = rel.sourceEntityId === state.entityId ? rel.targetEntityId : rel.sourceEntityId;
|
|
1043
|
-
if (state.path.includes(nextId)) continue;
|
|
1044
|
-
if (options.entityTypes && options.entityTypes.length > 0 && nextId !== targetId) {
|
|
1045
|
-
const entity = await getEntityLight(conn, repositoryId, nextId);
|
|
1046
|
-
if (!entity || !options.entityTypes.includes(entity.entityType)) continue;
|
|
1047
|
-
}
|
|
1048
|
-
const newPath = [...state.path, nextId];
|
|
1049
|
-
const newRelIds = [...state.relationshipIds, rel.id];
|
|
1050
|
-
if (nextId === targetId) {
|
|
1051
|
-
paths.push({ entityIds: newPath, relationshipIds: newRelIds });
|
|
1052
|
-
if (paths.length >= limit) break;
|
|
1053
|
-
} else {
|
|
1054
|
-
nextQueue.push({ entityId: nextId, path: newPath, relationshipIds: newRelIds });
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
queue = nextQueue;
|
|
1059
|
-
if (queue.length === 0) break;
|
|
1281
|
+
const baseQuery = unionQuery ?? `${edgeTraversal}${typeFilter}`;
|
|
1282
|
+
const projection = (0, import_deep_memory3.buildEdgeProjectChain)();
|
|
1283
|
+
const dataBindings = { ...baseBindings, rangeStart: offset, rangeEnd: offset + limit };
|
|
1284
|
+
const [countResult, dataResult] = await Promise.all([
|
|
1285
|
+
hasPropertyFilters ? Promise.resolve(null) : conn.submit(`${baseQuery}.dedup().count()`, baseBindings),
|
|
1286
|
+
conn.submit(
|
|
1287
|
+
`${baseQuery}.dedup().range(rangeStart, rangeEnd).${projection}`,
|
|
1288
|
+
dataBindings
|
|
1289
|
+
)
|
|
1290
|
+
]);
|
|
1291
|
+
const rawItems = dataResult.items;
|
|
1292
|
+
let items = rawItems.map(relationshipFromGremlin);
|
|
1293
|
+
if (hasPropertyFilters) {
|
|
1294
|
+
items = items.filter((rel) => (0, import_deep_memory3.matchesPropertyFilters)(rel.properties, options.propertyFilters));
|
|
1060
1295
|
}
|
|
1296
|
+
const total = countResult ? Number(countResult.items[0] ?? 0) : void 0;
|
|
1297
|
+
const hasMore = total != null ? offset + rawItems.length < total : rawItems.length === limit;
|
|
1061
1298
|
return {
|
|
1062
|
-
|
|
1063
|
-
|
|
1299
|
+
items,
|
|
1300
|
+
total,
|
|
1301
|
+
hasMore,
|
|
1302
|
+
limit,
|
|
1303
|
+
offset
|
|
1064
1304
|
};
|
|
1065
1305
|
}
|
|
1306
|
+
async function deleteRelationship(conn, repositoryId, relationshipId) {
|
|
1307
|
+
await conn.submit(
|
|
1308
|
+
"g.E().hasId(relId).has('repositoryId', rid).drop()",
|
|
1309
|
+
{ relId: relationshipId, rid: repositoryId }
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
async function deleteRelationshipsByType(conn, repositoryId, relationshipType) {
|
|
1313
|
+
const result = await conn.submit(
|
|
1314
|
+
"g.E().has('repositoryId', rid).hasLabel(rtype).aggregate('found').by('id').drop().cap('found')",
|
|
1315
|
+
{ rid: repositoryId, rtype: relationshipType }
|
|
1316
|
+
);
|
|
1317
|
+
const bucket = result.items[0];
|
|
1318
|
+
const deletedRelationships = Array.isArray(bucket) ? bucket.length : 0;
|
|
1319
|
+
return { deletedRelationships };
|
|
1320
|
+
}
|
|
1066
1321
|
|
|
1067
1322
|
// src/queries/timeline.ts
|
|
1068
1323
|
async function getTimeline(conn, repositoryId, entityId, options) {
|
|
1069
1324
|
const events = [];
|
|
1070
1325
|
const entityResult = await conn.submit(
|
|
1071
|
-
"g.V().has('repositoryId', rid).
|
|
1326
|
+
"g.V().has('repositoryId', rid).hasId(eid).has('entityType').valueMap('createdAt', 'modifiedAt')",
|
|
1072
1327
|
{ rid: repositoryId, eid: entityId }
|
|
1073
1328
|
);
|
|
1074
1329
|
if (entityResult.items.length > 0) {
|
|
@@ -1091,7 +1346,7 @@ async function getTimeline(conn, repositoryId, entityId, options) {
|
|
|
1091
1346
|
}
|
|
1092
1347
|
}
|
|
1093
1348
|
const relResult = await conn.submit(
|
|
1094
|
-
"g.V().has('repositoryId', rid).
|
|
1349
|
+
"g.V().has('repositoryId', rid).hasId(eid).has('entityType').bothE().valueMap('id', 'createdAt')",
|
|
1095
1350
|
{ rid: repositoryId, eid: entityId }
|
|
1096
1351
|
);
|
|
1097
1352
|
for (const item of relResult.items) {
|
|
@@ -1126,7 +1381,7 @@ function isInTimeRange(timestamp, timeRange) {
|
|
|
1126
1381
|
}
|
|
1127
1382
|
|
|
1128
1383
|
// src/queries/adaptive-import.ts
|
|
1129
|
-
var
|
|
1384
|
+
var import_deep_memory4 = require("@utaba/deep-memory");
|
|
1130
1385
|
var DEFAULT_MIN = 1;
|
|
1131
1386
|
var DEFAULT_START = 5;
|
|
1132
1387
|
var DEFAULT_MAX = 32;
|
|
@@ -1320,7 +1575,7 @@ async function runTaskWithUsage(fn) {
|
|
|
1320
1575
|
}
|
|
1321
1576
|
}
|
|
1322
1577
|
}
|
|
1323
|
-
function
|
|
1578
|
+
function sleep3(ms) {
|
|
1324
1579
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1325
1580
|
}
|
|
1326
1581
|
async function runAdaptive(items, controller, fn) {
|
|
@@ -1345,7 +1600,7 @@ async function runAdaptive(items, controller, fn) {
|
|
|
1345
1600
|
const slotsAvailable = inFlight < controller.getConcurrency();
|
|
1346
1601
|
if (cooldownRemaining <= 0 && slotsAvailable) break;
|
|
1347
1602
|
if (cooldownRemaining > 0) {
|
|
1348
|
-
await Promise.race([
|
|
1603
|
+
await Promise.race([sleep3(cooldownRemaining), gate.promise]);
|
|
1349
1604
|
} else {
|
|
1350
1605
|
await gate.promise;
|
|
1351
1606
|
}
|
|
@@ -1377,7 +1632,7 @@ async function runAdaptive(items, controller, fn) {
|
|
|
1377
1632
|
}
|
|
1378
1633
|
await Promise.all(workers);
|
|
1379
1634
|
if (aborted) {
|
|
1380
|
-
throw new
|
|
1635
|
+
throw new import_deep_memory4.ImportThrottleAbortError(
|
|
1381
1636
|
controller.getConcurrency(),
|
|
1382
1637
|
controller.getConsecutiveThrottlesAtMin(),
|
|
1383
1638
|
controller.getCompleted(),
|
|
@@ -1519,91 +1774,82 @@ async function importBulk(conn, repositoryId, data, options) {
|
|
|
1519
1774
|
}
|
|
1520
1775
|
return { entitiesImported, relationshipsImported, errors };
|
|
1521
1776
|
}
|
|
1777
|
+
var ENTITY_LADDER_CHAIN = buildEntityPropertyLadder();
|
|
1778
|
+
var RELATIONSHIP_LADDER_CHAIN = buildRelationshipPropertyLadder();
|
|
1779
|
+
var INSERT_ENTITY_QUERY = `g.addV(vertexLabel).property('id', vid).property('repositoryId', rid)${ENTITY_LADDER_CHAIN}`;
|
|
1780
|
+
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})`;
|
|
1522
1783
|
async function insertEntity(conn, repositoryId, entity) {
|
|
1523
|
-
const props = entityToGremlinProps(repositoryId, entity);
|
|
1524
1784
|
const bindings = {
|
|
1785
|
+
rid: repositoryId,
|
|
1525
1786
|
vid: entity.id,
|
|
1526
|
-
vertexLabel: entity.entityType
|
|
1787
|
+
vertexLabel: entity.entityType,
|
|
1788
|
+
...entityToLadderBindings(entity)
|
|
1527
1789
|
};
|
|
1528
|
-
|
|
1529
|
-
let idx = 0;
|
|
1530
|
-
for (const [key, value] of Object.entries(props)) {
|
|
1531
|
-
const paramName = `p${idx++}`;
|
|
1532
|
-
bindings[paramName] = value;
|
|
1533
|
-
propParts.push(`.property('${key}', ${paramName})`);
|
|
1534
|
-
}
|
|
1535
|
-
const query = `g.addV(vertexLabel).property('id', vid)${propParts.join("")}`;
|
|
1536
|
-
await conn.submit(query, bindings);
|
|
1790
|
+
await conn.submit(INSERT_ENTITY_QUERY, bindings);
|
|
1537
1791
|
}
|
|
1538
1792
|
async function insertRelationship(conn, repositoryId, rel) {
|
|
1539
|
-
const props = relationshipToGremlinProps(repositoryId, rel);
|
|
1540
1793
|
const bindings = {
|
|
1794
|
+
rid: repositoryId,
|
|
1541
1795
|
relId: rel.id,
|
|
1542
1796
|
srcId: rel.sourceEntityId,
|
|
1543
1797
|
tgtId: rel.targetEntityId,
|
|
1544
|
-
|
|
1545
|
-
|
|
1798
|
+
edgeLabel: rel.relationshipType,
|
|
1799
|
+
...relationshipToLadderBindings(rel)
|
|
1546
1800
|
};
|
|
1547
|
-
|
|
1548
|
-
let idx = 0;
|
|
1549
|
-
for (const [key, value] of Object.entries(props)) {
|
|
1550
|
-
const paramName = `p${idx++}`;
|
|
1551
|
-
bindings[paramName] = value;
|
|
1552
|
-
propParts.push(`.property('${key}', ${paramName})`);
|
|
1553
|
-
}
|
|
1554
|
-
const query = `g.V().has('repositoryId', rid).has('id', srcId).has('entityType').addE(edgeLabel).to(g.V().has('repositoryId', rid).has('id', tgtId).has('entityType')).property('id', relId)${propParts.join("")}`;
|
|
1555
|
-
await conn.submit(query, bindings);
|
|
1801
|
+
await conn.submit(INSERT_RELATIONSHIP_QUERY, bindings);
|
|
1556
1802
|
}
|
|
1557
1803
|
async function upsertEntity(conn, repositoryId, entity) {
|
|
1558
|
-
const props = entityToGremlinProps(repositoryId, entity);
|
|
1559
1804
|
const bindings = {
|
|
1560
|
-
vid: entity.id,
|
|
1561
1805
|
rid: repositoryId,
|
|
1562
|
-
|
|
1806
|
+
vid: entity.id,
|
|
1807
|
+
vertexLabel: entity.entityType,
|
|
1808
|
+
...entityToLadderBindings(entity)
|
|
1563
1809
|
};
|
|
1564
|
-
|
|
1565
|
-
let idx = 0;
|
|
1566
|
-
for (const [key, value] of Object.entries(props)) {
|
|
1567
|
-
const paramName = `p${idx++}`;
|
|
1568
|
-
bindings[paramName] = value;
|
|
1569
|
-
propParts.push(`.property('${key}', ${paramName})`);
|
|
1570
|
-
}
|
|
1571
|
-
const query = `g.V().has('repositoryId', rid).has('id', vid).has('entityType').fold().coalesce(unfold()${propParts.join("")}, addV(vertexLabel).property('id', vid)${propParts.join("")})`;
|
|
1572
|
-
await conn.submit(query, bindings);
|
|
1810
|
+
await conn.submit(UPSERT_ENTITY_QUERY, bindings);
|
|
1573
1811
|
}
|
|
1574
1812
|
async function upsertRelationship(conn, repositoryId, rel) {
|
|
1575
|
-
const props = relationshipToGremlinProps(repositoryId, rel);
|
|
1576
1813
|
const bindings = {
|
|
1814
|
+
rid: repositoryId,
|
|
1577
1815
|
relId: rel.id,
|
|
1578
1816
|
srcId: rel.sourceEntityId,
|
|
1579
1817
|
tgtId: rel.targetEntityId,
|
|
1580
|
-
|
|
1581
|
-
|
|
1818
|
+
edgeLabel: rel.relationshipType,
|
|
1819
|
+
...relationshipToLadderBindings(rel)
|
|
1582
1820
|
};
|
|
1583
|
-
|
|
1584
|
-
let idx = 0;
|
|
1585
|
-
for (const [key, value] of Object.entries(props)) {
|
|
1586
|
-
const paramName = `p${idx++}`;
|
|
1587
|
-
bindings[paramName] = value;
|
|
1588
|
-
propParts.push(`.property('${key}', ${paramName})`);
|
|
1589
|
-
}
|
|
1590
|
-
const createEdge = `g.V().has('repositoryId', rid).has('id', srcId).has('entityType').addE(edgeLabel).to(g.V().has('repositoryId', rid).has('id', tgtId).has('entityType')).property('id', relId)${propParts.join("")}`;
|
|
1591
|
-
const query = `g.E().has('repositoryId', rid).has('id', relId).fold().coalesce(unfold()${propParts.join("")}, ${createEdge})`;
|
|
1592
|
-
await conn.submit(query, bindings);
|
|
1821
|
+
await conn.submit(UPSERT_RELATIONSHIP_QUERY, bindings);
|
|
1593
1822
|
}
|
|
1594
1823
|
|
|
1595
1824
|
// src/CosmosDbProvider.ts
|
|
1596
1825
|
var PROVIDER_NAME = "cosmosdb";
|
|
1597
1826
|
var SCHEMA_VERSION = 1;
|
|
1598
1827
|
var META_VERTEX_ID = "_meta:schema";
|
|
1828
|
+
var GUARDED_INDEX_PATHS = [
|
|
1829
|
+
"/entityLabel",
|
|
1830
|
+
"/slug",
|
|
1831
|
+
"/summary",
|
|
1832
|
+
"/entityType",
|
|
1833
|
+
"/properties",
|
|
1834
|
+
"/repositoryId"
|
|
1835
|
+
];
|
|
1836
|
+
var VOCABULARY_CACHE_TTL_MS = 6e4;
|
|
1599
1837
|
var CosmosDbProvider = class {
|
|
1600
1838
|
conn;
|
|
1839
|
+
docClient;
|
|
1601
1840
|
config;
|
|
1602
|
-
compiler = new
|
|
1841
|
+
compiler = new import_deep_memory5.GremlinCompiler();
|
|
1603
1842
|
reportUsage;
|
|
1843
|
+
/**
|
|
1844
|
+
* Per-process vocabulary cache, keyed by repositoryId. Read lazily by
|
|
1845
|
+
* `traverseImpl` via `getVocabularyCached`; invalidated on `saveVocabulary`.
|
|
1846
|
+
* Each provider instance owns its own cache so isolated test providers do
|
|
1847
|
+
* not share state.
|
|
1848
|
+
*/
|
|
1849
|
+
vocabularyCache = /* @__PURE__ */ new Map();
|
|
1604
1850
|
constructor(config) {
|
|
1605
1851
|
this.config = config;
|
|
1606
|
-
this.reportUsage = (0,
|
|
1852
|
+
this.reportUsage = (0, import_deep_memory5.createSafeSink)(config.reportUsage);
|
|
1607
1853
|
this.conn = new CosmosDbConnection({
|
|
1608
1854
|
endpoint: config.endpoint,
|
|
1609
1855
|
key: config.key,
|
|
@@ -1613,6 +1859,15 @@ var CosmosDbProvider = class {
|
|
|
1613
1859
|
defaultTimeoutMs: config.defaultTimeoutMs,
|
|
1614
1860
|
rejectUnauthorized: config.rejectUnauthorized
|
|
1615
1861
|
});
|
|
1862
|
+
this.docClient = new CosmosDocumentClient({
|
|
1863
|
+
restEndpoint: this.getRestEndpoint(),
|
|
1864
|
+
key: config.key,
|
|
1865
|
+
database: config.database,
|
|
1866
|
+
container: config.container,
|
|
1867
|
+
rejectUnauthorized: config.rejectUnauthorized ?? true,
|
|
1868
|
+
maxRetries: config.maxRetries,
|
|
1869
|
+
defaultTimeoutMs: config.defaultTimeoutMs
|
|
1870
|
+
});
|
|
1616
1871
|
}
|
|
1617
1872
|
/**
|
|
1618
1873
|
* Run the given operation inside an async-local usage scope. While the
|
|
@@ -1657,8 +1912,8 @@ var CosmosDbProvider = class {
|
|
|
1657
1912
|
* ever reaching query construction.
|
|
1658
1913
|
*/
|
|
1659
1914
|
assertValidRepositoryId(repositoryId) {
|
|
1660
|
-
if (!(0,
|
|
1661
|
-
throw new
|
|
1915
|
+
if (!(0, import_deep_memory5.isValidUuid)(repositoryId)) {
|
|
1916
|
+
throw new import_deep_memory5.InvalidInputError(
|
|
1662
1917
|
"repositoryId",
|
|
1663
1918
|
`repositoryId must be a valid v4 UUID; got '${repositoryId}'`
|
|
1664
1919
|
);
|
|
@@ -1706,24 +1961,18 @@ var CosmosDbProvider = class {
|
|
|
1706
1961
|
await this.conn.close();
|
|
1707
1962
|
await this.conn.connect();
|
|
1708
1963
|
const existing = await this.conn.submit(
|
|
1709
|
-
"g.V().
|
|
1964
|
+
"g.V().hasId(metaId).hasLabel('_meta').valueMap(true)",
|
|
1710
1965
|
{ metaId: META_VERTEX_ID }
|
|
1711
1966
|
);
|
|
1712
1967
|
if (existing.items.length > 0) {
|
|
1713
1968
|
const props = existing.items[0];
|
|
1714
1969
|
const version = Number(unwrapGremlinValue(props["schemaVersion"]) ?? 0);
|
|
1715
|
-
if (version
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
schemaVersion: SCHEMA_VERSION
|
|
1721
|
-
};
|
|
1970
|
+
if (version < SCHEMA_VERSION || databaseCreated || schemaCreated) {
|
|
1971
|
+
await this.conn.submit(
|
|
1972
|
+
"g.V().hasId(metaId).hasLabel('_meta').property('schemaVersion', ver)",
|
|
1973
|
+
{ metaId: META_VERTEX_ID, ver: SCHEMA_VERSION }
|
|
1974
|
+
);
|
|
1722
1975
|
}
|
|
1723
|
-
await this.conn.submit(
|
|
1724
|
-
"g.V().has('id', metaId).hasLabel('_meta').property('schemaVersion', ver)",
|
|
1725
|
-
{ metaId: META_VERTEX_ID, ver: SCHEMA_VERSION }
|
|
1726
|
-
);
|
|
1727
1976
|
} else {
|
|
1728
1977
|
await this.conn.submit(
|
|
1729
1978
|
"g.addV('_meta').property('id', metaId).property('repositoryId', pk).property('schemaVersion', ver)",
|
|
@@ -1731,6 +1980,8 @@ var CosmosDbProvider = class {
|
|
|
1731
1980
|
);
|
|
1732
1981
|
schemaCreated = true;
|
|
1733
1982
|
}
|
|
1983
|
+
await ensureRepositoryIndex(this.conn);
|
|
1984
|
+
await this.runIndexingPolicyDiagnostic();
|
|
1734
1985
|
return {
|
|
1735
1986
|
databaseCreated,
|
|
1736
1987
|
schemaCreated,
|
|
@@ -1738,12 +1989,57 @@ var CosmosDbProvider = class {
|
|
|
1738
1989
|
schemaVersion: SCHEMA_VERSION
|
|
1739
1990
|
};
|
|
1740
1991
|
} catch (err) {
|
|
1741
|
-
throw new
|
|
1992
|
+
throw new import_deep_memory5.ProviderError(
|
|
1742
1993
|
`Failed to ensure CosmosDB schema: ${err instanceof Error ? err.message : String(err)}`,
|
|
1743
1994
|
"Verify the CosmosDB REST endpoint is accessible and the Gremlin endpoint is reachable."
|
|
1744
1995
|
);
|
|
1745
1996
|
}
|
|
1746
1997
|
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Step E — verify the container's indexing policy covers every path that
|
|
2000
|
+
* the SQL `findEntities` rewrite hits. The probe (2026-05-26) confirmed
|
|
2001
|
+
* code-managed containers get the Cosmos default policy (everything
|
|
2002
|
+
* indexed). This guard is for operator-facing protection: externally
|
|
2003
|
+
* provisioned containers (ARM/Bicep) can have `excludedPaths` set, which
|
|
2004
|
+
* would force `findEntities` to scan rather than index-lookup.
|
|
2005
|
+
*
|
|
2006
|
+
* Single GET against the colls resource, minimal RU. Never fails
|
|
2007
|
+
* `ensureSchema` — a diagnostic must not break provisioning.
|
|
2008
|
+
*/
|
|
2009
|
+
async runIndexingPolicyDiagnostic() {
|
|
2010
|
+
let policy;
|
|
2011
|
+
try {
|
|
2012
|
+
const props = await this.docClient.getContainerProperties();
|
|
2013
|
+
policy = props.indexingPolicy;
|
|
2014
|
+
} catch (err) {
|
|
2015
|
+
console.warn(
|
|
2016
|
+
`[CosmosDbProvider] could not verify indexing policy on container ${this.config.container}: ${err instanceof Error ? err.message : String(err)}`
|
|
2017
|
+
);
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
const offending = [];
|
|
2021
|
+
for (const entry of policy.excludedPaths) {
|
|
2022
|
+
const prefix = entry.path.replace(/\/[?*]$/, "");
|
|
2023
|
+
if (prefix === "" || prefix === "/") {
|
|
2024
|
+
for (const guard of GUARDED_INDEX_PATHS) {
|
|
2025
|
+
offending.push(`${guard} (covered by ${entry.path})`);
|
|
2026
|
+
}
|
|
2027
|
+
break;
|
|
2028
|
+
}
|
|
2029
|
+
for (const guard of GUARDED_INDEX_PATHS) {
|
|
2030
|
+
if (prefix === guard || guard.startsWith(prefix + "/")) {
|
|
2031
|
+
offending.push(`${guard} (excluded by ${entry.path})`);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
if (offending.length > 0) {
|
|
2036
|
+
console.warn(
|
|
2037
|
+
`[CosmosDbProvider] Container ${this.config.container} has indexing-policy excludedPaths that cover paths used by findEntities. The query will fall back to scan and may exceed RU budgets:
|
|
2038
|
+
${offending.join("\n ")}
|
|
2039
|
+
Verify the ARM/Bicep template that provisioned the container.`
|
|
2040
|
+
);
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
1747
2043
|
/** Derive the REST endpoint from config — either explicit or from the Gremlin endpoint host. */
|
|
1748
2044
|
getRestEndpoint() {
|
|
1749
2045
|
if (this.config.restEndpoint) return this.config.restEndpoint.replace(/\/+$/, "");
|
|
@@ -1815,13 +2111,38 @@ var CosmosDbProvider = class {
|
|
|
1815
2111
|
() => getVocabulary(this.conn, repositoryId)
|
|
1816
2112
|
);
|
|
1817
2113
|
}
|
|
2114
|
+
/**
|
|
2115
|
+
* Cached vocabulary read used by traversal compilation. The vocabulary is
|
|
2116
|
+
* compile-time context for the GremlinCompiler — it changes on the order of
|
|
2117
|
+
* once per session, but the traversal hot path pays one round-trip per call
|
|
2118
|
+
* to fetch it. The cache flips that to one round-trip per TTL window.
|
|
2119
|
+
*
|
|
2120
|
+
* Reads inside an active usage scope are still recorded if a fetch happens
|
|
2121
|
+
* (cache miss); cache hits emit no round-trip and therefore no usage entry.
|
|
2122
|
+
*/
|
|
2123
|
+
async getVocabularyCached(repositoryId) {
|
|
2124
|
+
const now = Date.now();
|
|
2125
|
+
const cached = this.vocabularyCache.get(repositoryId);
|
|
2126
|
+
if (cached && cached.expiresAt > now) {
|
|
2127
|
+
return cached.vocab;
|
|
2128
|
+
}
|
|
2129
|
+
const vocab = await getVocabulary(this.conn, repositoryId);
|
|
2130
|
+
this.vocabularyCache.set(repositoryId, {
|
|
2131
|
+
vocab,
|
|
2132
|
+
expiresAt: now + VOCABULARY_CACHE_TTL_MS
|
|
2133
|
+
});
|
|
2134
|
+
return vocab;
|
|
2135
|
+
}
|
|
2136
|
+
/** Drop the cache entry for a repository — call after any vocabulary write. */
|
|
2137
|
+
invalidateVocabularyCache(repositoryId) {
|
|
2138
|
+
this.vocabularyCache.delete(repositoryId);
|
|
2139
|
+
}
|
|
1818
2140
|
async saveVocabulary(repositoryId, vocabulary) {
|
|
1819
2141
|
this.assertValidRepositoryId(repositoryId);
|
|
1820
|
-
return this.track(
|
|
1821
|
-
|
|
1822
|
-
repositoryId
|
|
1823
|
-
|
|
1824
|
-
);
|
|
2142
|
+
return this.track("saveVocabulary", repositoryId, async () => {
|
|
2143
|
+
await saveVocabulary(this.conn, repositoryId, vocabulary);
|
|
2144
|
+
this.invalidateVocabularyCache(repositoryId);
|
|
2145
|
+
});
|
|
1825
2146
|
}
|
|
1826
2147
|
async getVocabularyChangeLog(repositoryId, options) {
|
|
1827
2148
|
this.assertValidRepositoryId(repositoryId);
|
|
@@ -1840,28 +2161,28 @@ var CosmosDbProvider = class {
|
|
|
1840
2161
|
() => createEntity(this.conn, repositoryId, entity)
|
|
1841
2162
|
);
|
|
1842
2163
|
}
|
|
1843
|
-
async getEntity(repositoryId, entityId) {
|
|
2164
|
+
async getEntity(repositoryId, entityId, options) {
|
|
1844
2165
|
this.assertValidRepositoryId(repositoryId);
|
|
1845
2166
|
return this.track(
|
|
1846
2167
|
"getEntity",
|
|
1847
2168
|
repositoryId,
|
|
1848
|
-
() => getEntity(this.conn, repositoryId, entityId)
|
|
2169
|
+
() => getEntity(this.conn, repositoryId, entityId, options)
|
|
1849
2170
|
);
|
|
1850
2171
|
}
|
|
1851
|
-
async getEntityBySlug(repositoryId, slug) {
|
|
2172
|
+
async getEntityBySlug(repositoryId, slug, options) {
|
|
1852
2173
|
this.assertValidRepositoryId(repositoryId);
|
|
1853
2174
|
return this.track(
|
|
1854
2175
|
"getEntityBySlug",
|
|
1855
2176
|
repositoryId,
|
|
1856
|
-
() => getEntityBySlug(this.conn, repositoryId, slug)
|
|
2177
|
+
() => getEntityBySlug(this.conn, repositoryId, slug, options)
|
|
1857
2178
|
);
|
|
1858
2179
|
}
|
|
1859
|
-
async getEntities(repositoryId, entityIds) {
|
|
2180
|
+
async getEntities(repositoryId, entityIds, options) {
|
|
1860
2181
|
this.assertValidRepositoryId(repositoryId);
|
|
1861
2182
|
return this.track(
|
|
1862
2183
|
"getEntities",
|
|
1863
2184
|
repositoryId,
|
|
1864
|
-
() => getEntities(this.conn, repositoryId, entityIds)
|
|
2185
|
+
() => getEntities(this.conn, repositoryId, entityIds, options)
|
|
1865
2186
|
);
|
|
1866
2187
|
}
|
|
1867
2188
|
async updateEntity(repositoryId, entityId, updates) {
|
|
@@ -1885,6 +2206,19 @@ var CosmosDbProvider = class {
|
|
|
1885
2206
|
this.assertValidRepositoryId(repositoryId);
|
|
1886
2207
|
return this.track("deleteEntities", repositoryId, () => this.deleteEntitiesImpl(repositoryId, ids));
|
|
1887
2208
|
}
|
|
2209
|
+
/**
|
|
2210
|
+
* Single round-trip bulk-delete via the aggregate-side-effect pattern:
|
|
2211
|
+
* collapses the per-chunk existence-check + drop into one Gremlin call.
|
|
2212
|
+
*
|
|
2213
|
+
* g.V()...hasId(within(...)).has('entityType')
|
|
2214
|
+
* .aggregate('found').by('id') // collects the ids that match
|
|
2215
|
+
* .drop() // drops the vertices (and cascaded edges)
|
|
2216
|
+
* .cap('found') // emits the bucket as the single result
|
|
2217
|
+
*
|
|
2218
|
+
* The bucket is always emitted as a single list item — empty when nothing
|
|
2219
|
+
* matched (probe-verified 2026-05-25, local-tests/phase7-shape-probe.mjs).
|
|
2220
|
+
* `notFound` is derived client-side as the set difference.
|
|
2221
|
+
*/
|
|
1888
2222
|
async deleteEntitiesImpl(repositoryId, ids) {
|
|
1889
2223
|
const deleted = [];
|
|
1890
2224
|
const CHUNK = 100;
|
|
@@ -1897,26 +2231,14 @@ var CosmosDbProvider = class {
|
|
|
1897
2231
|
bindings[p] = chunk[j];
|
|
1898
2232
|
idParams.push(p);
|
|
1899
2233
|
}
|
|
1900
|
-
const withinExpr = `
|
|
1901
|
-
const
|
|
1902
|
-
`g.V().has('repositoryId', rid).
|
|
2234
|
+
const withinExpr = `within(${idParams.join(", ")})`;
|
|
2235
|
+
const result = await this.conn.submit(
|
|
2236
|
+
`g.V().has('repositoryId', rid).hasId(${withinExpr}).has('entityType').aggregate('found').by('id').drop().cap('found')`,
|
|
1903
2237
|
bindings
|
|
1904
2238
|
);
|
|
1905
|
-
const
|
|
1906
|
-
if (
|
|
1907
|
-
|
|
1908
|
-
const dropIdParams = [];
|
|
1909
|
-
for (let j = 0; j < found.length; j++) {
|
|
1910
|
-
const p = `did${j}`;
|
|
1911
|
-
dropBindings[p] = found[j];
|
|
1912
|
-
dropIdParams.push(p);
|
|
1913
|
-
}
|
|
1914
|
-
const dropWithinExpr = `P.within(${dropIdParams.join(", ")})`;
|
|
1915
|
-
await this.conn.submit(
|
|
1916
|
-
`g.V().has('repositoryId', rid).has('id', ${dropWithinExpr}).has('entityType').drop()`,
|
|
1917
|
-
dropBindings
|
|
1918
|
-
);
|
|
1919
|
-
deleted.push(...found);
|
|
2239
|
+
const bucket = result.items[0];
|
|
2240
|
+
if (Array.isArray(bucket)) {
|
|
2241
|
+
deleted.push(...bucket);
|
|
1920
2242
|
}
|
|
1921
2243
|
}
|
|
1922
2244
|
const deletedSet = new Set(deleted);
|
|
@@ -1930,12 +2252,12 @@ var CosmosDbProvider = class {
|
|
|
1930
2252
|
() => deleteEntitiesByType(this.conn, repositoryId, entityType)
|
|
1931
2253
|
);
|
|
1932
2254
|
}
|
|
1933
|
-
async findEntities(repositoryId, query) {
|
|
2255
|
+
async findEntities(repositoryId, query, options) {
|
|
1934
2256
|
this.assertValidRepositoryId(repositoryId);
|
|
1935
2257
|
return this.track(
|
|
1936
2258
|
"findEntities",
|
|
1937
2259
|
repositoryId,
|
|
1938
|
-
() => findEntities(this.
|
|
2260
|
+
() => findEntities(this.docClient, repositoryId, query, options)
|
|
1939
2261
|
);
|
|
1940
2262
|
}
|
|
1941
2263
|
// ─── Relationships ─────────────────────────────────────────────────
|
|
@@ -1976,6 +2298,20 @@ var CosmosDbProvider = class {
|
|
|
1976
2298
|
this.assertValidRepositoryId(repositoryId);
|
|
1977
2299
|
return this.track("deleteRelationships", repositoryId, () => this.deleteRelationshipsImpl(repositoryId, ids));
|
|
1978
2300
|
}
|
|
2301
|
+
/**
|
|
2302
|
+
* Single round-trip bulk relationship delete via the aggregate-side-effect
|
|
2303
|
+
* pattern: collapses the per-chunk existence-check + drop into one Gremlin
|
|
2304
|
+
* call. Gremlin drop on edges is routed by the engine and the bucket gives
|
|
2305
|
+
* back the exact ids that were actually dropped, so `notFound` can be
|
|
2306
|
+
* derived client-side.
|
|
2307
|
+
*
|
|
2308
|
+
* Source-id partition routing is not exposed on this method (the public
|
|
2309
|
+
* surface accepts only edge ids), so the lookup may fan out across
|
|
2310
|
+
* partitions — see [docs/cosmosdb-gremlin-compatibility.md §`g.E().has`
|
|
2311
|
+
* doesn't always push partition down]. Callers that already hold a
|
|
2312
|
+
* StoredRelationship and want partition-scoped routing should add a
|
|
2313
|
+
* dedicated method when the need is concrete.
|
|
2314
|
+
*/
|
|
1979
2315
|
async deleteRelationshipsImpl(repositoryId, ids) {
|
|
1980
2316
|
const deleted = [];
|
|
1981
2317
|
const CHUNK = 100;
|
|
@@ -1988,23 +2324,14 @@ var CosmosDbProvider = class {
|
|
|
1988
2324
|
bindings[p] = chunk[j];
|
|
1989
2325
|
idParams.push(p);
|
|
1990
2326
|
}
|
|
1991
|
-
const withinExpr = `
|
|
1992
|
-
const
|
|
1993
|
-
`g.E().has('repositoryId', rid).
|
|
2327
|
+
const withinExpr = `within(${idParams.join(", ")})`;
|
|
2328
|
+
const result = await this.conn.submit(
|
|
2329
|
+
`g.E().has('repositoryId', rid).hasId(${withinExpr}).aggregate('found').by('id').drop().cap('found')`,
|
|
1994
2330
|
bindings
|
|
1995
2331
|
);
|
|
1996
|
-
const
|
|
1997
|
-
if (
|
|
1998
|
-
|
|
1999
|
-
this.getRestEndpoint(),
|
|
2000
|
-
this.config.key,
|
|
2001
|
-
this.config.database,
|
|
2002
|
-
this.config.container,
|
|
2003
|
-
repositoryId,
|
|
2004
|
-
found,
|
|
2005
|
-
this.config.rejectUnauthorized ?? true
|
|
2006
|
-
);
|
|
2007
|
-
deleted.push(...found);
|
|
2332
|
+
const bucket = result.items[0];
|
|
2333
|
+
if (Array.isArray(bucket)) {
|
|
2334
|
+
deleted.push(...bucket);
|
|
2008
2335
|
}
|
|
2009
2336
|
}
|
|
2010
2337
|
const deletedSet = new Set(deleted);
|
|
@@ -2024,17 +2351,213 @@ var CosmosDbProvider = class {
|
|
|
2024
2351
|
return this.track(
|
|
2025
2352
|
"exploreNeighborhood",
|
|
2026
2353
|
repositoryId,
|
|
2027
|
-
() =>
|
|
2354
|
+
() => this.exploreNeighborhoodImpl(repositoryId, entityId, options)
|
|
2028
2355
|
);
|
|
2029
2356
|
}
|
|
2357
|
+
/**
|
|
2358
|
+
* Compiler-model implementation of exploreNeighborhood.
|
|
2359
|
+
*
|
|
2360
|
+
* Strategy: for each depth `d` from 1 to `options.depth`, build a
|
|
2361
|
+
* cumulative `TraversalSpec` with `d` steps and `returnMode: 'all'`, run
|
|
2362
|
+
* it through {@link executeTraversal}, then walk one BFS layer client-side
|
|
2363
|
+
* from the previous frontier using the returned edges.
|
|
2364
|
+
*
|
|
2365
|
+
* The server-side step direction is fixed to `'both'` (catches every edge
|
|
2366
|
+
* in either direction). The directional + bidirectional filter is applied
|
|
2367
|
+
* client-side per hop — i.e. `direction: 'outbound'` includes inbound edges
|
|
2368
|
+
* where `bidirectional` is true. The CosmosDB Gremlin compiler does not
|
|
2369
|
+
* natively express the `union(outE, inE.has(bidirectional))` shape that
|
|
2370
|
+
* would push this filter server-side, so it stays client-side; the
|
|
2371
|
+
* observable contract (which edges count toward "outbound" given
|
|
2372
|
+
* bidirectionality) is preserved.
|
|
2373
|
+
*
|
|
2374
|
+
* Round-trips per call: `options.depth` (one per layer). The previous BFS
|
|
2375
|
+
* was `1 + fanout + fanout² + …` round-trips for the same depth.
|
|
2376
|
+
*/
|
|
2377
|
+
async exploreNeighborhoodImpl(repositoryId, entityId, options) {
|
|
2378
|
+
const layers = [];
|
|
2379
|
+
const visited = /* @__PURE__ */ new Set([entityId]);
|
|
2380
|
+
let frontier = /* @__PURE__ */ new Set([entityId]);
|
|
2381
|
+
for (let d = 1; d <= options.depth; d++) {
|
|
2382
|
+
if (frontier.size === 0) break;
|
|
2383
|
+
const spec = {
|
|
2384
|
+
start: { entityId },
|
|
2385
|
+
steps: buildExploreSteps(d, options),
|
|
2386
|
+
returnMode: "all",
|
|
2387
|
+
// Each round-trip fetches the cumulative subgraph at depth `d` and
|
|
2388
|
+
// we reconstruct layers client-side. The result can include every
|
|
2389
|
+
// edge and vertex reachable in ≤d hops in either direction; size
|
|
2390
|
+
// the limit generously.
|
|
2391
|
+
limit: 1e4,
|
|
2392
|
+
// Use 'full' + includeProvenance so executeTraversal returns full
|
|
2393
|
+
// StoredEntity rows (StorageNeighborhood embeds StoredEntity).
|
|
2394
|
+
detailLevel: "full",
|
|
2395
|
+
includeProvenance: true
|
|
2396
|
+
};
|
|
2397
|
+
const raw = await this.executeTraversal(repositoryId, spec);
|
|
2398
|
+
const edgesByVertex = /* @__PURE__ */ new Map();
|
|
2399
|
+
for (const rel of raw.allRelationships) {
|
|
2400
|
+
const a = edgesByVertex.get(rel.sourceEntityId);
|
|
2401
|
+
if (a) a.push(rel);
|
|
2402
|
+
else edgesByVertex.set(rel.sourceEntityId, [rel]);
|
|
2403
|
+
const b = edgesByVertex.get(rel.targetEntityId);
|
|
2404
|
+
if (b) b.push(rel);
|
|
2405
|
+
else edgesByVertex.set(rel.targetEntityId, [rel]);
|
|
2406
|
+
}
|
|
2407
|
+
const layer = {};
|
|
2408
|
+
const nextFrontier = /* @__PURE__ */ new Set();
|
|
2409
|
+
const layerEdgeSeen = /* @__PURE__ */ new Set();
|
|
2410
|
+
for (const fv of frontier) {
|
|
2411
|
+
const incident = edgesByVertex.get(fv) ?? [];
|
|
2412
|
+
for (const rel of incident) {
|
|
2413
|
+
const isSource = rel.sourceEntityId === fv;
|
|
2414
|
+
const isTarget = rel.targetEntityId === fv;
|
|
2415
|
+
let matchesDirection = false;
|
|
2416
|
+
let connectedId;
|
|
2417
|
+
if (isSource && (options.direction === "outbound" || options.direction === "both")) {
|
|
2418
|
+
matchesDirection = true;
|
|
2419
|
+
connectedId = rel.targetEntityId;
|
|
2420
|
+
} else if (isTarget && (options.direction === "inbound" || options.direction === "both")) {
|
|
2421
|
+
matchesDirection = true;
|
|
2422
|
+
connectedId = rel.sourceEntityId;
|
|
2423
|
+
} else if (rel.bidirectional) {
|
|
2424
|
+
if (isSource && options.direction === "inbound") {
|
|
2425
|
+
matchesDirection = true;
|
|
2426
|
+
connectedId = rel.targetEntityId;
|
|
2427
|
+
} else if (isTarget && options.direction === "outbound") {
|
|
2428
|
+
matchesDirection = true;
|
|
2429
|
+
connectedId = rel.sourceEntityId;
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
if (!matchesDirection || !connectedId) continue;
|
|
2433
|
+
if (visited.has(connectedId)) continue;
|
|
2434
|
+
if (options.relationshipPropertyFilters && options.relationshipPropertyFilters.length > 0) {
|
|
2435
|
+
if (!(0, import_deep_memory5.matchesPropertyFilters)(rel.properties, options.relationshipPropertyFilters)) continue;
|
|
2436
|
+
}
|
|
2437
|
+
const connectedEntity = raw.entityMap.get(connectedId);
|
|
2438
|
+
if (!connectedEntity) continue;
|
|
2439
|
+
if (options.entityTypes && options.entityTypes.length > 0 && !options.entityTypes.includes(connectedEntity.entityType)) {
|
|
2440
|
+
continue;
|
|
2441
|
+
}
|
|
2442
|
+
const edgeKey = `${rel.id}|${fv}->${connectedId}`;
|
|
2443
|
+
if (layerEdgeSeen.has(edgeKey)) continue;
|
|
2444
|
+
layerEdgeSeen.add(edgeKey);
|
|
2445
|
+
const relType = rel.relationshipType;
|
|
2446
|
+
if (!layer[relType]) {
|
|
2447
|
+
layer[relType] = { total: 0, entities: [], relationships: [] };
|
|
2448
|
+
}
|
|
2449
|
+
layer[relType].entities.push(connectedEntity);
|
|
2450
|
+
layer[relType].relationships.push(rel);
|
|
2451
|
+
layer[relType].total = layer[relType].entities.length;
|
|
2452
|
+
nextFrontier.add(connectedId);
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
for (const relType of Object.keys(layer)) {
|
|
2456
|
+
const group = layer[relType];
|
|
2457
|
+
const start = options.offsetPerType;
|
|
2458
|
+
const end = start + options.limitPerType;
|
|
2459
|
+
group.entities = group.entities.slice(start, end);
|
|
2460
|
+
group.relationships = group.relationships.slice(start, end);
|
|
2461
|
+
}
|
|
2462
|
+
if (Object.keys(layer).length > 0) {
|
|
2463
|
+
layers.push(layer);
|
|
2464
|
+
}
|
|
2465
|
+
for (const id of nextFrontier) {
|
|
2466
|
+
visited.add(id);
|
|
2467
|
+
}
|
|
2468
|
+
frontier = nextFrontier;
|
|
2469
|
+
}
|
|
2470
|
+
return { centerId: entityId, layers };
|
|
2471
|
+
}
|
|
2030
2472
|
async findPaths(repositoryId, sourceId, targetId, options) {
|
|
2031
2473
|
this.assertValidRepositoryId(repositoryId);
|
|
2032
2474
|
return this.track(
|
|
2033
2475
|
"findPaths",
|
|
2034
2476
|
repositoryId,
|
|
2035
|
-
() =>
|
|
2477
|
+
() => this.findPathsImpl(repositoryId, sourceId, targetId, options)
|
|
2036
2478
|
);
|
|
2037
2479
|
}
|
|
2480
|
+
/**
|
|
2481
|
+
* Compiler-model implementation of findPaths.
|
|
2482
|
+
*
|
|
2483
|
+
* Strategy: build a `TraversalSpec` with a single `'both'` direction step
|
|
2484
|
+
* in `repeat()` mode with `emitIntermediates: true` and `returnMode:
|
|
2485
|
+
* 'path'`, run it once. The compiler always emits `.simplePath()` in path
|
|
2486
|
+
* mode for cycle prevention, producing
|
|
2487
|
+
* `.emit().repeat(bothE().otherV()).times(maxDepth).simplePath()
|
|
2488
|
+
* .range(...).path().by(<v>).by(<e>)`, which yields paths of every length
|
|
2489
|
+
* from 0 (the start vertex alone) to `maxDepth`. Live-probed against the
|
|
2490
|
+
* Cosmos emulator 2026-05-25 — see local-tests/phase4-repeat-emit-probe.mjs.
|
|
2491
|
+
*
|
|
2492
|
+
* The pre-Phase-4 Cosmos BFS in (now-deleted) `packages/storage-cosmosdb/
|
|
2493
|
+
* src/queries/traversal.ts` traversed edges with unconditional `bothE()`
|
|
2494
|
+
* regardless of the `bidirectional` flag (path discovery is reachability,
|
|
2495
|
+
* not semantic direction). Mirror that here by using step direction
|
|
2496
|
+
* `'both'` and not applying any direction filter. The plan's §6
|
|
2497
|
+
* observable-outputs rule requires preserving this.
|
|
2498
|
+
*
|
|
2499
|
+
* One round-trip total. The previous BFS was up to `1 + fanout + fanout² + …`.
|
|
2500
|
+
*/
|
|
2501
|
+
async findPathsImpl(repositoryId, sourceId, targetId, options) {
|
|
2502
|
+
if (sourceId === targetId) {
|
|
2503
|
+
return { paths: [{ entityIds: [sourceId], relationshipIds: [] }], totalPaths: 1 };
|
|
2504
|
+
}
|
|
2505
|
+
const step = {
|
|
2506
|
+
direction: "both",
|
|
2507
|
+
repeat: { maxDepth: options.maxDepth, emitIntermediates: true }
|
|
2508
|
+
};
|
|
2509
|
+
if (options.relationshipTypes && options.relationshipTypes.length > 0) {
|
|
2510
|
+
step.relationshipTypes = options.relationshipTypes;
|
|
2511
|
+
}
|
|
2512
|
+
if (options.relationshipPropertyFilters && options.relationshipPropertyFilters.length > 0) {
|
|
2513
|
+
step.relationshipFilter = options.relationshipPropertyFilters;
|
|
2514
|
+
}
|
|
2515
|
+
const spec = {
|
|
2516
|
+
start: { entityId: sourceId },
|
|
2517
|
+
steps: [step],
|
|
2518
|
+
returnMode: "path",
|
|
2519
|
+
// Cycle prevention comes from the compiler unconditionally emitting
|
|
2520
|
+
// .simplePath() in path mode — replaces the explicit
|
|
2521
|
+
// `state.path.includes(nextId)` guard from the old BFS.
|
|
2522
|
+
// Pull the full pool of paths so we can post-filter to those ending at
|
|
2523
|
+
// targetId, then paginate. The emulator returns paths of every length
|
|
2524
|
+
// 0..maxDepth in one round-trip with the repeat+emit shape; cap
|
|
2525
|
+
// generously to ensure all candidates are inspected.
|
|
2526
|
+
limit: Math.max(options.limit + options.offset, options.limit) * 10,
|
|
2527
|
+
detailLevel: "full",
|
|
2528
|
+
includeProvenance: true
|
|
2529
|
+
};
|
|
2530
|
+
const raw = await this.executeTraversal(repositoryId, spec);
|
|
2531
|
+
const matchingPaths = [];
|
|
2532
|
+
for (const row of raw.pathRows) {
|
|
2533
|
+
const last = row.entityIds[row.entityIds.length - 1];
|
|
2534
|
+
if (last !== targetId) continue;
|
|
2535
|
+
if (options.entityTypes && options.entityTypes.length > 0) {
|
|
2536
|
+
let rejected = false;
|
|
2537
|
+
for (let i = 1; i < row.entityIds.length - 1; i++) {
|
|
2538
|
+
const intermediate = raw.entityMap.get(row.entityIds[i]);
|
|
2539
|
+
if (!intermediate) {
|
|
2540
|
+
rejected = true;
|
|
2541
|
+
break;
|
|
2542
|
+
}
|
|
2543
|
+
if (!options.entityTypes.includes(intermediate.entityType)) {
|
|
2544
|
+
rejected = true;
|
|
2545
|
+
break;
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
if (rejected) continue;
|
|
2549
|
+
}
|
|
2550
|
+
matchingPaths.push({
|
|
2551
|
+
entityIds: [...row.entityIds],
|
|
2552
|
+
relationshipIds: [...row.relationshipIds]
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2555
|
+
const paginated = matchingPaths.slice(options.offset, options.offset + options.limit);
|
|
2556
|
+
return {
|
|
2557
|
+
paths: paginated,
|
|
2558
|
+
totalPaths: matchingPaths.length
|
|
2559
|
+
};
|
|
2560
|
+
}
|
|
2038
2561
|
// ─── Timeline ──────────────────────────────────────────────────────
|
|
2039
2562
|
async getTimeline(repositoryId, entityId, options) {
|
|
2040
2563
|
this.assertValidRepositoryId(repositoryId);
|
|
@@ -2122,79 +2645,205 @@ var CosmosDbProvider = class {
|
|
|
2122
2645
|
}
|
|
2123
2646
|
async traverse(repositoryId, spec) {
|
|
2124
2647
|
this.assertValidRepositoryId(repositoryId);
|
|
2125
|
-
return this.track("traverse", repositoryId, () => this.
|
|
2648
|
+
return this.track("traverse", repositoryId, () => this.traverseInternal(repositoryId, spec));
|
|
2649
|
+
}
|
|
2650
|
+
/**
|
|
2651
|
+
* Compile + execute a TraversalSpec against this repository's partition.
|
|
2652
|
+
*
|
|
2653
|
+
* Internal entrypoint shared by `traverse` (the public surface) and the
|
|
2654
|
+
* compiler-model rewrites of `exploreNeighborhood` / `findPaths`. Does NOT
|
|
2655
|
+
* wrap in `track()` — the outer public method owns its own usage scope, and
|
|
2656
|
+
* inner submits accumulate into that scope (the nested-scope guard in
|
|
2657
|
+
* `track()` keeps nested public calls from emitting duplicate records).
|
|
2658
|
+
*/
|
|
2659
|
+
async traverseInternal(repositoryId, spec) {
|
|
2660
|
+
const raw = await this.executeTraversal(repositoryId, spec);
|
|
2661
|
+
const detailLevel = spec.detailLevel ?? "summary";
|
|
2662
|
+
const projectStoredEntity = (stored) => {
|
|
2663
|
+
const projected = (0, import_deep_memory5.projectEntity)(stored, detailLevel);
|
|
2664
|
+
if (!spec.includeProvenance) {
|
|
2665
|
+
delete projected["provenance"];
|
|
2666
|
+
}
|
|
2667
|
+
return projected;
|
|
2668
|
+
};
|
|
2669
|
+
const projectStoredRelationship = (rel, direction = "outbound") => ({
|
|
2670
|
+
id: rel.id,
|
|
2671
|
+
type: rel.relationshipType,
|
|
2672
|
+
sourceEntityId: rel.sourceEntityId,
|
|
2673
|
+
targetEntityId: rel.targetEntityId,
|
|
2674
|
+
direction,
|
|
2675
|
+
properties: rel.properties
|
|
2676
|
+
});
|
|
2677
|
+
let entities = [];
|
|
2678
|
+
let relationships;
|
|
2679
|
+
let paths;
|
|
2680
|
+
if (spec.returnMode === "terminal") {
|
|
2681
|
+
entities = raw.terminalEntities.map(projectStoredEntity);
|
|
2682
|
+
relationships = void 0;
|
|
2683
|
+
} else if (spec.returnMode === "all") {
|
|
2684
|
+
entities = raw.allEntities.map(projectStoredEntity);
|
|
2685
|
+
relationships = raw.allRelationships.map((r) => projectStoredRelationship(r));
|
|
2686
|
+
} else {
|
|
2687
|
+
paths = raw.pathRows.map((row) => ({
|
|
2688
|
+
length: Math.max(row.entityIds.length - 1, 0),
|
|
2689
|
+
entities: row.entityIds.map((id) => {
|
|
2690
|
+
const stored = raw.entityMap.get(id);
|
|
2691
|
+
if (!stored) {
|
|
2692
|
+
throw new import_deep_memory5.ProviderError(
|
|
2693
|
+
"Unpacking Gremlin path: entity referenced by path is missing from the result",
|
|
2694
|
+
"This indicates a Gremlin response shape mismatch \u2014 inspect compiledQuery."
|
|
2695
|
+
);
|
|
2696
|
+
}
|
|
2697
|
+
return projectStoredEntity(stored);
|
|
2698
|
+
}),
|
|
2699
|
+
relationships: row.relationshipIds.map((id, i) => {
|
|
2700
|
+
const stored = raw.relationshipMap.get(id);
|
|
2701
|
+
if (!stored) {
|
|
2702
|
+
throw new import_deep_memory5.ProviderError(
|
|
2703
|
+
"Unpacking Gremlin path: relationship referenced by path is missing from the result",
|
|
2704
|
+
"This indicates a Gremlin response shape mismatch \u2014 inspect compiledQuery."
|
|
2705
|
+
);
|
|
2706
|
+
}
|
|
2707
|
+
return projectStoredRelationship(stored, row.relationshipDirections[i]);
|
|
2708
|
+
})
|
|
2709
|
+
}));
|
|
2710
|
+
relationships = Array.from(raw.relationshipMap.values()).map(
|
|
2711
|
+
(rel) => projectStoredRelationship(rel, raw.pathRelFirstDirection.get(rel.id) ?? "outbound")
|
|
2712
|
+
);
|
|
2713
|
+
}
|
|
2714
|
+
const limit = spec.limit ?? 50;
|
|
2715
|
+
let total;
|
|
2716
|
+
if (spec.returnMode === "path") {
|
|
2717
|
+
total = paths?.length ?? 0;
|
|
2718
|
+
} else if (spec.returnMode === "all") {
|
|
2719
|
+
total = entities.length + (relationships?.length ?? 0);
|
|
2720
|
+
} else {
|
|
2721
|
+
total = entities.length;
|
|
2722
|
+
}
|
|
2723
|
+
const queryMetadata = {
|
|
2724
|
+
executionTimeMs: raw.executionTimeMs,
|
|
2725
|
+
resourceCost: raw.requestCharge != null ? { units: "RU", value: raw.requestCharge } : void 0,
|
|
2726
|
+
compiledQuery: raw.compiledQuery,
|
|
2727
|
+
compiledQueryLanguage: "gremlin",
|
|
2728
|
+
appliedLimits: {
|
|
2729
|
+
maxResults: limit,
|
|
2730
|
+
maxDepth: spec.steps?.length
|
|
2731
|
+
},
|
|
2732
|
+
truncated: total >= limit,
|
|
2733
|
+
truncationReason: total >= limit ? "result_limit" : void 0
|
|
2734
|
+
};
|
|
2735
|
+
return {
|
|
2736
|
+
entities,
|
|
2737
|
+
relationships,
|
|
2738
|
+
paths,
|
|
2739
|
+
total,
|
|
2740
|
+
returned: total,
|
|
2741
|
+
hasMore: total >= limit,
|
|
2742
|
+
queryMetadata
|
|
2743
|
+
};
|
|
2126
2744
|
}
|
|
2127
|
-
|
|
2745
|
+
/**
|
|
2746
|
+
* Lower-level traversal helper: compiles a spec, submits to Gremlin, and
|
|
2747
|
+
* parses the rows into raw {@link StoredEntity} / {@link StoredRelationship}
|
|
2748
|
+
* objects (no detail-level projection, no provenance stripping). Used by
|
|
2749
|
+
* `traverseInternal` and by the storage-layer rewrites of
|
|
2750
|
+
* `exploreNeighborhood` / `findPaths` that need the full stored shape to
|
|
2751
|
+
* rebuild `StorageNeighborhood` / `StoragePathResult`.
|
|
2752
|
+
*
|
|
2753
|
+
* Returns a discriminated bag — only the fields relevant to `spec.returnMode`
|
|
2754
|
+
* are populated:
|
|
2755
|
+
* - `'terminal'`: `terminalEntities` (in row order, no dedup beyond what the
|
|
2756
|
+
* server emitted).
|
|
2757
|
+
* - `'all'`: `allEntities` and `allRelationships`, already server-deduped.
|
|
2758
|
+
* - `'path'`: `pathRows` plus the `entityMap` / `relationshipMap` lookup
|
|
2759
|
+
* tables and `pathRelFirstDirection` for the first-seen direction per
|
|
2760
|
+
* deduped edge id.
|
|
2761
|
+
*/
|
|
2762
|
+
async executeTraversal(repositoryId, spec) {
|
|
2128
2763
|
const startTime = Date.now();
|
|
2129
|
-
const vocabulary = await this.
|
|
2764
|
+
const vocabulary = await this.getVocabularyCached(repositoryId);
|
|
2130
2765
|
const compiled = this.compiler.compile(spec, vocabulary);
|
|
2131
2766
|
const scopedQuery = compiled.query.replace(
|
|
2132
2767
|
"g.V()",
|
|
2133
2768
|
"g.V().has('repositoryId', pRid)"
|
|
2134
2769
|
);
|
|
2135
2770
|
const scopedParams = { ...compiled.params, pRid: repositoryId };
|
|
2771
|
+
let result;
|
|
2136
2772
|
try {
|
|
2137
|
-
|
|
2138
|
-
const executionTimeMs = Date.now() - startTime;
|
|
2139
|
-
const detailLevel = spec.detailLevel ?? "summary";
|
|
2140
|
-
const entities = [];
|
|
2141
|
-
const relationships = [];
|
|
2142
|
-
const paths = [];
|
|
2143
|
-
if (spec.returnMode === "path") {
|
|
2144
|
-
for (const item of result.items) {
|
|
2145
|
-
const pathData = item;
|
|
2146
|
-
if (pathData.objects) {
|
|
2147
|
-
const pathEntities = [];
|
|
2148
|
-
for (const obj of pathData.objects) {
|
|
2149
|
-
const props = obj;
|
|
2150
|
-
if (props["entityType"]) {
|
|
2151
|
-
const stored = entityFromGremlin(props);
|
|
2152
|
-
pathEntities.push((0, import_deep_memory6.projectEntity)(stored, detailLevel));
|
|
2153
|
-
}
|
|
2154
|
-
}
|
|
2155
|
-
if (pathEntities.length > 0) {
|
|
2156
|
-
paths.push({
|
|
2157
|
-
length: pathEntities.length - 1,
|
|
2158
|
-
entities: pathEntities,
|
|
2159
|
-
relationships: []
|
|
2160
|
-
});
|
|
2161
|
-
}
|
|
2162
|
-
}
|
|
2163
|
-
}
|
|
2164
|
-
} else {
|
|
2165
|
-
for (const item of result.items) {
|
|
2166
|
-
const stored = entityFromGremlin(item);
|
|
2167
|
-
entities.push((0, import_deep_memory6.projectEntity)(stored, detailLevel));
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
|
-
const limit = spec.limit ?? 50;
|
|
2171
|
-
const queryMetadata = {
|
|
2172
|
-
executionTimeMs,
|
|
2173
|
-
resourceCost: result.requestCharge != null ? { units: "RU", value: result.requestCharge } : void 0,
|
|
2174
|
-
compiledQuery: scopedQuery,
|
|
2175
|
-
compiledQueryLanguage: "gremlin",
|
|
2176
|
-
appliedLimits: {
|
|
2177
|
-
maxResults: limit,
|
|
2178
|
-
maxDepth: spec.steps?.length
|
|
2179
|
-
},
|
|
2180
|
-
truncated: entities.length >= limit,
|
|
2181
|
-
truncationReason: entities.length >= limit ? "result_limit" : void 0
|
|
2182
|
-
};
|
|
2183
|
-
return {
|
|
2184
|
-
entities: spec.returnMode === "path" ? [] : entities,
|
|
2185
|
-
relationships: spec.returnMode === "path" || spec.returnMode === "all" ? relationships : void 0,
|
|
2186
|
-
paths: spec.returnMode === "path" ? paths : void 0,
|
|
2187
|
-
total: entities.length + paths.length,
|
|
2188
|
-
returned: entities.length + paths.length,
|
|
2189
|
-
hasMore: entities.length >= limit || paths.length >= limit,
|
|
2190
|
-
queryMetadata
|
|
2191
|
-
};
|
|
2773
|
+
result = await this.conn.submit(scopedQuery, scopedParams);
|
|
2192
2774
|
} catch (err) {
|
|
2193
|
-
throw new
|
|
2775
|
+
throw new import_deep_memory5.ProviderError(
|
|
2194
2776
|
`Gremlin traversal failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2195
2777
|
"Check the traversal spec and ensure the CosmosDB connection is healthy."
|
|
2196
2778
|
);
|
|
2197
2779
|
}
|
|
2780
|
+
const executionTimeMs = Date.now() - startTime;
|
|
2781
|
+
const raw = {
|
|
2782
|
+
terminalEntities: [],
|
|
2783
|
+
allEntities: [],
|
|
2784
|
+
allRelationships: [],
|
|
2785
|
+
pathRows: [],
|
|
2786
|
+
entityMap: /* @__PURE__ */ new Map(),
|
|
2787
|
+
relationshipMap: /* @__PURE__ */ new Map(),
|
|
2788
|
+
pathRelFirstDirection: /* @__PURE__ */ new Map(),
|
|
2789
|
+
executionTimeMs,
|
|
2790
|
+
requestCharge: result.requestCharge,
|
|
2791
|
+
compiledQuery: scopedQuery
|
|
2792
|
+
};
|
|
2793
|
+
if (spec.returnMode === "terminal") {
|
|
2794
|
+
for (const item of result.items) {
|
|
2795
|
+
const stored = entityFromGremlin(item);
|
|
2796
|
+
raw.terminalEntities.push(stored);
|
|
2797
|
+
}
|
|
2798
|
+
} else if (spec.returnMode === "all") {
|
|
2799
|
+
for (const item of result.items) {
|
|
2800
|
+
const props = item;
|
|
2801
|
+
const kind = props["__kind"];
|
|
2802
|
+
if (kind === "v") {
|
|
2803
|
+
const stored = entityFromGremlin(props);
|
|
2804
|
+
raw.allEntities.push(stored);
|
|
2805
|
+
raw.entityMap.set(stored.id, stored);
|
|
2806
|
+
} else if (kind === "e") {
|
|
2807
|
+
const stored = relationshipFromGremlin(props);
|
|
2808
|
+
raw.allRelationships.push(stored);
|
|
2809
|
+
raw.relationshipMap.set(stored.id, stored);
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
} else {
|
|
2813
|
+
for (const item of result.items) {
|
|
2814
|
+
const pathData = item;
|
|
2815
|
+
if (!pathData.objects) continue;
|
|
2816
|
+
const pathEntityIds = [];
|
|
2817
|
+
const pathRelIds = [];
|
|
2818
|
+
const pathRelDirections = [];
|
|
2819
|
+
let lastVertexId = null;
|
|
2820
|
+
for (const obj of pathData.objects) {
|
|
2821
|
+
const props = obj;
|
|
2822
|
+
const kind = props["__kind"];
|
|
2823
|
+
if (kind === "v") {
|
|
2824
|
+
const stored = entityFromGremlin(props);
|
|
2825
|
+
raw.entityMap.set(stored.id, stored);
|
|
2826
|
+
pathEntityIds.push(stored.id);
|
|
2827
|
+
lastVertexId = stored.id;
|
|
2828
|
+
} else if (kind === "e") {
|
|
2829
|
+
const stored = relationshipFromGremlin(props);
|
|
2830
|
+
raw.relationshipMap.set(stored.id, stored);
|
|
2831
|
+
pathRelIds.push(stored.id);
|
|
2832
|
+
const direction = lastVertexId === stored.sourceEntityId ? "outbound" : "inbound";
|
|
2833
|
+
pathRelDirections.push(direction);
|
|
2834
|
+
if (!raw.pathRelFirstDirection.has(stored.id)) {
|
|
2835
|
+
raw.pathRelFirstDirection.set(stored.id, direction);
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
raw.pathRows.push({
|
|
2840
|
+
entityIds: pathEntityIds,
|
|
2841
|
+
relationshipIds: pathRelIds,
|
|
2842
|
+
relationshipDirections: pathRelDirections
|
|
2843
|
+
});
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
return raw;
|
|
2198
2847
|
}
|
|
2199
2848
|
/**
|
|
2200
2849
|
* Execute a raw Gremlin query with caller-supplied bindings.
|
|
@@ -2229,67 +2878,16 @@ function unwrapGremlinValue(val) {
|
|
|
2229
2878
|
if (Array.isArray(val) && val.length > 0) return val[0];
|
|
2230
2879
|
return val;
|
|
2231
2880
|
}
|
|
2232
|
-
function
|
|
2233
|
-
const
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
${date.toLowerCase()}
|
|
2237
|
-
|
|
2238
|
-
`;
|
|
2239
|
-
const keyBuffer = Buffer.from(key, "base64");
|
|
2240
|
-
const hmac = import_node_crypto.default.createHmac("sha256", keyBuffer);
|
|
2241
|
-
hmac.update(payload);
|
|
2242
|
-
const signature = hmac.digest("base64");
|
|
2243
|
-
return encodeURIComponent(`type=master&ver=1.0&sig=${signature}`);
|
|
2244
|
-
}
|
|
2245
|
-
async function cosmosRestPut(restBase, key, urlPath, resourceLink, resourceType, body, rejectUnauthorized) {
|
|
2246
|
-
const date = (/* @__PURE__ */ new Date()).toUTCString();
|
|
2247
|
-
const token = cosmosAuthToken("post", resourceType, resourceLink, date, key);
|
|
2248
|
-
const url = `${restBase}/${urlPath}`;
|
|
2249
|
-
const options = {
|
|
2250
|
-
method: "POST",
|
|
2251
|
-
headers: {
|
|
2252
|
-
"Authorization": token,
|
|
2253
|
-
"x-ms-version": "2018-12-31",
|
|
2254
|
-
"x-ms-date": date,
|
|
2255
|
-
"Content-Type": "application/json"
|
|
2256
|
-
},
|
|
2257
|
-
body: JSON.stringify(body)
|
|
2258
|
-
};
|
|
2259
|
-
if (!rejectUnauthorized) {
|
|
2260
|
-
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
|
|
2261
|
-
}
|
|
2262
|
-
const response = await fetch(url, options);
|
|
2263
|
-
if (response.status === 201) return true;
|
|
2264
|
-
if (response.status === 409) return false;
|
|
2265
|
-
const text = await response.text();
|
|
2266
|
-
throw new Error(`CosmosDB REST ${response.status}: ${text}`);
|
|
2267
|
-
}
|
|
2268
|
-
async function cosmosRestBatchDelete(restBase, key, database, container, partitionKeyValue, ids, rejectUnauthorized) {
|
|
2269
|
-
const resourceLink = `dbs/${database}/colls/${container}`;
|
|
2270
|
-
const date = (/* @__PURE__ */ new Date()).toUTCString();
|
|
2271
|
-
const token = cosmosAuthToken("post", "docs", resourceLink, date, key);
|
|
2272
|
-
if (!rejectUnauthorized) {
|
|
2273
|
-
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
|
|
2881
|
+
function buildExploreSteps(depth, options) {
|
|
2882
|
+
const base = { direction: "both" };
|
|
2883
|
+
if (options.relationshipTypes && options.relationshipTypes.length > 0) {
|
|
2884
|
+
base.relationshipTypes = options.relationshipTypes;
|
|
2274
2885
|
}
|
|
2275
|
-
const
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
headers: {
|
|
2279
|
-
"Authorization": token,
|
|
2280
|
-
"x-ms-version": "2020-07-15",
|
|
2281
|
-
"x-ms-date": date,
|
|
2282
|
-
"Content-Type": "application/json",
|
|
2283
|
-
"x-ms-documentdb-partitionkey": JSON.stringify([partitionKeyValue]),
|
|
2284
|
-
"x-ms-cosmos-is-batch-request": "true",
|
|
2285
|
-
"x-ms-cosmos-batch-atomic": "true"
|
|
2286
|
-
},
|
|
2287
|
-
body: JSON.stringify(ops)
|
|
2288
|
-
});
|
|
2289
|
-
if (response.status !== 200 && response.status !== 207) {
|
|
2290
|
-
const text = await response.text();
|
|
2291
|
-
throw new import_deep_memory6.ProviderError(`CosmosDB batch delete failed (${response.status}): ${text}`);
|
|
2886
|
+
const steps = [];
|
|
2887
|
+
for (let i = 0; i < depth; i++) {
|
|
2888
|
+
steps.push({ ...base });
|
|
2292
2889
|
}
|
|
2890
|
+
return steps;
|
|
2293
2891
|
}
|
|
2294
2892
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2295
2893
|
0 && (module.exports = {
|