leangraph 1.0.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 +21 -0
- package/README.md +456 -0
- package/dist/auth.d.ts +66 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +148 -0
- package/dist/auth.js.map +1 -0
- package/dist/backup.d.ts +51 -0
- package/dist/backup.d.ts.map +1 -0
- package/dist/backup.js +201 -0
- package/dist/backup.js.map +1 -0
- package/dist/cli-helpers.d.ts +17 -0
- package/dist/cli-helpers.d.ts.map +1 -0
- package/dist/cli-helpers.js +121 -0
- package/dist/cli-helpers.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +660 -0
- package/dist/cli.js.map +1 -0
- package/dist/db.d.ts +118 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +720 -0
- package/dist/db.js.map +1 -0
- package/dist/executor.d.ts +663 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +8578 -0
- package/dist/executor.js.map +1 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +86 -0
- package/dist/index.js.map +1 -0
- package/dist/local.d.ts +7 -0
- package/dist/local.d.ts.map +1 -0
- package/dist/local.js +119 -0
- package/dist/local.js.map +1 -0
- package/dist/parser.d.ts +365 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +2711 -0
- package/dist/parser.js.map +1 -0
- package/dist/property-value.d.ts +3 -0
- package/dist/property-value.d.ts.map +1 -0
- package/dist/property-value.js +30 -0
- package/dist/property-value.js.map +1 -0
- package/dist/remote.d.ts +6 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +93 -0
- package/dist/remote.js.map +1 -0
- package/dist/routes.d.ts +31 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +202 -0
- package/dist/routes.js.map +1 -0
- package/dist/server.d.ts +16 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +25 -0
- package/dist/server.js.map +1 -0
- package/dist/translator.d.ts +330 -0
- package/dist/translator.d.ts.map +1 -0
- package/dist/translator.js +13712 -0
- package/dist/translator.js.map +1 -0
- package/dist/types.d.ts +136 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +21 -0
- package/dist/types.js.map +1 -0
- package/package.json +77 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Conrad Lelubre
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
# LeanGraph
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/leangraph)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://opencypher.org/)
|
|
6
|
+
|
|
7
|
+
A lightweight, embeddable graph database with **full Cypher query support**, powered by SQLite.
|
|
8
|
+
|
|
9
|
+
> **100% openCypher TCK Compliance** — LeanGraph passes all 2,684 test scenarios from the openCypher Technology Compatibility Kit (Neo4j 3.5 baseline). Every Cypher feature that Neo4j 3.5 supports, LeanGraph supports.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install leangraph
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Native Dependencies
|
|
18
|
+
|
|
19
|
+
The `better-sqlite3` native module is an **optional dependency**:
|
|
20
|
+
|
|
21
|
+
- **Production mode** (remote HTTP client): No native dependencies, no compilation required
|
|
22
|
+
- **Development mode** (local SQLite): Requires `better-sqlite3`
|
|
23
|
+
|
|
24
|
+
If you only connect to a remote GraphDB server, you can skip native compilation entirely:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install leangraph --ignore-optional
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
For local/embedded mode, ensure `better-sqlite3` is installed:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install leangraph better-sqlite3
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { GraphDB } from 'leangraph';
|
|
40
|
+
|
|
41
|
+
const db = await GraphDB({
|
|
42
|
+
project: 'myapp', // or process.env.GRAPHDB_PROJECT
|
|
43
|
+
apiKey: 'lg_xxx', // or process.env.GRAPHDB_API_KEY
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Create nodes and relationships
|
|
47
|
+
await db.execute(`
|
|
48
|
+
CREATE (alice:User {name: 'Alice'})-[:FOLLOWS]->(bob:User {name: 'Bob'})
|
|
49
|
+
`);
|
|
50
|
+
|
|
51
|
+
// Query the graph
|
|
52
|
+
const users = await db.query('MATCH (u:User) RETURN u.name AS name');
|
|
53
|
+
console.log(users); // [{ name: 'Alice' }, { name: 'Bob' }]
|
|
54
|
+
|
|
55
|
+
db.close();
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Development vs Production Mode
|
|
59
|
+
|
|
60
|
+
LeanGraph automatically adapts based on the `NODE_ENV` environment variable:
|
|
61
|
+
|
|
62
|
+
| Mode | `NODE_ENV` | Behavior |
|
|
63
|
+
|------|-----------|----------|
|
|
64
|
+
| **Development** | `development` | Uses local SQLite database. `url` and `apiKey` are ignored. |
|
|
65
|
+
| **Production** | `production` (or unset) | Connects to remote server via HTTP. `url` and `apiKey` are required. |
|
|
66
|
+
|
|
67
|
+
This means you can use the **exact same code** in both environments:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// Works in both development and production!
|
|
71
|
+
const db = await GraphDB({ project: 'myapp' });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Development Mode
|
|
75
|
+
|
|
76
|
+
When `NODE_ENV=development`:
|
|
77
|
+
- A local SQLite database is created automatically
|
|
78
|
+
- No server setup required
|
|
79
|
+
- `url` and `apiKey` parameters are ignored
|
|
80
|
+
- Data persists at `./data/{env}/{project}.db` by default
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Run your app in development mode
|
|
84
|
+
NODE_ENV=development node app.js
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Production Mode
|
|
88
|
+
|
|
89
|
+
When `NODE_ENV=production` (or unset):
|
|
90
|
+
- Connects to a remote GraphDB server via HTTP
|
|
91
|
+
- `url` and `apiKey` are required
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Run your app in production mode
|
|
95
|
+
NODE_ENV=production GRAPHDB_API_KEY=xxx node app.js
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Configuration Options
|
|
99
|
+
|
|
100
|
+
| Option | Type | Required | Default | Description |
|
|
101
|
+
|--------|------|----------|---------|-------------|
|
|
102
|
+
| `url` | `string` | No | `GRAPHDB_URL` or `https://leangraph.io` | Base URL of the GraphDB server (production only) |
|
|
103
|
+
| `project` | `string` | Yes | `GRAPHDB_PROJECT` | Project name |
|
|
104
|
+
| `apiKey` | `string` | No | `GRAPHDB_API_KEY` | API key for authentication (production only) |
|
|
105
|
+
| `env` | `string` | No | `NODE_ENV` or `production` | Environment (determines database isolation) |
|
|
106
|
+
| `dataPath` | `string` | No | `GRAPHDB_DATA_PATH` or `./data` | Path for local data storage (development only). Use `':memory:'` for in-memory database |
|
|
107
|
+
|
|
108
|
+
### Examples
|
|
109
|
+
|
|
110
|
+
**Production** (default when `NODE_ENV` is unset or `production`):
|
|
111
|
+
```typescript
|
|
112
|
+
const db = await GraphDB({
|
|
113
|
+
project: 'myapp', // or process.env.GRAPHDB_PROJECT
|
|
114
|
+
apiKey: 'lg_xxx', // or process.env.GRAPHDB_API_KEY
|
|
115
|
+
url: 'https://my-server', // or process.env.GRAPHDB_URL (default: leangraph.io)
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Development** (when `NODE_ENV=development`):
|
|
120
|
+
```typescript
|
|
121
|
+
const db = await GraphDB({
|
|
122
|
+
project: 'myapp', // or process.env.GRAPHDB_PROJECT
|
|
123
|
+
dataPath: './local-data', // or process.env.GRAPHDB_DATA_PATH (default: ./data)
|
|
124
|
+
});
|
|
125
|
+
// url and apiKey are ignored - uses local SQLite
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Testing** (when `NODE_ENV=development`):
|
|
129
|
+
```typescript
|
|
130
|
+
const db = await GraphDB({
|
|
131
|
+
project: 'test-project',
|
|
132
|
+
dataPath: ':memory:', // in-memory database, resets on each run
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## API Reference
|
|
137
|
+
|
|
138
|
+
### `GraphDB(options): Promise<GraphDBClient>`
|
|
139
|
+
|
|
140
|
+
Create a new GraphDB client. Returns a promise that resolves to a client instance.
|
|
141
|
+
|
|
142
|
+
### `db.query<T>(cypher, params?): Promise<T[]>`
|
|
143
|
+
|
|
144
|
+
Execute a Cypher query and return results as an array.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
const users = await db.query<{ name: string; age: number }>(
|
|
148
|
+
'MATCH (u:User) WHERE u.age > $minAge RETURN u.name AS name, u.age AS age',
|
|
149
|
+
{ minAge: 21 }
|
|
150
|
+
);
|
|
151
|
+
// users = [{ name: 'Alice', age: 25 }, { name: 'Bob', age: 30 }]
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### `db.execute(cypher, params?): Promise<void>`
|
|
155
|
+
|
|
156
|
+
Execute a mutating query (CREATE, SET, DELETE, MERGE) without expecting return data.
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
await db.execute('CREATE (n:User {name: $name, email: $email})', {
|
|
160
|
+
name: 'Alice',
|
|
161
|
+
email: 'alice@example.com'
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### `db.queryRaw<T>(cypher, params?): Promise<QueryResponse<T>>`
|
|
166
|
+
|
|
167
|
+
Execute a query and return the full response including metadata.
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
const response = await db.queryRaw('MATCH (n) RETURN n LIMIT 10');
|
|
171
|
+
console.log(response.meta.count); // Number of rows
|
|
172
|
+
console.log(response.meta.time_ms); // Query execution time in ms
|
|
173
|
+
console.log(response.data); // Array of results
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### `db.createNode(label, properties?): Promise<string>`
|
|
177
|
+
|
|
178
|
+
Create a node and return its ID.
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
const userId = await db.createNode('User', { name: 'Alice', email: 'alice@example.com' });
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### `db.getNode(label, filter): Promise<Record<string, unknown> | null>`
|
|
185
|
+
|
|
186
|
+
Find a node by label and properties.
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
const user = await db.getNode('User', { email: 'alice@example.com' });
|
|
190
|
+
if (user) {
|
|
191
|
+
console.log(user.name); // 'Alice'
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### `db.updateNode(id, properties): Promise<void>`
|
|
196
|
+
|
|
197
|
+
Update properties on a node.
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
await db.updateNode(userId, { name: 'Alice Smith', verified: true });
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### `db.deleteNode(id): Promise<void>`
|
|
204
|
+
|
|
205
|
+
Delete a node and all its relationships (DETACH DELETE).
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
await db.deleteNode(userId);
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### `db.createEdge(sourceId, type, targetId, properties?): Promise<void>`
|
|
212
|
+
|
|
213
|
+
Create a relationship between two nodes.
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
await db.createEdge(aliceId, 'FOLLOWS', bobId, { since: '2024-01-01' });
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### `db.health(): Promise<{ status: string; timestamp: string }>`
|
|
220
|
+
|
|
221
|
+
Check server health. In development mode, always returns `{ status: 'ok', ... }`.
|
|
222
|
+
|
|
223
|
+
### `db.close(): void`
|
|
224
|
+
|
|
225
|
+
Close the client and release resources. **Always call this when done.**
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
const db = await GraphDB({ ... });
|
|
229
|
+
try {
|
|
230
|
+
// ... use db
|
|
231
|
+
} finally {
|
|
232
|
+
db.close();
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Cypher Quick Reference
|
|
237
|
+
|
|
238
|
+
### Supported Clauses
|
|
239
|
+
|
|
240
|
+
| Clause | Example |
|
|
241
|
+
|--------|---------|
|
|
242
|
+
| `CREATE` | `CREATE (n:User {name: 'Alice'})` |
|
|
243
|
+
| `MATCH` | `MATCH (n:User) RETURN n` |
|
|
244
|
+
| `OPTIONAL MATCH` | `OPTIONAL MATCH (n)-[:KNOWS]->(m) RETURN m` |
|
|
245
|
+
| `MERGE` | `MERGE (n:User {email: $email})` |
|
|
246
|
+
| `WHERE` | `WHERE n.age > 21 AND n.active = true` |
|
|
247
|
+
| `SET` | `SET n.name = 'Bob', n.updated = true` |
|
|
248
|
+
| `DELETE` | `DELETE n` |
|
|
249
|
+
| `DETACH DELETE` | `DETACH DELETE n` |
|
|
250
|
+
| `RETURN` | `RETURN n.name AS name, count(*) AS total` |
|
|
251
|
+
| `WITH` | `WITH n, count(*) AS cnt WHERE cnt > 1` |
|
|
252
|
+
| `UNWIND` | `UNWIND $list AS item CREATE (n {value: item})` |
|
|
253
|
+
| `UNION / UNION ALL` | `MATCH (n:A) RETURN n UNION MATCH (m:B) RETURN m` |
|
|
254
|
+
| `ORDER BY` | `ORDER BY n.name DESC` |
|
|
255
|
+
| `SKIP / LIMIT` | `SKIP 10 LIMIT 5` |
|
|
256
|
+
| `DISTINCT` | `RETURN DISTINCT n.category` |
|
|
257
|
+
| `CASE/WHEN` | `RETURN CASE WHEN n.age > 18 THEN 'adult' ELSE 'minor' END` |
|
|
258
|
+
| `CALL` | `CALL db.labels() YIELD label RETURN label` |
|
|
259
|
+
|
|
260
|
+
### Operators
|
|
261
|
+
|
|
262
|
+
| Category | Operators |
|
|
263
|
+
|----------|-----------|
|
|
264
|
+
| Comparison | `=`, `<>`, `<`, `>`, `<=`, `>=` |
|
|
265
|
+
| Logical | `AND`, `OR`, `NOT` |
|
|
266
|
+
| String | `CONTAINS`, `STARTS WITH`, `ENDS WITH` |
|
|
267
|
+
| List | `IN` |
|
|
268
|
+
| Null | `IS NULL`, `IS NOT NULL` |
|
|
269
|
+
| Pattern | `EXISTS` |
|
|
270
|
+
| Arithmetic | `+`, `-`, `*`, `/`, `%` |
|
|
271
|
+
|
|
272
|
+
### Functions
|
|
273
|
+
|
|
274
|
+
**Aggregation:** `COUNT`, `SUM`, `AVG`, `MIN`, `MAX`, `COLLECT`
|
|
275
|
+
|
|
276
|
+
**Scalar:** `ID`, `coalesce`
|
|
277
|
+
|
|
278
|
+
**String:** `toUpper`, `toLower`, `trim`, `substring`, `replace`, `toString`, `split`
|
|
279
|
+
|
|
280
|
+
**List:** `size`, `head`, `last`, `tail`, `keys`, `range`
|
|
281
|
+
|
|
282
|
+
**Node/Relationship:** `labels`, `type`, `properties`
|
|
283
|
+
|
|
284
|
+
**Math:** `abs`, `ceil`, `floor`, `round`, `rand`, `sqrt`
|
|
285
|
+
|
|
286
|
+
**Date/Time:** `date`, `datetime`, `timestamp`
|
|
287
|
+
|
|
288
|
+
### Variable-Length Paths
|
|
289
|
+
|
|
290
|
+
```cypher
|
|
291
|
+
-- Find friends of friends (1 to 3 hops)
|
|
292
|
+
MATCH (a:User {name: 'Alice'})-[:KNOWS*1..3]->(b:User)
|
|
293
|
+
RETURN DISTINCT b.name
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Procedures
|
|
297
|
+
|
|
298
|
+
```cypher
|
|
299
|
+
-- List all labels
|
|
300
|
+
CALL db.labels() YIELD label RETURN label
|
|
301
|
+
|
|
302
|
+
-- List all relationship types
|
|
303
|
+
CALL db.relationshipTypes() YIELD type RETURN type
|
|
304
|
+
|
|
305
|
+
-- List all property keys
|
|
306
|
+
CALL db.propertyKeys() YIELD key RETURN key
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Running the Server (Production)
|
|
310
|
+
|
|
311
|
+
For production deployments, run a dedicated server:
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
# Start the server
|
|
315
|
+
npx leangraph serve --port 3000 --data ./data
|
|
316
|
+
|
|
317
|
+
# Or with custom host binding
|
|
318
|
+
npx leangraph serve --port 3000 --host 0.0.0.0 --data ./data
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Creating Projects
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
# Create a new project (generates API key)
|
|
325
|
+
npx leangraph create myapp --data ./data
|
|
326
|
+
|
|
327
|
+
# Output:
|
|
328
|
+
# [created] production/myapp.db
|
|
329
|
+
# API Key: lg_abc123...
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### CLI Reference
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
# Server
|
|
336
|
+
leangraph serve [options]
|
|
337
|
+
-p, --port <port> Port to listen on (default: 3000)
|
|
338
|
+
-d, --data <path> Data directory (default: /var/data/leangraph)
|
|
339
|
+
-H, --host <host> Host to bind to (default: localhost)
|
|
340
|
+
-b, --backup <path> Backup directory (enables backup endpoints)
|
|
341
|
+
|
|
342
|
+
# Project management
|
|
343
|
+
leangraph create <project> Create new project with API keys
|
|
344
|
+
leangraph delete <project> Delete project (use --force)
|
|
345
|
+
leangraph list List all projects
|
|
346
|
+
|
|
347
|
+
# Environment management
|
|
348
|
+
leangraph clone <project> --from <env> --to <env> Copy between environments
|
|
349
|
+
leangraph wipe <project> --env <env> Clear environment database
|
|
350
|
+
|
|
351
|
+
# Direct queries
|
|
352
|
+
leangraph query <env> <project> "CYPHER"
|
|
353
|
+
|
|
354
|
+
# Backup
|
|
355
|
+
leangraph backup [options]
|
|
356
|
+
-o, --output <path> Backup directory
|
|
357
|
+
-p, --project <name> Backup specific project
|
|
358
|
+
--status Show backup status
|
|
359
|
+
|
|
360
|
+
# API keys
|
|
361
|
+
leangraph apikey add <project>
|
|
362
|
+
leangraph apikey list
|
|
363
|
+
leangraph apikey remove <prefix>
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Why LeanGraph?
|
|
367
|
+
|
|
368
|
+
| Feature | LeanGraph | Neo4j |
|
|
369
|
+
|---------|-----------------|-------|
|
|
370
|
+
| **Deployment** | Single package, zero config | Complex setup, JVM required |
|
|
371
|
+
| **Development** | Local SQLite, no server needed | Server required |
|
|
372
|
+
| **Backup** | Just copy the SQLite file | Enterprise license required |
|
|
373
|
+
| **Resource usage** | ~50MB RAM | 1GB+ RAM minimum |
|
|
374
|
+
| **Cypher support** | Full (Neo4j 3.5 parity) | Full |
|
|
375
|
+
| **Cost** | Free, MIT license | Free tier limited |
|
|
376
|
+
|
|
377
|
+
LeanGraph is ideal for:
|
|
378
|
+
- Applications needing graph queries without ops burden
|
|
379
|
+
- Projects that outgrow JSON but don't need a full graph database
|
|
380
|
+
- Self-hosted deployments where simplicity matters
|
|
381
|
+
- Development and testing with instant local databases
|
|
382
|
+
|
|
383
|
+
## Advanced Usage
|
|
384
|
+
|
|
385
|
+
### Direct Database Access
|
|
386
|
+
|
|
387
|
+
For advanced use cases, you can access the underlying components:
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
import { GraphDatabase, Executor, parse, translate } from 'leangraph';
|
|
391
|
+
|
|
392
|
+
// Direct database access
|
|
393
|
+
const db = new GraphDatabase('./my-database.db');
|
|
394
|
+
db.initialize();
|
|
395
|
+
|
|
396
|
+
const executor = new Executor(db);
|
|
397
|
+
const result = executor.execute('MATCH (n) RETURN n LIMIT 10');
|
|
398
|
+
|
|
399
|
+
db.close();
|
|
400
|
+
|
|
401
|
+
// Parse Cypher to AST
|
|
402
|
+
const parseResult = parse('MATCH (n:User) RETURN n');
|
|
403
|
+
if (parseResult.success) {
|
|
404
|
+
console.log(parseResult.query);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Translate AST to SQL
|
|
408
|
+
const translation = translate(parseResult.query, {});
|
|
409
|
+
console.log(translation.statements);
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Running a Custom Server
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
import { createServer } from 'leangraph';
|
|
416
|
+
import { serve } from '@hono/node-server';
|
|
417
|
+
|
|
418
|
+
const { app, dbManager } = createServer({
|
|
419
|
+
dataPath: './data',
|
|
420
|
+
apiKeys: {
|
|
421
|
+
'my-api-key': { project: 'myapp', env: 'production' }
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
serve({ fetch: app.fetch, port: 3000 });
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## Known Limitations
|
|
429
|
+
|
|
430
|
+
### Large Integer Precision
|
|
431
|
+
|
|
432
|
+
JavaScript cannot precisely represent integers larger than `Number.MAX_SAFE_INTEGER` (9,007,199,254,740,991). Integers beyond this range will lose precision, which can cause unexpected behavior when comparing values.
|
|
433
|
+
|
|
434
|
+
**Example of the problem:**
|
|
435
|
+
```javascript
|
|
436
|
+
// These two different numbers become equal in JavaScript!
|
|
437
|
+
const a = 4611686018427387905;
|
|
438
|
+
const b = 4611686018427387900;
|
|
439
|
+
console.log(a === b); // true (both round to 4611686018427388000)
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
**Workaround:** Use strings for large integer IDs:
|
|
443
|
+
```cypher
|
|
444
|
+
// Instead of:
|
|
445
|
+
CREATE (u:User {id: 4611686018427387905})
|
|
446
|
+
|
|
447
|
+
// Use strings:
|
|
448
|
+
CREATE (u:User {id: '4611686018427387905'})
|
|
449
|
+
MATCH (u:User {id: '4611686018427387905'}) RETURN u
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
This limitation affects all JavaScript-based systems, including Neo4j's JavaScript driver. For IDs that may exceed the safe integer range, string representation is the recommended approach.
|
|
453
|
+
|
|
454
|
+
## License
|
|
455
|
+
|
|
456
|
+
[MIT](https://github.com/co-l/leangraph/blob/main/LICENSE) - Conrad Lelubre
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Context, Next } from "hono";
|
|
2
|
+
export interface ApiKeyConfig {
|
|
3
|
+
project?: string;
|
|
4
|
+
env?: string;
|
|
5
|
+
admin?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface ValidationResult {
|
|
8
|
+
valid: boolean;
|
|
9
|
+
project?: string;
|
|
10
|
+
env?: string;
|
|
11
|
+
admin?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface KeyInfo {
|
|
14
|
+
prefix: string;
|
|
15
|
+
project?: string;
|
|
16
|
+
env?: string;
|
|
17
|
+
admin?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare class ApiKeyStore {
|
|
20
|
+
private keys;
|
|
21
|
+
/**
|
|
22
|
+
* Add an API key with its configuration.
|
|
23
|
+
*/
|
|
24
|
+
addKey(key: string, config: ApiKeyConfig): void;
|
|
25
|
+
/**
|
|
26
|
+
* Remove an API key.
|
|
27
|
+
*/
|
|
28
|
+
removeKey(key: string): void;
|
|
29
|
+
/**
|
|
30
|
+
* Validate an API key and return its permissions.
|
|
31
|
+
*/
|
|
32
|
+
validate(key: string): ValidationResult;
|
|
33
|
+
/**
|
|
34
|
+
* List all keys (with prefixes only for security).
|
|
35
|
+
*/
|
|
36
|
+
listKeys(): KeyInfo[];
|
|
37
|
+
/**
|
|
38
|
+
* Check if any keys are configured.
|
|
39
|
+
*/
|
|
40
|
+
hasKeys(): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Load keys from an object (for initialization from config).
|
|
43
|
+
*/
|
|
44
|
+
loadKeys(keys: Record<string, ApiKeyConfig>): void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Hono middleware for API key authentication.
|
|
48
|
+
*
|
|
49
|
+
* - Skips authentication for /health endpoint
|
|
50
|
+
* - Requires Bearer token in Authorization header
|
|
51
|
+
* - Checks project/env restrictions for /query endpoints
|
|
52
|
+
* - Requires admin flag for /admin endpoints
|
|
53
|
+
*/
|
|
54
|
+
export declare function authMiddleware(store: ApiKeyStore): (c: Context, next: Next) => Promise<void | (Response & import("hono").TypedResponse<{
|
|
55
|
+
success: false;
|
|
56
|
+
error: {
|
|
57
|
+
message: string;
|
|
58
|
+
};
|
|
59
|
+
}, 401, "json">) | (Response & import("hono").TypedResponse<{
|
|
60
|
+
success: false;
|
|
61
|
+
error: {
|
|
62
|
+
message: string;
|
|
63
|
+
};
|
|
64
|
+
}, 403, "json">)>;
|
|
65
|
+
export declare function generateApiKey(): string;
|
|
66
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAMrC,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAMD,qBAAa,WAAW;IACtB,OAAO,CAAC,IAAI,CAAwC;IAEpD;;OAEG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI;IAI/C;;OAEG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAI5B;;OAEG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB;IAevC;;OAEG;IACH,QAAQ,IAAI,OAAO,EAAE;IAerB;;OAEG;IACH,OAAO,IAAI,OAAO;IAIlB;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,GAAG,IAAI;CAKnD;AAMD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,IACjC,GAAG,OAAO,EAAE,MAAM,IAAI;;;;;;;;;;kBA0FrC;AAMD,wBAAgB,cAAc,IAAI,MAAM,CASvC"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// API Key Authentication for LeanGraph
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// ApiKeyStore
|
|
4
|
+
// ============================================================================
|
|
5
|
+
export class ApiKeyStore {
|
|
6
|
+
keys = new Map();
|
|
7
|
+
/**
|
|
8
|
+
* Add an API key with its configuration.
|
|
9
|
+
*/
|
|
10
|
+
addKey(key, config) {
|
|
11
|
+
this.keys.set(key, config);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Remove an API key.
|
|
15
|
+
*/
|
|
16
|
+
removeKey(key) {
|
|
17
|
+
this.keys.delete(key);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Validate an API key and return its permissions.
|
|
21
|
+
*/
|
|
22
|
+
validate(key) {
|
|
23
|
+
const config = this.keys.get(key);
|
|
24
|
+
if (!config) {
|
|
25
|
+
return { valid: false };
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
valid: true,
|
|
29
|
+
project: config.project,
|
|
30
|
+
env: config.env,
|
|
31
|
+
admin: config.admin,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* List all keys (with prefixes only for security).
|
|
36
|
+
*/
|
|
37
|
+
listKeys() {
|
|
38
|
+
const result = [];
|
|
39
|
+
for (const [key, config] of this.keys) {
|
|
40
|
+
result.push({
|
|
41
|
+
prefix: key.slice(0, 4) + "...",
|
|
42
|
+
project: config.project,
|
|
43
|
+
env: config.env,
|
|
44
|
+
admin: config.admin,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Check if any keys are configured.
|
|
51
|
+
*/
|
|
52
|
+
hasKeys() {
|
|
53
|
+
return this.keys.size > 0;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Load keys from an object (for initialization from config).
|
|
57
|
+
*/
|
|
58
|
+
loadKeys(keys) {
|
|
59
|
+
for (const [key, config] of Object.entries(keys)) {
|
|
60
|
+
this.addKey(key, config);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Auth Middleware
|
|
66
|
+
// ============================================================================
|
|
67
|
+
/**
|
|
68
|
+
* Hono middleware for API key authentication.
|
|
69
|
+
*
|
|
70
|
+
* - Skips authentication for /health endpoint
|
|
71
|
+
* - Requires Bearer token in Authorization header
|
|
72
|
+
* - Checks project/env restrictions for /query endpoints
|
|
73
|
+
* - Requires admin flag for /admin endpoints
|
|
74
|
+
*/
|
|
75
|
+
export function authMiddleware(store) {
|
|
76
|
+
return async (c, next) => {
|
|
77
|
+
const path = c.req.path;
|
|
78
|
+
// Skip auth for health endpoint
|
|
79
|
+
if (path === "/health") {
|
|
80
|
+
return next();
|
|
81
|
+
}
|
|
82
|
+
// Get authorization header
|
|
83
|
+
const authHeader = c.req.header("Authorization");
|
|
84
|
+
if (!authHeader) {
|
|
85
|
+
return c.json({
|
|
86
|
+
success: false,
|
|
87
|
+
error: { message: "Missing Authorization header" },
|
|
88
|
+
}, 401);
|
|
89
|
+
}
|
|
90
|
+
// Check Bearer format
|
|
91
|
+
if (!authHeader.startsWith("Bearer ")) {
|
|
92
|
+
return c.json({
|
|
93
|
+
success: false,
|
|
94
|
+
error: { message: "Authorization header must use Bearer scheme" },
|
|
95
|
+
}, 401);
|
|
96
|
+
}
|
|
97
|
+
const apiKey = authHeader.slice(7); // Remove "Bearer "
|
|
98
|
+
const validation = store.validate(apiKey);
|
|
99
|
+
if (!validation.valid) {
|
|
100
|
+
return c.json({
|
|
101
|
+
success: false,
|
|
102
|
+
error: { message: "Invalid API key" },
|
|
103
|
+
}, 401);
|
|
104
|
+
}
|
|
105
|
+
// Check admin access for /admin endpoints
|
|
106
|
+
if (path.startsWith("/admin") && !validation.admin) {
|
|
107
|
+
return c.json({
|
|
108
|
+
success: false,
|
|
109
|
+
error: { message: "Admin access required for this endpoint" },
|
|
110
|
+
}, 403);
|
|
111
|
+
}
|
|
112
|
+
// Check project/env restrictions for query endpoints
|
|
113
|
+
if (path.startsWith("/query/")) {
|
|
114
|
+
const parts = path.split("/");
|
|
115
|
+
const env = parts[2];
|
|
116
|
+
const project = parts[3];
|
|
117
|
+
// Check project restriction
|
|
118
|
+
if (validation.project && validation.project !== project) {
|
|
119
|
+
return c.json({
|
|
120
|
+
success: false,
|
|
121
|
+
error: { message: `Access denied for project: ${project}` },
|
|
122
|
+
}, 403);
|
|
123
|
+
}
|
|
124
|
+
// Check environment restriction
|
|
125
|
+
if (validation.env && validation.env !== env) {
|
|
126
|
+
return c.json({
|
|
127
|
+
success: false,
|
|
128
|
+
error: { message: `Access denied for environment: ${env}` },
|
|
129
|
+
}, 403);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Store validation result in context for later use
|
|
133
|
+
c.set("auth", validation);
|
|
134
|
+
return next();
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
// ============================================================================
|
|
138
|
+
// Helper: Generate a random API key
|
|
139
|
+
// ============================================================================
|
|
140
|
+
export function generateApiKey() {
|
|
141
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
142
|
+
let key = "";
|
|
143
|
+
for (let i = 0; i < 32; i++) {
|
|
144
|
+
key += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
145
|
+
}
|
|
146
|
+
return key;
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|