@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 CHANGED
@@ -48,7 +48,7 @@
48
48
  "Contribution" shall mean any work of authorship, including
49
49
  the original version of the Work and any modifications or additions
50
50
  to that Work or Derivative Works thereof, that is intentionally
51
- submitted to the Licensor for inclusion in the Work by the copyright owner
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
52
  or by an individual or Legal Entity authorized to submit on behalf of
53
53
  the copyright owner. For the purposes of this definition, "submitted"
54
54
  means any form of electronic, verbal, or written communication sent
@@ -60,7 +60,7 @@
60
60
  designated in writing by the copyright owner as "Not a Contribution."
61
61
 
62
62
  "Contributor" shall mean Licensor and any individual or Legal Entity
63
- on behalf of whom a Contribution has been received by the Licensor and
63
+ on behalf of whom a Contribution has been received by Licensor and
64
64
  subsequently incorporated within the Work.
65
65
 
66
66
  2. Grant of Copyright License. Subject to the terms and conditions of
@@ -106,7 +106,7 @@
106
106
  (d) If the Work includes a "NOTICE" text file as part of its
107
107
  distribution, then any Derivative Works that You distribute must
108
108
  include a readable copy of the attribution notices contained
109
- within such NOTICE file, excluding any notices that do not
109
+ within such NOTICE file, excluding those notices that do not
110
110
  pertain to any part of the Derivative Works, in at least one
111
111
  of the following places: within a NOTICE text file distributed
112
112
  as part of the Derivative Works; within the Source form or
@@ -175,7 +175,18 @@
175
175
 
176
176
  END OF TERMS AND CONDITIONS
177
177
 
178
- Copyright 2024 Utaba AI Ltd
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2024 Tim Wheeler
179
190
 
180
191
  Licensed under the Apache License, Version 2.0 (the "License");
181
192
  you may not use this file except in compliance with the License.
