mushroomdb-client 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +281 -0
- package/dist/client.d.ts +176 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +239 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +406 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +21 -0
- package/dist/types.js.map +1 -0
- package/dist/ws.d.ts +92 -0
- package/dist/ws.d.ts.map +1 -0
- package/dist/ws.js +139 -0
- package/dist/ws.js.map +1 -0
- package/package.json +42 -0
- package/src/client.ts +364 -0
- package/src/index.ts +47 -0
- package/src/types.ts +460 -0
- package/src/ws.ts +200 -0
package/README.md
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# mushroomdb-client
|
|
2
|
+
|
|
3
|
+
TypeScript client for the [mushroomdb](https://github.com/MatthewSherlin/mushroomdb) graph database.
|
|
4
|
+
Wraps the HTTP and WebSocket API exposed by `mushroomdb serve`.
|
|
5
|
+
|
|
6
|
+
> **Not yet published to npm.** Install from the repo path (see below).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Installation (from repo)
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install /path/to/graph-db/clients/typescript
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Or add to `package.json`:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"mushroomdb-client": "file:../path/to/graph-db/clients/typescript"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Node 18+ required. Fetch is built-in. For WebSocket in Node < 21, install `ws`:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npm install ws
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { MushroomClient } from 'mushroomdb-client';
|
|
38
|
+
|
|
39
|
+
const client = new MushroomClient('http://127.0.0.1:8080');
|
|
40
|
+
|
|
41
|
+
// Read query
|
|
42
|
+
const result = await client.query(
|
|
43
|
+
'MATCH (p:Person) RETURN p.id AS id, p.name AS name LIMIT 10',
|
|
44
|
+
);
|
|
45
|
+
console.log(result.columns); // ['id', 'name']
|
|
46
|
+
console.log(result.rows); // [['person-01', 'Alice'], ...]
|
|
47
|
+
|
|
48
|
+
// Parameterised read
|
|
49
|
+
const row = await client.query(
|
|
50
|
+
'MATCH (p:Person {id: $pid}) RETURN p.name AS name',
|
|
51
|
+
{ params: { pid: 'person-01' } },
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Write (CREATE, MERGE, SET, DELETE)
|
|
55
|
+
await client.query("CREATE (n:Company {id: 'acme', name: 'Acme Corp'})");
|
|
56
|
+
|
|
57
|
+
// Bulk ingest
|
|
58
|
+
await client.ingest({
|
|
59
|
+
label: 'Product',
|
|
60
|
+
rows: [
|
|
61
|
+
{ id: 'prod-1', name: 'Widget', price: 9.99 },
|
|
62
|
+
{ id: 'prod-2', name: 'Gadget', price: 24.99 },
|
|
63
|
+
],
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Stats
|
|
67
|
+
const stats = await client.stats();
|
|
68
|
+
console.log(stats.nodes_live, stats.edges);
|
|
69
|
+
|
|
70
|
+
// Rule suggestions
|
|
71
|
+
const report = await client.suggest();
|
|
72
|
+
console.log(report.truncated, report.suggestions.length);
|
|
73
|
+
|
|
74
|
+
// Graph algorithms
|
|
75
|
+
const pr = await client.algo('pagerank');
|
|
76
|
+
console.log(pr.scores.slice(0, 5)); // top-5 nodes by PageRank
|
|
77
|
+
|
|
78
|
+
const wcc = await client.algo('wcc');
|
|
79
|
+
console.log(wcc.components.length);
|
|
80
|
+
|
|
81
|
+
const deg = await client.algo('degree', { direction: 'out' });
|
|
82
|
+
console.log(deg.scores.slice(0, 5));
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## WebSocket subscriptions
|
|
88
|
+
|
|
89
|
+
Subscribe to post-commit events over `GET /subscribe`.
|
|
90
|
+
|
|
91
|
+
**No auto-reconnect in v1.** When the connection drops, no further events are
|
|
92
|
+
delivered. Reconnect manually if required.
|
|
93
|
+
|
|
94
|
+
**The `lagged` event** is passed to `onEvent` like any other event. It fires
|
|
95
|
+
when the server's per-subscriber queue (65,536 events) overflows. On receiving
|
|
96
|
+
it, re-read the affected graph state for lossless consumers.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import WS from 'ws'; // Node < 21: npm install ws
|
|
100
|
+
import type { WsConstructor } from 'mushroomdb-client';
|
|
101
|
+
|
|
102
|
+
const handle = await client.subscribe(
|
|
103
|
+
{
|
|
104
|
+
rules: ['skill_fit'], // rule-fire events
|
|
105
|
+
writes: true, // node/prop write events
|
|
106
|
+
wsConstructor: WS as unknown as WsConstructor, // Node < 21 only
|
|
107
|
+
},
|
|
108
|
+
(ev) => {
|
|
109
|
+
switch (ev.type) {
|
|
110
|
+
case 'edge_fired':
|
|
111
|
+
console.log(`Rule ${ev.rule}: ${ev.src_key} → ${ev.dst_key} (seq ${ev.commit_seq})`);
|
|
112
|
+
break;
|
|
113
|
+
case 'node_inserted':
|
|
114
|
+
console.log(`New node: ${ev.key} (label ${ev.label})`);
|
|
115
|
+
break;
|
|
116
|
+
case 'lagged':
|
|
117
|
+
console.warn(`Missed ${ev.missed} events — re-read state`);
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// ... do work ...
|
|
124
|
+
|
|
125
|
+
// Always await close to avoid dangling handles.
|
|
126
|
+
await handle.close();
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## API reference
|
|
132
|
+
|
|
133
|
+
### `new MushroomClient(baseUrl: string, opts?: { token?: string })`
|
|
134
|
+
|
|
135
|
+
Create a client. `baseUrl` is the HTTP base URL printed by `mushroomdb serve`,
|
|
136
|
+
e.g. `"http://127.0.0.1:8080"`. When `opts.token` is set, every HTTP fetch
|
|
137
|
+
sends `Authorization: Bearer <token>`.
|
|
138
|
+
|
|
139
|
+
### `client.query(cypher, opts?) → Promise<QueryResult>`
|
|
140
|
+
|
|
141
|
+
Run a Cypher query (read or write). The server detects write statements and
|
|
142
|
+
acquires the write lock automatically. Returns `{ columns: string[], rows: CellValue[][] }`.
|
|
143
|
+
|
|
144
|
+
`opts.params` — bound parameters; values must be JSON scalars
|
|
145
|
+
(`string | number | boolean`).
|
|
146
|
+
|
|
147
|
+
### `client.ingest(req) → Promise<IngestReport>`
|
|
148
|
+
|
|
149
|
+
Bulk-ingest nodes and optional edges. `req.label`, `req.rows` are required.
|
|
150
|
+
See `IngestOptions` and `IngestEdge` for advanced options.
|
|
151
|
+
|
|
152
|
+
### `client.stats() → Promise<Stats>`
|
|
153
|
+
|
|
154
|
+
Returns live node/edge counts and per-rule statistics.
|
|
155
|
+
|
|
156
|
+
### `client.suggest() → Promise<SuggestReport>`
|
|
157
|
+
|
|
158
|
+
Profile the database and return candidate linking rules. CPU-intensive;
|
|
159
|
+
capped at 5 s server-side. The `truncated` flag is `true` when the budget
|
|
160
|
+
fires early.
|
|
161
|
+
|
|
162
|
+
### `client.explain(a, b) → Promise<Explanation[]>`
|
|
163
|
+
|
|
164
|
+
Wires `GET /explain?a=&b=`. Returns rule-derived edges between two node keys.
|
|
165
|
+
|
|
166
|
+
### `client.createRule(def) → Promise<void>`
|
|
167
|
+
|
|
168
|
+
Wires `POST /rules` with a `RuleDef` JSON body.
|
|
169
|
+
|
|
170
|
+
### `client.node(key) → Promise<NodeInfo | null>`
|
|
171
|
+
|
|
172
|
+
Wires `GET /node/{key}`. Returns `null` for an unknown key (HTTP 404).
|
|
173
|
+
|
|
174
|
+
### `client.neighborhood(key, opts?) → Promise<Neighborhood>`
|
|
175
|
+
|
|
176
|
+
Wires `GET /node/{key}/neighborhood`. `opts.depth` defaults to the server's
|
|
177
|
+
(1). Result columns are `key`, `label`, `depth`.
|
|
178
|
+
|
|
179
|
+
### `client.algo(name, config?) → Promise<AlgoReport>`
|
|
180
|
+
|
|
181
|
+
Run a graph algorithm:
|
|
182
|
+
|
|
183
|
+
| name | config type | result type |
|
|
184
|
+
|------|-------------|-------------|
|
|
185
|
+
| `"pagerank"` | `PageRankConfig` | `PageRankReport` — `{ scores: [key, score][], converged }` |
|
|
186
|
+
| `"wcc"` | `WccConfig` | `WccReport` — `{ components: [key, component_id][], truncated }` |
|
|
187
|
+
| `"degree"` | `DegreeConfig` | `DegreeReport` — `{ scores: [key, degree][], truncated }` |
|
|
188
|
+
|
|
189
|
+
All config fields are optional; server defaults apply.
|
|
190
|
+
|
|
191
|
+
### `client.subscribe(opts, onEvent) → Promise<SubscribeHandle>`
|
|
192
|
+
|
|
193
|
+
Open a WebSocket subscription. Returns a handle with `close(): Promise<void>`.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Error handling
|
|
198
|
+
|
|
199
|
+
All HTTP errors throw `MushroomError` with the server's error detail in
|
|
200
|
+
`err.detail` (and `err.message`):
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
import { MushroomError } from 'mushroomdb-client';
|
|
204
|
+
try {
|
|
205
|
+
await client.query('BAD CYPHER');
|
|
206
|
+
} catch (err) {
|
|
207
|
+
if (err instanceof MushroomError) {
|
|
208
|
+
console.error('Query failed:', err.detail);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Known server-side limitations
|
|
216
|
+
|
|
217
|
+
These are limitations of the mushroomdb server's Cypher implementation that
|
|
218
|
+
affect how you write queries through this client.
|
|
219
|
+
|
|
220
|
+
### 1. `CREATE ... RETURN` is supported
|
|
221
|
+
|
|
222
|
+
You can include a `RETURN` clause directly after `CREATE` or `MERGE` to get
|
|
223
|
+
back the created or matched bindings in a single statement:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
// Single-statement create + return:
|
|
227
|
+
const result = await client.query(
|
|
228
|
+
"CREATE (n:Widget {id: 'w1', name: 'Sprocket'}) RETURN n.name AS nm"
|
|
229
|
+
);
|
|
230
|
+
// result.rows[0][0] === 'Sprocket'
|
|
231
|
+
|
|
232
|
+
// MERGE + RETURN (returns the node whether created or matched):
|
|
233
|
+
const r2 = await client.query(
|
|
234
|
+
"MERGE (n:Tag {id: 'rust'}) RETURN n"
|
|
235
|
+
);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### 2. Every node requires a string `id` property
|
|
239
|
+
|
|
240
|
+
When using `CREATE`, nodes must include an `id` field with a string value.
|
|
241
|
+
This is the key the server uses to identify the node:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
// WRONG — missing 'id'
|
|
245
|
+
await client.query("CREATE (n:Widget {name: 'Sprocket'})");
|
|
246
|
+
|
|
247
|
+
// CORRECT
|
|
248
|
+
await client.query("CREATE (n:Widget {id: 'w1', name: 'Sprocket'})");
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Node-only vs browser-compatible
|
|
254
|
+
|
|
255
|
+
| Feature | Browser | Node 18+ |
|
|
256
|
+
|---------|---------|----------|
|
|
257
|
+
| `query`, `ingest`, `stats`, `suggest`, `algo`, `explain`, `createRule`, `node`, `neighborhood` | Yes (uses `fetch`) | Yes |
|
|
258
|
+
| `subscribe` | Yes (uses global `WebSocket`) | Requires `wsConstructor` option + `ws` package |
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Running the tests
|
|
263
|
+
|
|
264
|
+
```sh
|
|
265
|
+
cd clients/typescript
|
|
266
|
+
npm ci
|
|
267
|
+
npm test
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Tests spawn a real `mushroomdb` binary. The first run builds it with `cargo`
|
|
271
|
+
(takes ~30 s on a cold cache). If the build fails, all tests are **skipped**
|
|
272
|
+
with a clear message (not marked as failed).
|
|
273
|
+
|
|
274
|
+
Set `CARGO=/path/to/cargo` to use a specific cargo binary (required in CI or
|
|
275
|
+
on machines where cargo is not on `PATH`).
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## License
|
|
280
|
+
|
|
281
|
+
Apache-2.0. Copyright 2026 Matthew Sherlin.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for the mushroomdb server.
|
|
3
|
+
*
|
|
4
|
+
* Uses the browser-standard `fetch` API (built into Node 18+).
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { MushroomClient } from 'mushroomdb-client';
|
|
8
|
+
*
|
|
9
|
+
* const client = new MushroomClient('http://127.0.0.1:8080');
|
|
10
|
+
* const result = await client.query('MATCH (n:Person) RETURN n.name LIMIT 10');
|
|
11
|
+
* console.log(result.columns, result.rows);
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
import { MushroomError, type DegreeConfig, type DegreeReport, type Explanation, type IngestReport, type IngestRequest, type Neighborhood, type NodeInfo, type PageRankConfig, type PageRankReport, type QueryResult, type RuleDef, type Stats, type SuggestReport, type WccConfig, type WccReport } from "./types.js";
|
|
15
|
+
import { type SubscribeHandle, type SubscribeOptions, type WsConstructor } from "./ws.js";
|
|
16
|
+
export type { AlgoDir, AlgoReport, CellValue, DegreeConfig, DegreeReport, Explanation, IngestEdge, IngestOptions, IngestReport, IngestRequest, Neighborhood, NodeInfo, PageRankConfig, PageRankReport, PredicateKind, PredicateSummary, QueryResult, RuleDef, RulePredicate, RuleStats, RuleSuggestion, Stats, SuggestReport, WccConfig, WccReport, } from "./types.js";
|
|
17
|
+
export { MushroomError };
|
|
18
|
+
/** Optional constructor flags for {@link MushroomClient}. */
|
|
19
|
+
export interface ClientOptions {
|
|
20
|
+
/**
|
|
21
|
+
* When set, sent as `Authorization: Bearer <token>` on every HTTP fetch.
|
|
22
|
+
* Cookie auth is a browser/explorer concern and is not implemented here.
|
|
23
|
+
*/
|
|
24
|
+
token?: string;
|
|
25
|
+
}
|
|
26
|
+
/** Parameters for a Cypher query. Values must be JSON scalars. */
|
|
27
|
+
export type QueryParams = Record<string, string | number | boolean>;
|
|
28
|
+
/** Options accepted by {@link MushroomClient.query}. */
|
|
29
|
+
export interface QueryOptions {
|
|
30
|
+
/** Bound parameters. Values must be JSON scalars (string | number | boolean). */
|
|
31
|
+
params?: QueryParams;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* HTTP + WebSocket client for mushroomdb.
|
|
35
|
+
*
|
|
36
|
+
* All methods use the browser-standard `fetch` API and are therefore
|
|
37
|
+
* compatible with both Node.js 18+ and modern browsers.
|
|
38
|
+
*
|
|
39
|
+
* **Node-only**: The `subscribe` method requires a WebSocket implementation.
|
|
40
|
+
* In Node < 21, install the `ws` package and pass `wsConstructor` in the
|
|
41
|
+
* subscribe options. See {@link SubscribeOptions}.
|
|
42
|
+
*/
|
|
43
|
+
export declare class MushroomClient {
|
|
44
|
+
private readonly baseUrl;
|
|
45
|
+
private readonly wsBase;
|
|
46
|
+
private readonly token;
|
|
47
|
+
/**
|
|
48
|
+
* @param baseUrl HTTP base URL of the mushroomdb server, e.g.
|
|
49
|
+
* `"http://127.0.0.1:8080"`. Trailing slash is stripped.
|
|
50
|
+
* @param opts Optional `{ token }` — sent as `Authorization: Bearer`.
|
|
51
|
+
*/
|
|
52
|
+
constructor(baseUrl: string, opts?: ClientOptions);
|
|
53
|
+
private url;
|
|
54
|
+
private wsUrl;
|
|
55
|
+
private authHeaders;
|
|
56
|
+
/**
|
|
57
|
+
* Execute a fetch and decode the JSON body.
|
|
58
|
+
* Throws {@link MushroomError} on non-2xx responses.
|
|
59
|
+
*/
|
|
60
|
+
private fetchJson;
|
|
61
|
+
/**
|
|
62
|
+
* Run a Cypher query (read or write).
|
|
63
|
+
*
|
|
64
|
+
* The server auto-detects write statements (`CREATE`, `MERGE`, `SET`,
|
|
65
|
+
* `DELETE`) and acquires the appropriate lock. Both read and write queries
|
|
66
|
+
* go to `POST /query?format=json`.
|
|
67
|
+
*
|
|
68
|
+
* @param cypher Cypher query string.
|
|
69
|
+
* @param opts Optional bound parameters (JSON scalar values only).
|
|
70
|
+
* @returns Column names and a 2-D array of {@link CellValue} rows.
|
|
71
|
+
*/
|
|
72
|
+
query(cypher: string, opts?: QueryOptions): Promise<QueryResult>;
|
|
73
|
+
/**
|
|
74
|
+
* Ingest nodes (and optional edges) into the database.
|
|
75
|
+
*
|
|
76
|
+
* Wraps `POST /ingest`. The server acquires the write lock, applies the
|
|
77
|
+
* rows to the WAL, and runs all rules incrementally.
|
|
78
|
+
*
|
|
79
|
+
* @param req Ingest payload — `label`, `rows`, optional `options` and `edges`.
|
|
80
|
+
* @returns Server ingest report (opaque; check for absence of errors).
|
|
81
|
+
*/
|
|
82
|
+
ingest(req: IngestRequest): Promise<IngestReport>;
|
|
83
|
+
/**
|
|
84
|
+
* Get database-wide statistics.
|
|
85
|
+
*
|
|
86
|
+
* Wraps `GET /stats`. Returns live node/edge counts and per-rule stats.
|
|
87
|
+
*/
|
|
88
|
+
stats(): Promise<Stats>;
|
|
89
|
+
/**
|
|
90
|
+
* Profile the database and return rule suggestions.
|
|
91
|
+
*
|
|
92
|
+
* Wraps `GET /suggest`. CPU-intensive — runs in the server's blocking
|
|
93
|
+
* thread-pool with a 5-second global budget. The {@link SuggestReport}
|
|
94
|
+
* includes a `truncated` flag when the budget fires early.
|
|
95
|
+
*
|
|
96
|
+
* Suggestions are not auto-applied; call `POST /rules` to create a rule.
|
|
97
|
+
*/
|
|
98
|
+
suggest(): Promise<SuggestReport>;
|
|
99
|
+
/**
|
|
100
|
+
* Run a graph algorithm.
|
|
101
|
+
*
|
|
102
|
+
* Wraps `POST /algo/{pagerank|wcc|degree}`.
|
|
103
|
+
*
|
|
104
|
+
* @param algo Algorithm name.
|
|
105
|
+
* @param config Optional algorithm-specific configuration.
|
|
106
|
+
* @returns Algorithm report — see {@link PageRankReport}, {@link WccReport},
|
|
107
|
+
* {@link DegreeReport}.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```ts
|
|
111
|
+
* const pr = await client.algo('pagerank') as PageRankReport;
|
|
112
|
+
* console.log(pr.scores.slice(0, 5));
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
algo(algo: "pagerank", config?: PageRankConfig): Promise<PageRankReport>;
|
|
116
|
+
algo(algo: "wcc", config?: WccConfig): Promise<WccReport>;
|
|
117
|
+
algo(algo: "degree", config?: DegreeConfig): Promise<DegreeReport>;
|
|
118
|
+
/**
|
|
119
|
+
* Explain rule-derived edges between two node keys.
|
|
120
|
+
*
|
|
121
|
+
* Wraps `GET /explain?a=&b=`.
|
|
122
|
+
*/
|
|
123
|
+
explain(a: string, b: string): Promise<Explanation[]>;
|
|
124
|
+
/**
|
|
125
|
+
* Create a derivation rule.
|
|
126
|
+
*
|
|
127
|
+
* Wraps `POST /rules`. The server acquires the write lock, validates the
|
|
128
|
+
* {@link RuleDef}, and backfills matching pairs.
|
|
129
|
+
*/
|
|
130
|
+
createRule(def: RuleDef): Promise<void>;
|
|
131
|
+
/**
|
|
132
|
+
* Fetch a node by key.
|
|
133
|
+
*
|
|
134
|
+
* Wraps `GET /node/{key}`. Returns `null` when the server answers 404
|
|
135
|
+
* (unknown key). Other HTTP errors throw {@link MushroomError}.
|
|
136
|
+
*/
|
|
137
|
+
node(key: string): Promise<NodeInfo | null>;
|
|
138
|
+
/**
|
|
139
|
+
* Depth-N neighborhood of a node.
|
|
140
|
+
*
|
|
141
|
+
* Wraps `GET /node/{key}/neighborhood`. Default depth is the server's
|
|
142
|
+
* (1). Columns are `key`, `label`, `depth`.
|
|
143
|
+
*/
|
|
144
|
+
neighborhood(key: string, opts?: {
|
|
145
|
+
depth?: number;
|
|
146
|
+
}): Promise<Neighborhood>;
|
|
147
|
+
/**
|
|
148
|
+
* Subscribe to post-commit events over WebSocket (`GET /subscribe`).
|
|
149
|
+
*
|
|
150
|
+
* Returns a promise that resolves when the server acknowledges the
|
|
151
|
+
* subscription (`{"subscribed":true}`). After that, `onEvent` is called
|
|
152
|
+
* for each {@link DbEvent}, including {@link DbEvent.lagged} frames.
|
|
153
|
+
*
|
|
154
|
+
* **No auto-reconnect in v1.** When the connection drops, no further events
|
|
155
|
+
* are delivered. Reconnect manually if required.
|
|
156
|
+
*
|
|
157
|
+
* **Always await `handle.close()`** when done — an open WebSocket keeps the
|
|
158
|
+
* Node.js event loop alive and will cause test hangs.
|
|
159
|
+
*
|
|
160
|
+
* **Node.js < 21**: pass `wsConstructor` — see {@link SubscribeOptions}.
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```ts
|
|
164
|
+
* import WS from 'ws';
|
|
165
|
+
* const handle = await client.subscribe(
|
|
166
|
+
* { writes: true, wsConstructor: WS as WsConstructor },
|
|
167
|
+
* (ev) => console.log(ev),
|
|
168
|
+
* );
|
|
169
|
+
* // ... do work ...
|
|
170
|
+
* await handle.close();
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
subscribe(opts: SubscribeOptions, onEvent: (event: import("./types.js").DbEvent) => void): Promise<SubscribeHandle>;
|
|
174
|
+
}
|
|
175
|
+
export type { SubscribeHandle, SubscribeOptions, WsConstructor };
|
|
176
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for the mushroomdb server.
|
|
3
|
+
*
|
|
4
|
+
* Uses the browser-standard `fetch` API (built into Node 18+).
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { MushroomClient } from 'mushroomdb-client';
|
|
8
|
+
*
|
|
9
|
+
* const client = new MushroomClient('http://127.0.0.1:8080');
|
|
10
|
+
* const result = await client.query('MATCH (n:Person) RETURN n.name LIMIT 10');
|
|
11
|
+
* console.log(result.columns, result.rows);
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
import { MushroomError, } from "./types.js";
|
|
15
|
+
import { subscribe as wsSubscribe, } from "./ws.js";
|
|
16
|
+
export { MushroomError };
|
|
17
|
+
/**
|
|
18
|
+
* HTTP + WebSocket client for mushroomdb.
|
|
19
|
+
*
|
|
20
|
+
* All methods use the browser-standard `fetch` API and are therefore
|
|
21
|
+
* compatible with both Node.js 18+ and modern browsers.
|
|
22
|
+
*
|
|
23
|
+
* **Node-only**: The `subscribe` method requires a WebSocket implementation.
|
|
24
|
+
* In Node < 21, install the `ws` package and pass `wsConstructor` in the
|
|
25
|
+
* subscribe options. See {@link SubscribeOptions}.
|
|
26
|
+
*/
|
|
27
|
+
export class MushroomClient {
|
|
28
|
+
baseUrl;
|
|
29
|
+
wsBase;
|
|
30
|
+
token;
|
|
31
|
+
/**
|
|
32
|
+
* @param baseUrl HTTP base URL of the mushroomdb server, e.g.
|
|
33
|
+
* `"http://127.0.0.1:8080"`. Trailing slash is stripped.
|
|
34
|
+
* @param opts Optional `{ token }` — sent as `Authorization: Bearer`.
|
|
35
|
+
*/
|
|
36
|
+
constructor(baseUrl, opts) {
|
|
37
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
38
|
+
this.wsBase = this.baseUrl.replace(/^http/, "ws");
|
|
39
|
+
this.token = opts?.token;
|
|
40
|
+
}
|
|
41
|
+
// -------------------------------------------------------------------------
|
|
42
|
+
// Internal helpers
|
|
43
|
+
// -------------------------------------------------------------------------
|
|
44
|
+
url(path) {
|
|
45
|
+
return `${this.baseUrl}${path}`;
|
|
46
|
+
}
|
|
47
|
+
wsUrl(path) {
|
|
48
|
+
return `${this.wsBase}${path}`;
|
|
49
|
+
}
|
|
50
|
+
authHeaders() {
|
|
51
|
+
return this.token ? { Authorization: `Bearer ${this.token}` } : {};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Execute a fetch and decode the JSON body.
|
|
55
|
+
* Throws {@link MushroomError} on non-2xx responses.
|
|
56
|
+
*/
|
|
57
|
+
async fetchJson(path, init) {
|
|
58
|
+
const resp = await fetch(this.url(path), {
|
|
59
|
+
...init,
|
|
60
|
+
headers: {
|
|
61
|
+
"Content-Type": "application/json",
|
|
62
|
+
Accept: "application/json",
|
|
63
|
+
...this.authHeaders(),
|
|
64
|
+
...(init?.headers ?? {}),
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
68
|
+
const body = await resp.json();
|
|
69
|
+
if (!resp.ok) {
|
|
70
|
+
const detail = typeof body?.error === "string" ? body.error : `HTTP ${resp.status}`;
|
|
71
|
+
throw new MushroomError(detail);
|
|
72
|
+
}
|
|
73
|
+
return body;
|
|
74
|
+
}
|
|
75
|
+
// -------------------------------------------------------------------------
|
|
76
|
+
// HTTP endpoints
|
|
77
|
+
// -------------------------------------------------------------------------
|
|
78
|
+
/**
|
|
79
|
+
* Run a Cypher query (read or write).
|
|
80
|
+
*
|
|
81
|
+
* The server auto-detects write statements (`CREATE`, `MERGE`, `SET`,
|
|
82
|
+
* `DELETE`) and acquires the appropriate lock. Both read and write queries
|
|
83
|
+
* go to `POST /query?format=json`.
|
|
84
|
+
*
|
|
85
|
+
* @param cypher Cypher query string.
|
|
86
|
+
* @param opts Optional bound parameters (JSON scalar values only).
|
|
87
|
+
* @returns Column names and a 2-D array of {@link CellValue} rows.
|
|
88
|
+
*/
|
|
89
|
+
async query(cypher, opts) {
|
|
90
|
+
return this.fetchJson("/query?format=json", {
|
|
91
|
+
method: "POST",
|
|
92
|
+
body: JSON.stringify({
|
|
93
|
+
cypher,
|
|
94
|
+
...(opts?.params ? { params: opts.params } : {}),
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Ingest nodes (and optional edges) into the database.
|
|
100
|
+
*
|
|
101
|
+
* Wraps `POST /ingest`. The server acquires the write lock, applies the
|
|
102
|
+
* rows to the WAL, and runs all rules incrementally.
|
|
103
|
+
*
|
|
104
|
+
* @param req Ingest payload — `label`, `rows`, optional `options` and `edges`.
|
|
105
|
+
* @returns Server ingest report (opaque; check for absence of errors).
|
|
106
|
+
*/
|
|
107
|
+
async ingest(req) {
|
|
108
|
+
return this.fetchJson("/ingest", {
|
|
109
|
+
method: "POST",
|
|
110
|
+
body: JSON.stringify(req),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get database-wide statistics.
|
|
115
|
+
*
|
|
116
|
+
* Wraps `GET /stats`. Returns live node/edge counts and per-rule stats.
|
|
117
|
+
*/
|
|
118
|
+
async stats() {
|
|
119
|
+
return this.fetchJson("/stats");
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Profile the database and return rule suggestions.
|
|
123
|
+
*
|
|
124
|
+
* Wraps `GET /suggest`. CPU-intensive — runs in the server's blocking
|
|
125
|
+
* thread-pool with a 5-second global budget. The {@link SuggestReport}
|
|
126
|
+
* includes a `truncated` flag when the budget fires early.
|
|
127
|
+
*
|
|
128
|
+
* Suggestions are not auto-applied; call `POST /rules` to create a rule.
|
|
129
|
+
*/
|
|
130
|
+
async suggest() {
|
|
131
|
+
return this.fetchJson("/suggest");
|
|
132
|
+
}
|
|
133
|
+
async algo(algo, config) {
|
|
134
|
+
// The server structs carry #[serde(default)], so sending only the fields
|
|
135
|
+
// the caller explicitly set (or an empty body {}) is valid — the server
|
|
136
|
+
// fills in its own defaults for any missing fields.
|
|
137
|
+
return this.fetchJson(`/algo/${algo}`, {
|
|
138
|
+
method: "POST",
|
|
139
|
+
body: JSON.stringify(config ?? {}),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Explain rule-derived edges between two node keys.
|
|
144
|
+
*
|
|
145
|
+
* Wraps `GET /explain?a=&b=`.
|
|
146
|
+
*/
|
|
147
|
+
async explain(a, b) {
|
|
148
|
+
const qs = new URLSearchParams({ a, b });
|
|
149
|
+
return this.fetchJson(`/explain?${qs.toString()}`);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Create a derivation rule.
|
|
153
|
+
*
|
|
154
|
+
* Wraps `POST /rules`. The server acquires the write lock, validates the
|
|
155
|
+
* {@link RuleDef}, and backfills matching pairs.
|
|
156
|
+
*/
|
|
157
|
+
async createRule(def) {
|
|
158
|
+
await this.fetchJson("/rules", {
|
|
159
|
+
method: "POST",
|
|
160
|
+
body: JSON.stringify(def),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Fetch a node by key.
|
|
165
|
+
*
|
|
166
|
+
* Wraps `GET /node/{key}`. Returns `null` when the server answers 404
|
|
167
|
+
* (unknown key). Other HTTP errors throw {@link MushroomError}.
|
|
168
|
+
*/
|
|
169
|
+
async node(key) {
|
|
170
|
+
const resp = await fetch(this.url(`/node/${encodeURIComponent(key)}`), {
|
|
171
|
+
headers: {
|
|
172
|
+
Accept: "application/json",
|
|
173
|
+
...this.authHeaders(),
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
177
|
+
const body = await resp.json();
|
|
178
|
+
if (resp.status === 404) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
if (!resp.ok) {
|
|
182
|
+
const detail = typeof body?.error === "string" ? body.error : `HTTP ${resp.status}`;
|
|
183
|
+
throw new MushroomError(detail);
|
|
184
|
+
}
|
|
185
|
+
return body;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Depth-N neighborhood of a node.
|
|
189
|
+
*
|
|
190
|
+
* Wraps `GET /node/{key}/neighborhood`. Default depth is the server's
|
|
191
|
+
* (1). Columns are `key`, `label`, `depth`.
|
|
192
|
+
*/
|
|
193
|
+
async neighborhood(key, opts) {
|
|
194
|
+
const qs = new URLSearchParams();
|
|
195
|
+
if (opts?.depth !== undefined) {
|
|
196
|
+
qs.set("depth", String(opts.depth));
|
|
197
|
+
}
|
|
198
|
+
const query = qs.toString();
|
|
199
|
+
const path = `/node/${encodeURIComponent(key)}/neighborhood${query ? `?${query}` : ""}`;
|
|
200
|
+
return this.fetchJson(path);
|
|
201
|
+
}
|
|
202
|
+
// -------------------------------------------------------------------------
|
|
203
|
+
// WebSocket
|
|
204
|
+
// -------------------------------------------------------------------------
|
|
205
|
+
/**
|
|
206
|
+
* Subscribe to post-commit events over WebSocket (`GET /subscribe`).
|
|
207
|
+
*
|
|
208
|
+
* Returns a promise that resolves when the server acknowledges the
|
|
209
|
+
* subscription (`{"subscribed":true}`). After that, `onEvent` is called
|
|
210
|
+
* for each {@link DbEvent}, including {@link DbEvent.lagged} frames.
|
|
211
|
+
*
|
|
212
|
+
* **No auto-reconnect in v1.** When the connection drops, no further events
|
|
213
|
+
* are delivered. Reconnect manually if required.
|
|
214
|
+
*
|
|
215
|
+
* **Always await `handle.close()`** when done — an open WebSocket keeps the
|
|
216
|
+
* Node.js event loop alive and will cause test hangs.
|
|
217
|
+
*
|
|
218
|
+
* **Node.js < 21**: pass `wsConstructor` — see {@link SubscribeOptions}.
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* ```ts
|
|
222
|
+
* import WS from 'ws';
|
|
223
|
+
* const handle = await client.subscribe(
|
|
224
|
+
* { writes: true, wsConstructor: WS as WsConstructor },
|
|
225
|
+
* (ev) => console.log(ev),
|
|
226
|
+
* );
|
|
227
|
+
* // ... do work ...
|
|
228
|
+
* await handle.close();
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
subscribe(opts, onEvent) {
|
|
232
|
+
let url = this.wsUrl("/subscribe");
|
|
233
|
+
if (this.token) {
|
|
234
|
+
url += `?token=${encodeURIComponent(this.token)}`;
|
|
235
|
+
}
|
|
236
|
+
return wsSubscribe(url, opts, onEvent);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
//# sourceMappingURL=client.js.map
|