@utaba/deep-memory-storage-cosmosdb 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +219 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -1
- package/dist/index.d.ts +40 -1
- package/dist/index.js +220 -23
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.cts
CHANGED
|
@@ -32,6 +32,16 @@ declare class CosmosDbProvider implements StorageProvider, GraphTraversalProvide
|
|
|
32
32
|
private readonly config;
|
|
33
33
|
private readonly compiler;
|
|
34
34
|
constructor(config: CosmosDbProviderConfig);
|
|
35
|
+
/**
|
|
36
|
+
* Reject any repositoryId that is not a valid v4 UUID.
|
|
37
|
+
*
|
|
38
|
+
* Every partition-scoped query filters on repositoryId. Since repositoryId is
|
|
39
|
+
* the CosmosDB partition key, a predicate with a different value would target
|
|
40
|
+
* a different partition. Strict format validation at the provider boundary
|
|
41
|
+
* prevents injection (e.g. strings containing quotes or Gremlin syntax) from
|
|
42
|
+
* ever reaching query construction.
|
|
43
|
+
*/
|
|
44
|
+
private assertValidRepositoryId;
|
|
35
45
|
initialize(): Promise<void>;
|
|
36
46
|
dispose(): Promise<void>;
|
|
37
47
|
ensureSchema(): Promise<EnsureSchemaResult>;
|
|
@@ -56,6 +66,10 @@ declare class CosmosDbProvider implements StorageProvider, GraphTraversalProvide
|
|
|
56
66
|
getEntities(repositoryId: string, entityIds: string[]): Promise<Map<string, StoredEntity>>;
|
|
57
67
|
updateEntity(repositoryId: string, entityId: string, updates: StoredEntityUpdate): Promise<StoredEntity>;
|
|
58
68
|
deleteEntity(repositoryId: string, entityId: string): Promise<void>;
|
|
69
|
+
deleteEntities(repositoryId: string, ids: string[]): Promise<{
|
|
70
|
+
deleted: string[];
|
|
71
|
+
notFound: string[];
|
|
72
|
+
}>;
|
|
59
73
|
deleteEntitiesByType(repositoryId: string, entityType: string): Promise<{
|
|
60
74
|
deletedEntities: number;
|
|
61
75
|
deletedRelationships: number;
|
|
@@ -65,6 +79,10 @@ declare class CosmosDbProvider implements StorageProvider, GraphTraversalProvide
|
|
|
65
79
|
getRelationship(repositoryId: string, relationshipId: string): Promise<StoredRelationship | null>;
|
|
66
80
|
getEntityRelationships(repositoryId: string, entityId: string, options?: RelationshipQueryOptions): Promise<PaginatedResult<StoredRelationship>>;
|
|
67
81
|
deleteRelationship(repositoryId: string, relationshipId: string): Promise<void>;
|
|
82
|
+
deleteRelationships(repositoryId: string, ids: string[]): Promise<{
|
|
83
|
+
deleted: string[];
|
|
84
|
+
notFound: string[];
|
|
85
|
+
}>;
|
|
68
86
|
deleteRelationshipsByType(repositoryId: string, relationshipType: string): Promise<{
|
|
69
87
|
deletedRelationships: number;
|
|
70
88
|
}>;
|
|
@@ -74,7 +92,28 @@ declare class CosmosDbProvider implements StorageProvider, GraphTraversalProvide
|
|
|
74
92
|
exportAll(repositoryId: string): AsyncIterable<ExportChunk>;
|
|
75
93
|
importBulk(repositoryId: string, data: ImportChunk[], options?: BulkImportOptions): Promise<BulkImportResult>;
|
|
76
94
|
getCapabilities(): GraphTraversalCapabilities;
|
|
77
|
-
traverse(repositoryId: string, spec: TraversalSpec
|
|
95
|
+
traverse(repositoryId: string, spec: TraversalSpec): Promise<TraversalResult>;
|
|
96
|
+
/**
|
|
97
|
+
* Execute a raw Gremlin query with caller-supplied bindings.
|
|
98
|
+
*
|
|
99
|
+
* ⚠️ ELEVATED PRIVILEGE — SYSTEM-LEVEL OPERATION ⚠️
|
|
100
|
+
*
|
|
101
|
+
* This method is an unscoped pass-through: it does not filter by repository,
|
|
102
|
+
* does not inject the partition key, and performs no validation on the query
|
|
103
|
+
* string. A single call can read or mutate any vertex or edge in the
|
|
104
|
+
* container regardless of which repository (partition) it belongs to.
|
|
105
|
+
*
|
|
106
|
+
* DO NOT expose this method to AI agents, end users, or any untrusted caller.
|
|
107
|
+
* It is intended for:
|
|
108
|
+
* - administrative tooling (migrations, diagnostics, repairs)
|
|
109
|
+
* - internal library operations that need cross-partition reach
|
|
110
|
+
*
|
|
111
|
+
* `repositoryId` is accepted for interface symmetry but is intentionally
|
|
112
|
+
* ignored here — the caller is trusted to scope the query themselves.
|
|
113
|
+
*
|
|
114
|
+
* For agent-facing graph queries use {@link traverse}, which enforces the
|
|
115
|
+
* repositoryId partition predicate.
|
|
116
|
+
*/
|
|
78
117
|
executeNativeQuery(_repositoryId: string, query: string, params?: Record<string, unknown>): Promise<TraversalResult>;
|
|
79
118
|
}
|
|
80
119
|
|
package/dist/index.d.ts
CHANGED
|
@@ -32,6 +32,16 @@ declare class CosmosDbProvider implements StorageProvider, GraphTraversalProvide
|
|
|
32
32
|
private readonly config;
|
|
33
33
|
private readonly compiler;
|
|
34
34
|
constructor(config: CosmosDbProviderConfig);
|
|
35
|
+
/**
|
|
36
|
+
* Reject any repositoryId that is not a valid v4 UUID.
|
|
37
|
+
*
|
|
38
|
+
* Every partition-scoped query filters on repositoryId. Since repositoryId is
|
|
39
|
+
* the CosmosDB partition key, a predicate with a different value would target
|
|
40
|
+
* a different partition. Strict format validation at the provider boundary
|
|
41
|
+
* prevents injection (e.g. strings containing quotes or Gremlin syntax) from
|
|
42
|
+
* ever reaching query construction.
|
|
43
|
+
*/
|
|
44
|
+
private assertValidRepositoryId;
|
|
35
45
|
initialize(): Promise<void>;
|
|
36
46
|
dispose(): Promise<void>;
|
|
37
47
|
ensureSchema(): Promise<EnsureSchemaResult>;
|
|
@@ -56,6 +66,10 @@ declare class CosmosDbProvider implements StorageProvider, GraphTraversalProvide
|
|
|
56
66
|
getEntities(repositoryId: string, entityIds: string[]): Promise<Map<string, StoredEntity>>;
|
|
57
67
|
updateEntity(repositoryId: string, entityId: string, updates: StoredEntityUpdate): Promise<StoredEntity>;
|
|
58
68
|
deleteEntity(repositoryId: string, entityId: string): Promise<void>;
|
|
69
|
+
deleteEntities(repositoryId: string, ids: string[]): Promise<{
|
|
70
|
+
deleted: string[];
|
|
71
|
+
notFound: string[];
|
|
72
|
+
}>;
|
|
59
73
|
deleteEntitiesByType(repositoryId: string, entityType: string): Promise<{
|
|
60
74
|
deletedEntities: number;
|
|
61
75
|
deletedRelationships: number;
|
|
@@ -65,6 +79,10 @@ declare class CosmosDbProvider implements StorageProvider, GraphTraversalProvide
|
|
|
65
79
|
getRelationship(repositoryId: string, relationshipId: string): Promise<StoredRelationship | null>;
|
|
66
80
|
getEntityRelationships(repositoryId: string, entityId: string, options?: RelationshipQueryOptions): Promise<PaginatedResult<StoredRelationship>>;
|
|
67
81
|
deleteRelationship(repositoryId: string, relationshipId: string): Promise<void>;
|
|
82
|
+
deleteRelationships(repositoryId: string, ids: string[]): Promise<{
|
|
83
|
+
deleted: string[];
|
|
84
|
+
notFound: string[];
|
|
85
|
+
}>;
|
|
68
86
|
deleteRelationshipsByType(repositoryId: string, relationshipType: string): Promise<{
|
|
69
87
|
deletedRelationships: number;
|
|
70
88
|
}>;
|
|
@@ -74,7 +92,28 @@ declare class CosmosDbProvider implements StorageProvider, GraphTraversalProvide
|
|
|
74
92
|
exportAll(repositoryId: string): AsyncIterable<ExportChunk>;
|
|
75
93
|
importBulk(repositoryId: string, data: ImportChunk[], options?: BulkImportOptions): Promise<BulkImportResult>;
|
|
76
94
|
getCapabilities(): GraphTraversalCapabilities;
|
|
77
|
-
traverse(repositoryId: string, spec: TraversalSpec
|
|
95
|
+
traverse(repositoryId: string, spec: TraversalSpec): Promise<TraversalResult>;
|
|
96
|
+
/**
|
|
97
|
+
* Execute a raw Gremlin query with caller-supplied bindings.
|
|
98
|
+
*
|
|
99
|
+
* ⚠️ ELEVATED PRIVILEGE — SYSTEM-LEVEL OPERATION ⚠️
|
|
100
|
+
*
|
|
101
|
+
* This method is an unscoped pass-through: it does not filter by repository,
|
|
102
|
+
* does not inject the partition key, and performs no validation on the query
|
|
103
|
+
* string. A single call can read or mutate any vertex or edge in the
|
|
104
|
+
* container regardless of which repository (partition) it belongs to.
|
|
105
|
+
*
|
|
106
|
+
* DO NOT expose this method to AI agents, end users, or any untrusted caller.
|
|
107
|
+
* It is intended for:
|
|
108
|
+
* - administrative tooling (migrations, diagnostics, repairs)
|
|
109
|
+
* - internal library operations that need cross-partition reach
|
|
110
|
+
*
|
|
111
|
+
* `repositoryId` is accepted for interface symmetry but is intentionally
|
|
112
|
+
* ignored here — the caller is trusted to scope the query themselves.
|
|
113
|
+
*
|
|
114
|
+
* For agent-facing graph queries use {@link traverse}, which enforces the
|
|
115
|
+
* repositoryId partition predicate.
|
|
116
|
+
*/
|
|
78
117
|
executeNativeQuery(_repositoryId: string, query: string, params?: Record<string, unknown>): Promise<TraversalResult>;
|
|
79
118
|
}
|
|
80
119
|
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/CosmosDbProvider.ts
|
|
2
2
|
import crypto from "crypto";
|
|
3
|
-
import { GremlinCompiler, ProviderError } from "@utaba/deep-memory";
|
|
3
|
+
import { GremlinCompiler, ProviderError, InvalidInputError, isValidUuid, projectEntity } from "@utaba/deep-memory";
|
|
4
4
|
|
|
5
5
|
// src/CosmosDbConnection.ts
|
|
6
6
|
import gremlin from "gremlin";
|
|
@@ -635,12 +635,19 @@ async function updateEntity(conn, repositoryId, entityId, updates) {
|
|
|
635
635
|
bindings[paramName] = value;
|
|
636
636
|
propParts.push(`.property('${key}', ${paramName})`);
|
|
637
637
|
};
|
|
638
|
+
const dropProp = (key) => {
|
|
639
|
+
propParts.push(`.sideEffect(properties('${key}').drop())`);
|
|
640
|
+
};
|
|
641
|
+
if (updates.entityType !== void 0) addProp("entityType", updates.entityType);
|
|
638
642
|
if (updates.label !== void 0) addProp("entityLabel", updates.label);
|
|
639
643
|
if (updates.slug !== void 0) addProp("slug", updates.slug);
|
|
640
|
-
if (updates.summary
|
|
644
|
+
if (updates.summary === null) dropProp("summary");
|
|
645
|
+
else if (updates.summary !== void 0) addProp("summary", updates.summary);
|
|
641
646
|
if (updates.properties !== void 0) addProp("properties", JSON.stringify(updates.properties));
|
|
642
|
-
if (updates.data
|
|
643
|
-
if (updates.
|
|
647
|
+
if (updates.data === null) dropProp("data");
|
|
648
|
+
else if (updates.data !== void 0) addProp("data", updates.data);
|
|
649
|
+
if (updates.dataFormat === null) dropProp("dataFormat");
|
|
650
|
+
else if (updates.dataFormat !== void 0) addProp("dataFormat", updates.dataFormat);
|
|
644
651
|
if (updates.embedding !== void 0) addProp("embedding", JSON.stringify(updates.embedding));
|
|
645
652
|
addProp("modifiedBy", updates.provenance.modifiedBy);
|
|
646
653
|
addProp("modifiedByType", updates.provenance.modifiedByType);
|
|
@@ -688,9 +695,37 @@ async function findEntities(conn, repositoryId, query) {
|
|
|
688
695
|
});
|
|
689
696
|
filterClause += `.has('entityType', within(${typeParams.join(", ")}))`;
|
|
690
697
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
698
|
+
const hasPropertyFilter = query.properties != null && Object.keys(query.properties).length > 0;
|
|
699
|
+
const needsClientFilter = Boolean(query.searchTerm) || hasPropertyFilter;
|
|
700
|
+
if (needsClientFilter) {
|
|
701
|
+
const dataResult2 = await conn.submit(
|
|
702
|
+
`g.V()${filterClause}.valueMap(true)`,
|
|
703
|
+
bindings
|
|
704
|
+
);
|
|
705
|
+
let items2 = dataResult2.items.map(entityFromGremlin);
|
|
706
|
+
if (query.searchTerm) {
|
|
707
|
+
const term = query.searchTerm.toLowerCase();
|
|
708
|
+
items2 = items2.filter(
|
|
709
|
+
(e) => e.label.toLowerCase().includes(term) || e.slug.toLowerCase().includes(term) || e.summary != null && e.summary.toLowerCase().includes(term)
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
if (hasPropertyFilter) {
|
|
713
|
+
items2 = items2.filter((entity) => {
|
|
714
|
+
for (const [key, value] of Object.entries(query.properties)) {
|
|
715
|
+
if (entity.properties[key] !== value) return false;
|
|
716
|
+
}
|
|
717
|
+
return true;
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
const total2 = items2.length;
|
|
721
|
+
const paged = items2.slice(query.offset, query.offset + query.limit);
|
|
722
|
+
return {
|
|
723
|
+
items: paged,
|
|
724
|
+
total: total2,
|
|
725
|
+
hasMore: query.offset + query.limit < total2,
|
|
726
|
+
limit: query.limit,
|
|
727
|
+
offset: query.offset
|
|
728
|
+
};
|
|
694
729
|
}
|
|
695
730
|
const countResult = await conn.submit(
|
|
696
731
|
`g.V()${filterClause}.count()`,
|
|
@@ -703,15 +738,7 @@ async function findEntities(conn, repositoryId, query) {
|
|
|
703
738
|
`g.V()${filterClause}.range(rangeStart, rangeEnd).valueMap(true)`,
|
|
704
739
|
bindings
|
|
705
740
|
);
|
|
706
|
-
|
|
707
|
-
if (query.properties && Object.keys(query.properties).length > 0) {
|
|
708
|
-
items = items.filter((entity) => {
|
|
709
|
-
for (const [key, value] of Object.entries(query.properties)) {
|
|
710
|
-
if (entity.properties[key] !== value) return false;
|
|
711
|
-
}
|
|
712
|
-
return true;
|
|
713
|
-
});
|
|
714
|
-
}
|
|
741
|
+
const items = dataResult.items.map(entityFromGremlin);
|
|
715
742
|
return {
|
|
716
743
|
items,
|
|
717
744
|
total,
|
|
@@ -1282,6 +1309,23 @@ var CosmosDbProvider = class {
|
|
|
1282
1309
|
rejectUnauthorized: config.rejectUnauthorized
|
|
1283
1310
|
});
|
|
1284
1311
|
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Reject any repositoryId that is not a valid v4 UUID.
|
|
1314
|
+
*
|
|
1315
|
+
* Every partition-scoped query filters on repositoryId. Since repositoryId is
|
|
1316
|
+
* the CosmosDB partition key, a predicate with a different value would target
|
|
1317
|
+
* a different partition. Strict format validation at the provider boundary
|
|
1318
|
+
* prevents injection (e.g. strings containing quotes or Gremlin syntax) from
|
|
1319
|
+
* ever reaching query construction.
|
|
1320
|
+
*/
|
|
1321
|
+
assertValidRepositoryId(repositoryId) {
|
|
1322
|
+
if (!isValidUuid(repositoryId)) {
|
|
1323
|
+
throw new InvalidInputError(
|
|
1324
|
+
"repositoryId",
|
|
1325
|
+
`repositoryId must be a valid v4 UUID; got '${repositoryId}'`
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1285
1329
|
// ─── Lifecycle ─────────────────────────────────────────────────────
|
|
1286
1330
|
async initialize() {
|
|
1287
1331
|
await this.conn.connect();
|
|
@@ -1367,93 +1411,195 @@ var CosmosDbProvider = class {
|
|
|
1367
1411
|
}
|
|
1368
1412
|
// ─── Repository ────────────────────────────────────────────────────
|
|
1369
1413
|
async createRepository(config) {
|
|
1414
|
+
this.assertValidRepositoryId(config.repositoryId);
|
|
1370
1415
|
return createRepository(this.conn, config);
|
|
1371
1416
|
}
|
|
1372
1417
|
async getRepository(repositoryId) {
|
|
1418
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1373
1419
|
return getRepository(this.conn, repositoryId);
|
|
1374
1420
|
}
|
|
1375
1421
|
async listRepositories(filter) {
|
|
1376
1422
|
return listRepositories(this.conn, filter);
|
|
1377
1423
|
}
|
|
1378
1424
|
async updateRepository(repositoryId, updates) {
|
|
1425
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1379
1426
|
return updateRepository(this.conn, repositoryId, updates);
|
|
1380
1427
|
}
|
|
1381
1428
|
async deleteRepository(repositoryId, onProgress) {
|
|
1429
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1382
1430
|
return deleteRepository(this.conn, repositoryId, onProgress);
|
|
1383
1431
|
}
|
|
1384
1432
|
async deleteAllContents(repositoryId, onProgress) {
|
|
1433
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1385
1434
|
return deleteAllContents(this.conn, repositoryId, onProgress);
|
|
1386
1435
|
}
|
|
1387
1436
|
async getRepositoryStats(repositoryId) {
|
|
1437
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1388
1438
|
return getRepositoryStats(this.conn, repositoryId);
|
|
1389
1439
|
}
|
|
1390
1440
|
// ─── Vocabulary ────────────────────────────────────────────────────
|
|
1391
1441
|
async getVocabulary(repositoryId) {
|
|
1442
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1392
1443
|
return getVocabulary(this.conn, repositoryId);
|
|
1393
1444
|
}
|
|
1394
1445
|
async saveVocabulary(repositoryId, vocabulary) {
|
|
1446
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1395
1447
|
return saveVocabulary(this.conn, repositoryId, vocabulary);
|
|
1396
1448
|
}
|
|
1397
1449
|
async getVocabularyChangeLog(repositoryId, options) {
|
|
1450
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1398
1451
|
return getVocabularyChangeLog(this.conn, repositoryId, options);
|
|
1399
1452
|
}
|
|
1400
1453
|
// ─── Entities ──────────────────────────────────────────────────────
|
|
1401
1454
|
async createEntity(repositoryId, entity) {
|
|
1455
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1402
1456
|
return createEntity(this.conn, repositoryId, entity);
|
|
1403
1457
|
}
|
|
1404
1458
|
async getEntity(repositoryId, entityId) {
|
|
1459
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1405
1460
|
return getEntity(this.conn, repositoryId, entityId);
|
|
1406
1461
|
}
|
|
1407
1462
|
async getEntityBySlug(repositoryId, slug) {
|
|
1463
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1408
1464
|
return getEntityBySlug(this.conn, repositoryId, slug);
|
|
1409
1465
|
}
|
|
1410
1466
|
async getEntities(repositoryId, entityIds) {
|
|
1467
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1411
1468
|
return getEntities(this.conn, repositoryId, entityIds);
|
|
1412
1469
|
}
|
|
1413
1470
|
async updateEntity(repositoryId, entityId, updates) {
|
|
1471
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1414
1472
|
return updateEntity(this.conn, repositoryId, entityId, updates);
|
|
1415
1473
|
}
|
|
1416
1474
|
async deleteEntity(repositoryId, entityId) {
|
|
1475
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1417
1476
|
return deleteEntity(this.conn, repositoryId, entityId);
|
|
1418
1477
|
}
|
|
1478
|
+
async deleteEntities(repositoryId, ids) {
|
|
1479
|
+
if (ids.length === 0) return { deleted: [], notFound: [] };
|
|
1480
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1481
|
+
const deleted = [];
|
|
1482
|
+
const CHUNK = 100;
|
|
1483
|
+
for (let i = 0; i < ids.length; i += CHUNK) {
|
|
1484
|
+
const chunk = ids.slice(i, i + CHUNK);
|
|
1485
|
+
const bindings = { rid: repositoryId };
|
|
1486
|
+
const idParams = [];
|
|
1487
|
+
for (let j = 0; j < chunk.length; j++) {
|
|
1488
|
+
const p = `id${j}`;
|
|
1489
|
+
bindings[p] = chunk[j];
|
|
1490
|
+
idParams.push(p);
|
|
1491
|
+
}
|
|
1492
|
+
const withinExpr = `P.within(${idParams.join(", ")})`;
|
|
1493
|
+
const existResult = await this.conn.submit(
|
|
1494
|
+
`g.V().has('repositoryId', rid).has('id', ${withinExpr}).has('entityType').values('id')`,
|
|
1495
|
+
bindings
|
|
1496
|
+
);
|
|
1497
|
+
const found = existResult.items;
|
|
1498
|
+
if (found.length > 0) {
|
|
1499
|
+
const dropBindings = { rid: repositoryId };
|
|
1500
|
+
const dropIdParams = [];
|
|
1501
|
+
for (let j = 0; j < found.length; j++) {
|
|
1502
|
+
const p = `did${j}`;
|
|
1503
|
+
dropBindings[p] = found[j];
|
|
1504
|
+
dropIdParams.push(p);
|
|
1505
|
+
}
|
|
1506
|
+
const dropWithinExpr = `P.within(${dropIdParams.join(", ")})`;
|
|
1507
|
+
await this.conn.submit(
|
|
1508
|
+
`g.V().has('repositoryId', rid).has('id', ${dropWithinExpr}).has('entityType').drop()`,
|
|
1509
|
+
dropBindings
|
|
1510
|
+
);
|
|
1511
|
+
deleted.push(...found);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
const deletedSet = new Set(deleted);
|
|
1515
|
+
return { deleted, notFound: ids.filter((id) => !deletedSet.has(id)) };
|
|
1516
|
+
}
|
|
1419
1517
|
async deleteEntitiesByType(repositoryId, entityType) {
|
|
1518
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1420
1519
|
return deleteEntitiesByType(this.conn, repositoryId, entityType);
|
|
1421
1520
|
}
|
|
1422
1521
|
async findEntities(repositoryId, query) {
|
|
1522
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1423
1523
|
return findEntities(this.conn, repositoryId, query);
|
|
1424
1524
|
}
|
|
1425
1525
|
// ─── Relationships ─────────────────────────────────────────────────
|
|
1426
1526
|
async createRelationship(repositoryId, relationship) {
|
|
1527
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1427
1528
|
return createRelationship(this.conn, repositoryId, relationship);
|
|
1428
1529
|
}
|
|
1429
1530
|
async getRelationship(repositoryId, relationshipId) {
|
|
1531
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1430
1532
|
return getRelationship(this.conn, repositoryId, relationshipId);
|
|
1431
1533
|
}
|
|
1432
1534
|
async getEntityRelationships(repositoryId, entityId, options) {
|
|
1535
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1433
1536
|
return getEntityRelationships(this.conn, repositoryId, entityId, options);
|
|
1434
1537
|
}
|
|
1435
1538
|
async deleteRelationship(repositoryId, relationshipId) {
|
|
1539
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1436
1540
|
return deleteRelationship(this.conn, repositoryId, relationshipId);
|
|
1437
1541
|
}
|
|
1542
|
+
async deleteRelationships(repositoryId, ids) {
|
|
1543
|
+
if (ids.length === 0) return { deleted: [], notFound: [] };
|
|
1544
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1545
|
+
const deleted = [];
|
|
1546
|
+
const CHUNK = 100;
|
|
1547
|
+
for (let i = 0; i < ids.length; i += CHUNK) {
|
|
1548
|
+
const chunk = ids.slice(i, i + CHUNK);
|
|
1549
|
+
const bindings = { rid: repositoryId };
|
|
1550
|
+
const idParams = [];
|
|
1551
|
+
for (let j = 0; j < chunk.length; j++) {
|
|
1552
|
+
const p = `id${j}`;
|
|
1553
|
+
bindings[p] = chunk[j];
|
|
1554
|
+
idParams.push(p);
|
|
1555
|
+
}
|
|
1556
|
+
const withinExpr = `P.within(${idParams.join(", ")})`;
|
|
1557
|
+
const existResult = await this.conn.submit(
|
|
1558
|
+
`g.E().has('repositoryId', rid).has('id', ${withinExpr}).values('id')`,
|
|
1559
|
+
bindings
|
|
1560
|
+
);
|
|
1561
|
+
const found = existResult.items;
|
|
1562
|
+
if (found.length > 0) {
|
|
1563
|
+
await cosmosRestBatchDelete(
|
|
1564
|
+
this.getRestEndpoint(),
|
|
1565
|
+
this.config.key,
|
|
1566
|
+
this.config.database,
|
|
1567
|
+
this.config.container,
|
|
1568
|
+
repositoryId,
|
|
1569
|
+
found,
|
|
1570
|
+
this.config.rejectUnauthorized ?? true
|
|
1571
|
+
);
|
|
1572
|
+
deleted.push(...found);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
const deletedSet = new Set(deleted);
|
|
1576
|
+
return { deleted, notFound: ids.filter((id) => !deletedSet.has(id)) };
|
|
1577
|
+
}
|
|
1438
1578
|
async deleteRelationshipsByType(repositoryId, relationshipType) {
|
|
1579
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1439
1580
|
return deleteRelationshipsByType(this.conn, repositoryId, relationshipType);
|
|
1440
1581
|
}
|
|
1441
1582
|
// ─── Graph Traversal (StorageProvider) ─────────────────────────────
|
|
1442
1583
|
async exploreNeighborhood(repositoryId, entityId, options) {
|
|
1584
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1443
1585
|
return exploreNeighborhood(this.conn, repositoryId, entityId, options);
|
|
1444
1586
|
}
|
|
1445
1587
|
async findPaths(repositoryId, sourceId, targetId, options) {
|
|
1588
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1446
1589
|
return findPaths(this.conn, repositoryId, sourceId, targetId, options);
|
|
1447
1590
|
}
|
|
1448
1591
|
// ─── Timeline ──────────────────────────────────────────────────────
|
|
1449
1592
|
async getTimeline(repositoryId, entityId, options) {
|
|
1593
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1450
1594
|
return getTimeline(this.conn, repositoryId, entityId, options);
|
|
1451
1595
|
}
|
|
1452
1596
|
// ─── Bulk Operations ───────────────────────────────────────────────
|
|
1453
1597
|
exportAll(repositoryId) {
|
|
1598
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1454
1599
|
return exportAll(this.conn, repositoryId);
|
|
1455
1600
|
}
|
|
1456
1601
|
async importBulk(repositoryId, data, options) {
|
|
1602
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1457
1603
|
return importBulk(this.conn, repositoryId, data, options);
|
|
1458
1604
|
}
|
|
1459
1605
|
// ─── GraphTraversalProvider ────────────────────────────────────────
|
|
@@ -1470,17 +1616,20 @@ var CosmosDbProvider = class {
|
|
|
1470
1616
|
supportsRelationshipSummary: false
|
|
1471
1617
|
};
|
|
1472
1618
|
}
|
|
1473
|
-
async traverse(repositoryId, spec
|
|
1619
|
+
async traverse(repositoryId, spec) {
|
|
1620
|
+
this.assertValidRepositoryId(repositoryId);
|
|
1474
1621
|
const startTime = Date.now();
|
|
1475
1622
|
const vocabulary = await this.getVocabulary(repositoryId);
|
|
1476
|
-
const compiled =
|
|
1623
|
+
const compiled = this.compiler.compile(spec, vocabulary);
|
|
1477
1624
|
const scopedQuery = compiled.query.replace(
|
|
1478
1625
|
"g.V()",
|
|
1479
|
-
|
|
1626
|
+
"g.V().has('repositoryId', pRid)"
|
|
1480
1627
|
);
|
|
1628
|
+
const scopedParams = { ...compiled.params, pRid: repositoryId };
|
|
1481
1629
|
try {
|
|
1482
|
-
const result = await this.conn.submit(scopedQuery,
|
|
1630
|
+
const result = await this.conn.submit(scopedQuery, scopedParams);
|
|
1483
1631
|
const executionTimeMs = Date.now() - startTime;
|
|
1632
|
+
const detailLevel = spec.detailLevel ?? "summary";
|
|
1484
1633
|
const entities = [];
|
|
1485
1634
|
const relationships = [];
|
|
1486
1635
|
const paths = [];
|
|
@@ -1492,7 +1641,8 @@ var CosmosDbProvider = class {
|
|
|
1492
1641
|
for (const obj of pathData.objects) {
|
|
1493
1642
|
const props = obj;
|
|
1494
1643
|
if (props["entityType"]) {
|
|
1495
|
-
|
|
1644
|
+
const stored = entityFromGremlin(props);
|
|
1645
|
+
pathEntities.push(projectEntity(stored, detailLevel));
|
|
1496
1646
|
}
|
|
1497
1647
|
}
|
|
1498
1648
|
if (pathEntities.length > 0) {
|
|
@@ -1506,8 +1656,8 @@ var CosmosDbProvider = class {
|
|
|
1506
1656
|
}
|
|
1507
1657
|
} else {
|
|
1508
1658
|
for (const item of result.items) {
|
|
1509
|
-
const
|
|
1510
|
-
entities.push(
|
|
1659
|
+
const stored = entityFromGremlin(item);
|
|
1660
|
+
entities.push(projectEntity(stored, detailLevel));
|
|
1511
1661
|
}
|
|
1512
1662
|
}
|
|
1513
1663
|
const limit = spec.limit ?? 50;
|
|
@@ -1539,6 +1689,27 @@ var CosmosDbProvider = class {
|
|
|
1539
1689
|
);
|
|
1540
1690
|
}
|
|
1541
1691
|
}
|
|
1692
|
+
/**
|
|
1693
|
+
* Execute a raw Gremlin query with caller-supplied bindings.
|
|
1694
|
+
*
|
|
1695
|
+
* ⚠️ ELEVATED PRIVILEGE — SYSTEM-LEVEL OPERATION ⚠️
|
|
1696
|
+
*
|
|
1697
|
+
* This method is an unscoped pass-through: it does not filter by repository,
|
|
1698
|
+
* does not inject the partition key, and performs no validation on the query
|
|
1699
|
+
* string. A single call can read or mutate any vertex or edge in the
|
|
1700
|
+
* container regardless of which repository (partition) it belongs to.
|
|
1701
|
+
*
|
|
1702
|
+
* DO NOT expose this method to AI agents, end users, or any untrusted caller.
|
|
1703
|
+
* It is intended for:
|
|
1704
|
+
* - administrative tooling (migrations, diagnostics, repairs)
|
|
1705
|
+
* - internal library operations that need cross-partition reach
|
|
1706
|
+
*
|
|
1707
|
+
* `repositoryId` is accepted for interface symmetry but is intentionally
|
|
1708
|
+
* ignored here — the caller is trusted to scope the query themselves.
|
|
1709
|
+
*
|
|
1710
|
+
* For agent-facing graph queries use {@link traverse}, which enforces the
|
|
1711
|
+
* repositoryId partition predicate.
|
|
1712
|
+
*/
|
|
1542
1713
|
async executeNativeQuery(_repositoryId, query, params) {
|
|
1543
1714
|
const startTime = Date.now();
|
|
1544
1715
|
try {
|
|
@@ -1617,6 +1788,32 @@ async function cosmosRestPut(restBase, key, urlPath, resourceLink, resourceType,
|
|
|
1617
1788
|
const text = await response.text();
|
|
1618
1789
|
throw new Error(`CosmosDB REST ${response.status}: ${text}`);
|
|
1619
1790
|
}
|
|
1791
|
+
async function cosmosRestBatchDelete(restBase, key, database, container, partitionKeyValue, ids, rejectUnauthorized) {
|
|
1792
|
+
const resourceLink = `dbs/${database}/colls/${container}`;
|
|
1793
|
+
const date = (/* @__PURE__ */ new Date()).toUTCString();
|
|
1794
|
+
const token = cosmosAuthToken("post", "docs", resourceLink, date, key);
|
|
1795
|
+
if (!rejectUnauthorized) {
|
|
1796
|
+
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
|
|
1797
|
+
}
|
|
1798
|
+
const ops = ids.map((id) => ({ operationType: "Delete", id }));
|
|
1799
|
+
const response = await fetch(`${restBase}/${resourceLink}/docs`, {
|
|
1800
|
+
method: "POST",
|
|
1801
|
+
headers: {
|
|
1802
|
+
"Authorization": token,
|
|
1803
|
+
"x-ms-version": "2020-07-15",
|
|
1804
|
+
"x-ms-date": date,
|
|
1805
|
+
"Content-Type": "application/json",
|
|
1806
|
+
"x-ms-documentdb-partitionkey": JSON.stringify([partitionKeyValue]),
|
|
1807
|
+
"x-ms-cosmos-is-batch-request": "true",
|
|
1808
|
+
"x-ms-cosmos-batch-atomic": "true"
|
|
1809
|
+
},
|
|
1810
|
+
body: JSON.stringify(ops)
|
|
1811
|
+
});
|
|
1812
|
+
if (response.status !== 200 && response.status !== 207) {
|
|
1813
|
+
const text = await response.text();
|
|
1814
|
+
throw new ProviderError(`CosmosDB batch delete failed (${response.status}): ${text}`);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1620
1817
|
export {
|
|
1621
1818
|
CosmosDbConnection,
|
|
1622
1819
|
CosmosDbProvider
|