package/README.md ADDED
@@ -0,0 +1,262 @@
1
+ # @utaba/deep-memory-storage-cosmosdb
2
+
3
+ CosmosDB Gremlin storage provider for [`@utaba/deep-memory`](https://www.npmjs.com/package/@utaba/deep-memory). Implements both `StorageProvider` and `GraphTraversalProvider` — a single instance gives deep-memory persistent storage *and* native graph query capabilities backed by Azure CosmosDB.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @utaba/deep-memory @utaba/deep-memory-storage-cosmosdb
9
+ ```
10
+
11
+ **Runtime dependency:** [`gremlin`](https://www.npmjs.com/package/gremlin) (Apache TinkerPop JavaScript driver).
12
+
13
+ ## Quick Start (production / Azure)
14
+
15
+ ```typescript
16
+ import { DeepMemory } from '@utaba/deep-memory';
17
+ import { CosmosDbProvider } from '@utaba/deep-memory-storage-cosmosdb';
18
+
19
+ const provider = new CosmosDbProvider({
20
+ endpoint: 'wss://your-account.gremlin.cosmos.azure.com:443/',
21
+ key: process.env.COSMOSDB_KEY!,
22
+ database: 'deep-memory',
23
+ container: 'graph',
24
+ });
25
+
26
+ await provider.ensureSchema(); // creates db + container if needed
27
+ await provider.initialise(); // opens Gremlin WebSocket
28
+
29
+ const dm = new DeepMemory({
30
+ storage: provider,
31
+ graphTraversal: provider, // same instance — implements both interfaces
32
+ });
33
+ ```
34
+
35
+ For local development with the CosmosDB emulator, see [Local emulator setup](#local-emulator-setup) below.
36
+
37
+ ## Configuration
38
+
39
+ ### `CosmosDbProviderConfig`
40
+
41
+ | Option | Type | Default | Description |
42
+ |--------|------|---------|-------------|
43
+ | `endpoint` | `string` | *required* | Gremlin WebSocket endpoint (e.g. `wss://your-account.gremlin.cosmos.azure.com:443/`) |
44
+ | `restEndpoint` | `string` | derived from `endpoint` | CosmosDB REST endpoint for database/container provisioning. Defaults to the Gremlin hostname on port 8081. |
45
+ | `key` | `string` | *required* | CosmosDB primary key |
46
+ | `database` | `string` | *required* | Database name |
47
+ | `container` | `string` | *required* | Container (graph) name |
48
+ | `partitionKey` | `string` | `/repositoryId` | Partition key path |
49
+ | `maxRetries` | `number` | `3` | Retries for transient errors (429 throttling, 503 unavailable) |
50
+ | `defaultTimeoutMs` | `number` | `30000` | Default query timeout |
51
+ | `rejectUnauthorized` | `boolean` | `true` | Set `false` for the local emulator (self-signed certs) |
52
+
53
+ CosmosDB Gremlin API does **not** support managed identity for data plane operations — authentication always uses an account key.
54
+
55
+ ## Lifecycle
56
+
57
+ ```typescript
58
+ const provider = new CosmosDbProvider({ ... });
59
+
60
+ await provider.ensureSchema(); // creates db + container, writes _meta vertex
61
+ await provider.initialise(); // opens Gremlin WebSocket
62
+
63
+ const dm = new DeepMemory({ storage: provider, graphTraversal: provider });
64
+ // ... use ...
65
+
66
+ await provider.dispose(); // closes WebSocket
67
+ ```
68
+
69
+ `ensureSchema()` uses the CosmosDB REST API to create the database and container if they don't exist, then writes a `_meta` schema version vertex. Subsequent calls detect the existing schema and return early.
70
+
71
+ ## Data Model
72
+
73
+ All data is partitioned by `repositoryId` — every vertex and edge stores it. This enables:
74
+
75
+ - Single-partition queries for all operations within a repository
76
+ - Efficient cascade deletes (drop all documents in a partition)
77
+ - Cross-partition queries only for `listRepositories()`
78
+
79
+ ### Vertex Types
80
+
81
+ | Label | Purpose | ID Format |
82
+ |-------|---------|-----------|
83
+ | `_meta` | Schema version tracking | `_meta:schema` |
84
+ | `_repository` | Repository definitions and governance config | `repo:{repositoryId}` |
85
+ | `_vocabulary` | One vocabulary JSON document per repository | `vocab:{repositoryId}` |
86
+ | `_vocabularyChangeLog` | Audit trail for vocabulary changes | `vocablog:{changeId}` |
87
+ | `{entityType}` | Graph nodes — vertex label is the entity type | Entity GUID |
88
+
89
+ ### Edge Types
90
+
91
+ Edge labels are relationship types. Each edge stores `sourceEntityId`, `targetEntityId`, `bidirectional`, properties (as JSON), and provenance.
92
+
93
+ ### Property Storage
94
+
95
+ | Data | Storage | Notes |
96
+ |------|---------|-------|
97
+ | Entity properties | JSON string in `properties` vertex property | Parsed on read |
98
+ | Embeddings | JSON string in `embedding` vertex property | Stored for export/import fidelity; not searchable via Gremlin |
99
+ | Governance config | JSON string in `governanceConfig` vertex property | On `_repository` vertices |
100
+ | Vocabulary | JSON string in `vocabulary` vertex property | On `_vocabulary` vertices |
101
+
102
+ ## Capabilities
103
+
104
+ `findEntities()` supports:
105
+
106
+ - **Type filter** — restrict to specific entity types
107
+ - **Text search** — case-insensitive slug-based matching via `TextP.containing()`
108
+ - **Pagination** — Gremlin `range()` with total count
109
+
110
+ `GraphTraversalProvider` capabilities:
111
+
112
+ | Capability | Supported |
113
+ |-----------|-----------|
114
+ | Native Gremlin queries | Yes |
115
+ | Relationship property filters | Yes |
116
+ | Entity property filters | Yes |
117
+ | Repeat/loop traversals | Yes |
118
+ | Dedup | Yes |
119
+ | Aggregation | Limited (CosmosDB Gremlin limitation) |
120
+ | Relationship summaries | No |
121
+
122
+ RU cost is reported in `QueryMetadata.resourceCost` for graph traversal operations.
123
+
124
+ ## Limitations
125
+
126
+ | Limitation | Impact |
127
+ |-----------|--------|
128
+ | No full-text search | `findEntities()` text matching is slug-based only — pair with a separate `SearchProvider` for richer search |
129
+ | No vector similarity | Embeddings stored for portability but not searchable |
130
+ | Limited aggregation | `count()`, `sum()`, `mean()` have limited support |
131
+ | No lambda steps | Cannot use closures in Gremlin queries |
132
+ | Arrays stored as JSON strings | Cannot filter on array elements server-side |
133
+
134
+ ## Bulk Operations
135
+
136
+ `exportAll()` returns an async iterable of chunks (batches of 100), entities first then relationships. `importBulk()` uses upsert semantics and adapts concurrency to RU-constrained tiers — see [Adaptive import deep-dive](https://github.com/TjWheeler/deep-memory/blob/main/docs/storage-cosmosdb-adaptive-import.md) for the control loop, throttle detection, and circuit breaker behavior.
137
+
138
+ ## Error Handling
139
+
140
+ All errors use the `@utaba/deep-memory` error hierarchy (`ProviderError`, `RepositoryNotFoundError`, `DuplicateEntityError`, etc.). Transient errors (429 throttling, 503 unavailable) are automatically retried with exponential backoff up to `maxRetries`.
141
+
142
+ ## Local emulator setup
143
+
144
+ The **Windows desktop** CosmosDB emulator supports the Gremlin API. The Docker emulator does **not**.
145
+
146
+ ### Installation
147
+
148
+ 1. Install the [Azure CosmosDB Emulator](https://learn.microsoft.com/en-us/azure/cosmos-db/local-emulator) on Windows.
149
+ 2. The default emulator key is: `C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==`
150
+
151
+ ### Starting the emulator with Gremlin
152
+
153
+ From an **admin** PowerShell:
154
+
155
+ ```powershell
156
+ & "C:\Program Files\Azure Cosmos DB Emulator\Microsoft.Azure.Cosmos.Emulator.exe" /EnableGremlinEndpoint
157
+ ```
158
+
159
+ This starts the emulator with:
160
+ - REST API on port **8081** (HTTPS)
161
+ - Gremlin endpoint on port **8901** (WebSocket)
162
+
163
+ ### Starting with network access (required for WSL2)
164
+
165
+ If you're connecting from WSL2, the emulator must listen on all interfaces. `/AllowNetworkAccess` requires the `/Key` parameter:
166
+
167
+ ```powershell
168
+ & "C:\Program Files\Azure Cosmos DB Emulator\Microsoft.Azure.Cosmos.Emulator.exe" `
169
+ /AllowNetworkAccess `
170
+ /Key=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw== `
171
+ /EnableGremlinEndpoint
172
+ ```
173
+
174
+ You may also need Windows Firewall rules:
175
+
176
+ ```powershell
177
+ netsh advfirewall firewall add rule name="CosmosDB REST" dir=in action=allow protocol=TCP localport=8081
178
+ netsh advfirewall firewall add rule name="CosmosDB Gremlin" dir=in action=allow protocol=TCP localport=8901
179
+ ```
180
+
181
+ ### Connecting from WSL2
182
+
183
+ From WSL2, connect using `host.docker.internal` which resolves to the Windows host:
184
+
185
+ ```typescript
186
+ const provider = new CosmosDbProvider({
187
+ endpoint: 'ws://host.docker.internal:8901/',
188
+ restEndpoint: 'https://host.docker.internal:8081',
189
+ key: 'C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==',
190
+ database: 'deep-memory-test',
191
+ container: 'graph-test',
192
+ rejectUnauthorized: false,
193
+ });
194
+ ```
195
+
196
+ **Note:** The Gremlin endpoint uses `ws://` (plain WebSocket), not `wss://`. The emulator's self-signed certificate causes TLS errors with the Gremlin client when using `wss://`.
197
+
198
+ ### Troubleshooting
199
+
200
+ | Problem | Solution |
201
+ |---------|----------|
202
+ | "Multiple attempts to restart" error | Full reset: shut down, delete `%LOCALAPPDATA%\CosmosDBEmulator`, reinstall if needed |
203
+ | Port 8081/8901 not reachable from WSL2 | Start with `/AllowNetworkAccess` and add firewall rules |
204
+ | Gremlin endpoint not starting | Ensure `/EnableGremlinEndpoint` flag is present at startup |
205
+ | Emulator crash loop | Delete data: `Remove-Item -Recurse -Force "$env:LOCALAPPDATA\CosmosDBEmulator"` then restart |
206
+ | TLS errors on Gremlin connection | Use `ws://` not `wss://`, and set `rejectUnauthorized: false` |
207
+
208
+ ## Azure production deployment
209
+
210
+ 1. Create a CosmosDB account with **Apache Gremlin** API in the Azure portal.
211
+ 2. Note the Gremlin endpoint (e.g. `wss://your-account.gremlin.cosmos.azure.com:443/`).
212
+ 3. Get the primary key from the Keys blade.
213
+ 4. Call `ensureSchema()` once on first deployment — it creates the database and container.
214
+
215
+ ```typescript
216
+ const provider = new CosmosDbProvider({
217
+ endpoint: 'wss://your-account.gremlin.cosmos.azure.com:443/',
218
+ key: process.env.COSMOSDB_KEY!,
219
+ database: 'deep-memory',
220
+ container: 'graph',
221
+ });
222
+ ```
223
+
224
+ ### Request Unit (RU) cost considerations
225
+
226
+ CosmosDB charges per Request Unit. The provider reports RU costs in `QueryMetadata.resourceCost` for `GraphTraversalProvider` operations. Key cost drivers:
227
+
228
+ - **Write operations** — entity/relationship creation costs ~5-10 RU per document
229
+ - **Cross-partition queries** — `listRepositories()` is the only cross-partition query
230
+ - **Graph traversals** — cost scales with depth and fan-out; use filters to constrain
231
+ - **Bulk imports** — `importBulk()` uses an adaptive concurrency controller that ramps down on 429s and back up when the cluster keeps up. See the [adaptive import deep-dive](https://github.com/TjWheeler/deep-memory/blob/main/docs/storage-cosmosdb-adaptive-import.md).
232
+
233
+ ## Testing
234
+
235
+ The conformance test suite requires a running CosmosDB emulator with Gremlin enabled:
236
+
237
+ ```bash
238
+ COSMOSDB_GREMLIN_ENDPOINT=ws://host.docker.internal:8901/ \
239
+ COSMOSDB_REST_ENDPOINT=https://host.docker.internal:8081 \
240
+ COSMOSDB_KEY=<emulator-key> \
241
+ pnpm --filter @utaba/deep-memory-storage-cosmosdb test
242
+ ```
243
+
244
+ Without `COSMOSDB_GREMLIN_ENDPOINT`, tests are skipped.
245
+
246
+ ## Exports
247
+
248
+ ```typescript
249
+ // Provider class (implements StorageProvider + GraphTraversalProvider)
250
+ import { CosmosDbProvider } from '@utaba/deep-memory-storage-cosmosdb';
251
+
252
+ // Config type
253
+ import type { CosmosDbProviderConfig } from '@utaba/deep-memory-storage-cosmosdb';
254
+
255
+ // Low-level connection (for advanced usage)
256
+ import { CosmosDbConnection } from '@utaba/deep-memory-storage-cosmosdb';
257
+ import type { CosmosDbConnectionConfig, GremlinResult } from '@utaba/deep-memory-storage-cosmosdb';
258
+ ```
259
+
260
+ ## See also
261
+
262
+ - [Adaptive import deep-dive](https://github.com/TjWheeler/deep-memory/blob/main/docs/storage-cosmosdb-adaptive-import.md) — how `importBulk` adapts concurrency to RU-constrained tiers