nedb-engine 2.8.6 → 3.1.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 -65
- package/README.md +60 -2
- package/index.d.ts +59 -0
- package/nedb.darwin-arm64.node +0 -0
- package/nedb.darwin-x64.node +0 -0
- package/nedb.linux-x64-gnu.node +0 -0
- package/nedb.win32-x64-msvc.node +0 -0
- package/nedbd-v2-darwin-arm64 +0 -0
- package/nedbd-v2-darwin-x64 +0 -0
- package/nedbd-v2-linux-x64 +0 -0
- package/nedbd-v2-win-x64.exe +0 -0
- package/package.json +10 -2
- package/wrap/index.js +24 -0
- package/wrap/mongo.js +91 -0
- package/wrap/redis.js +134 -0
- package/wrap/sql.js +151 -0
- package/wrap/surface.js +330 -0
package/LICENSE
CHANGED
|
@@ -1,65 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
2. Using the Licensed Work to store, process, serve, index, replicate, query, or manage production data.
|
|
23
|
-
3. Offering the Licensed Work, or any derivative work, as a hosted, managed, embedded, bundled, white-labeled, commercial, paid, revenue-generating, or customer-facing product or service.
|
|
24
|
-
4. Offering database-as-a-service, storage-as-a-service, cache-as-a-service, search-as-a-service, analytics-as-a-service, AI-memory-as-a-service, agent-memory-as-a-service, blockchain-indexing-as-a-service, or any substantially similar service using the Licensed Work.
|
|
25
|
-
5. Using the Licensed Work to compete with NEDB, Interchained LLC, VibeCode 101, AiAssist Secure, or any affiliated product, service, infrastructure platform, database engine, AI runtime, agent platform, or hosted developer infrastructure.
|
|
26
|
-
6. Embedding the Licensed Work into commercial software, SaaS products, internal business systems, enterprise systems, hosted infrastructure, blockchain infrastructure, AI-agent infrastructure, or customer deliverables.
|
|
27
|
-
7. Removing, hiding, modifying, or misrepresenting this License, copyright notices, attribution notices, authorship notices, repository references, or licensing notices.
|
|
28
|
-
8. Circumventing license checks, access controls, paid licensing requirements, commercial-use restrictions, attribution requirements, or technical protections included with the Licensed Work.
|
|
29
|
-
9. Using the Licensed Work in a way that implies endorsement, partnership, sponsorship, certification, or approval by the Licensor without written permission.
|
|
30
|
-
10. Reselling, relicensing, sublicensing, renting, leasing, or otherwise commercially exploiting the Licensed Work except as expressly authorized in writing.
|
|
31
|
-
|
|
32
|
-
Additional production, commercial, hosted, enterprise, embedded, OEM, resale, white-label, managed-service, competitive, or otherwise restricted use grants are available only by separate written permission from the Licensor.
|
|
33
|
-
|
|
34
|
-
Written permission must be obtained from at least one of the following authorized licensing contacts:
|
|
35
|
-
|
|
36
|
-
[founders@vibecode-101.com](mailto:founders@vibecode-101.com)
|
|
37
|
-
[dev@interchained.org](mailto:dev@interchained.org)
|
|
38
|
-
|
|
39
|
-
Permission is valid only if it is expressly granted in writing by an authorized representative of the Licensor and specifically identifies the permitted use, scope, duration, parties, and any applicable commercial terms.
|
|
40
|
-
|
|
41
|
-
A general email conversation, informal message, pull request, issue comment, social media message, verbal discussion, repository access, package download, or contribution acceptance does not create a production, commercial, hosted, enterprise, resale, or competitive use license.
|
|
42
|
-
|
|
43
|
-
Terms:
|
|
44
|
-
|
|
45
|
-
The Licensor hereby grants you the right to copy, modify, create derivative works, redistribute, and make non-production use of the Licensed Work. The Licensor may make an Additional Use Grant, above, permitting limited production use.
|
|
46
|
-
|
|
47
|
-
Effective on the Change Date, or the fourth anniversary of the first publicly available distribution of a specific version of the Licensed Work under this License, whichever comes first, the Licensor hereby grants you rights under the terms of the Change License, and the rights granted in the paragraph above terminate.
|
|
48
|
-
|
|
49
|
-
If your use of the Licensed Work does not comply with the requirements currently in effect as described in this License, you must purchase or obtain a separate commercial license from the Licensor, its affiliated entities, or authorized resellers, or you must refrain from using the Licensed Work.
|
|
50
|
-
|
|
51
|
-
All copies of the original and modified Licensed Work, and derivative works of the Licensed Work, are subject to this License. This License applies separately for each version of the Licensed Work, and the Change Date may vary for each version of the Licensed Work released by Licensor.
|
|
52
|
-
|
|
53
|
-
You must conspicuously display this License on each original or modified copy of the Licensed Work. If you receive the Licensed Work in original or modified form from a third party, the terms and conditions set forth in this License apply to your use of that work.
|
|
54
|
-
|
|
55
|
-
Any use of the Licensed Work in violation of this License will automatically terminate your rights under this License for the current and all other versions of the Licensed Work.
|
|
56
|
-
|
|
57
|
-
This License does not grant you any right in any trademark, service mark, trade name, logo, domain name, brand identity, product name, project name, or other identifier of Licensor or its affiliates, except as expressly required to preserve legally required notices.
|
|
58
|
-
|
|
59
|
-
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, TITLE, SECURITY, ACCURACY, AVAILABILITY, DATA INTEGRITY, PERFORMANCE, AND FITNESS FOR PRODUCTION USE.
|
|
60
|
-
|
|
61
|
-
TO THE EXTENT PERMITTED BY APPLICABLE LAW, LICENSOR SHALL NOT BE LIABLE FOR ANY CLAIM, DAMAGES, LOSSES, COSTS, EXPENSES, BUSINESS INTERRUPTION, LOST PROFITS, LOST REVENUE, LOST DATA, SECURITY INCIDENTS, SERVICE OUTAGES, OR OTHER LIABILITY ARISING FROM OR RELATED TO THE LICENSED WORK OR YOUR USE OF THE LICENSED WORK.
|
|
62
|
-
|
|
63
|
-
Notice:
|
|
64
|
-
|
|
65
|
-
The Business Source License is not an Open Source license. However, each version of the Licensed Work will become available under the Change License on the applicable Change Date, or on the fourth anniversary of the first publicly available distribution of that version under this License, whichever comes first.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Interchained LLC and VibeCode 101
|
|
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
CHANGED
|
@@ -13,9 +13,15 @@ One Rust core → ships to **PyPI** and **npm** from a single source.
|
|
|
13
13
|
[](https://github.com/Eth-Interchained/nedb/actions)
|
|
14
14
|
[](https://pypi.org/project/nedb-engine-client/)
|
|
15
15
|
[](https://www.npmjs.com/package/nedb-engine-client)
|
|
16
|
+
[](https://github.com/Eth-Interchained/nedb/blob/master/LICENSE)
|
|
16
17
|
|
|
17
18
|
**[Studio → studio.interchained.org](https://studio.interchained.org)** · **[nedb.aiassist.net](https://nedb.aiassist.net)**
|
|
18
19
|
|
|
20
|
+
> ## 🟢 MIT Licensed — free for any use, including commercial & production
|
|
21
|
+
> NEDB is licensed under the **MIT License** — use it in production, embed it in commercial
|
|
22
|
+
> products, ship it in closed-source software, fork it, sell it. No restrictions, no royalties,
|
|
23
|
+
> no copyleft. See [`LICENSE`](LICENSE). The database is free; the moat is what you build on it.
|
|
24
|
+
|
|
19
25
|
</div>
|
|
20
26
|
|
|
21
27
|
---
|
|
@@ -297,7 +303,58 @@ Typed errors throughout: `NedbAuthError`, `NedbNotFound`, `NedbBadRequest`,
|
|
|
297
303
|
|
|
298
304
|
---
|
|
299
305
|
|
|
300
|
-
##
|
|
306
|
+
## The wrap adapter family — provenance for the databases you already run
|
|
307
|
+
|
|
308
|
+
**One line. Any stack.** Wrap your existing connection and gain tamper-evident, causally-provable, bi-temporal storage *alongside* your app — no migration, no rip-and-replace. NEDB never touches your namespace; shadow data lives only in the embedded DAG engine.
|
|
309
|
+
|
|
310
|
+
| Language | Package | Redis | SQLite | MySQL | MongoDB | PostgreSQL |
|
|
311
|
+
|---|---|:---:|:---:|:---:|:---:|:---:|
|
|
312
|
+
| Python | `pip install nedb-engine` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
313
|
+
| Node.js | `npm install nedb-engine` → `require('nedb-engine/wrap')` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
314
|
+
| Rust | `nedb-wrap` (crates.io) | ✅ (`redis` feature) | 📋 engine-direct | 📋 engine-direct | 📋 engine-direct | 📋 engine-direct |
|
|
315
|
+
|
|
316
|
+
✅ adapter shipped · 📋 embed the DAG core directly (`Surface` trait, same contract)
|
|
317
|
+
|
|
318
|
+
The same contract everywhere — register → backfill → shadow → full NEDB API:
|
|
319
|
+
|
|
320
|
+
```python
|
|
321
|
+
# Python — every write auto-chained
|
|
322
|
+
import redis, json
|
|
323
|
+
from nedb import wrap_redis
|
|
324
|
+
|
|
325
|
+
r = wrap_redis(redis.Redis("localhost", 6379), db_name="rideshare",
|
|
326
|
+
dag_path="./nedb-data") # embedded v2/v3 DAG — no server
|
|
327
|
+
r.nedb.register("driver:*", "driver", value_parser=json.loads)
|
|
328
|
+
r.nedb.backfill()
|
|
329
|
+
r.nedb.shadow_writes = True
|
|
330
|
+
r.set("driver:d1", json.dumps({"name": "Bob", "status": "active"}))
|
|
331
|
+
r.nedb.query('FROM driver WHERE status = "active"')
|
|
332
|
+
r.nedb.verify() # → True — BLAKE2b chain intact
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
```js
|
|
336
|
+
// Node.js — same shape, real Rust DAG core via napi-rs
|
|
337
|
+
const { wrapRedis } = require('nedb-engine/wrap');
|
|
338
|
+
const r = wrapRedis(redisClient, { dbName: 'rideshare', dagPath: './nedb-data' });
|
|
339
|
+
r.nedb.register('driver:*', 'driver');
|
|
340
|
+
r.nedb.shadowWrites = true;
|
|
341
|
+
await r.set('driver:d1', JSON.stringify({ name: 'Bob' }));
|
|
342
|
+
r.nedb.query('FROM driver');
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
```rust
|
|
346
|
+
// Rust — embed the engine directly (nedb-wrap crate)
|
|
347
|
+
use nedb_wrap::Surface;
|
|
348
|
+
let s = Surface::in_memory(); // or Surface::open(path)?
|
|
349
|
+
s.register("driver:*", "driver");
|
|
350
|
+
s.shadow_writes.store(true, std::sync::atomic::Ordering::Relaxed);
|
|
351
|
+
s.shadow("driver:d1", serde_json::json!({"name": "Bob"}), true)?;
|
|
352
|
+
assert!(s.verify());
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Engine selection (all languages): `nedbd` HTTP server if you point at one (v1 AOF, `--dag` v2, `--dag-v3` v3) → **embedded DAG** if the native wheel is installed → v1 in-process fallback. Pass `dag_path=` for a durable store, `dag_tmk=` for AES-256-GCM encryption.
|
|
356
|
+
|
|
357
|
+
## Redis layer-2 — wrap_redis() in depth
|
|
301
358
|
|
|
302
359
|
Already running on Redis? Wrap your connection in one line and gain NEDB features *alongside* your existing Redis app — no migration required.
|
|
303
360
|
|
|
@@ -899,7 +956,8 @@ Requires `GITHUB_TOKEN` (`repo` + `workflow` scope) in the environment. It never
|
|
|
899
956
|
|
|
900
957
|
## License
|
|
901
958
|
|
|
902
|
-
|
|
959
|
+
**MIT License** — free for any use, including commercial and production. See [`LICENSE`](LICENSE).
|
|
960
|
+
© 2026 INTERCHAINED LLC — [interchained.org](https://interchained.org)
|
|
903
961
|
|
|
904
962
|
---
|
|
905
963
|
|
package/index.d.ts
CHANGED
|
@@ -3,3 +3,62 @@
|
|
|
3
3
|
// Runtime behavior (durable-mode auto-flush-on-exit) is added by the wrapper in
|
|
4
4
|
// index.js; the type surface is exactly the generated native binding's.
|
|
5
5
|
export * from './native';
|
|
6
|
+
|
|
7
|
+
// ── wrap adapter family (wrap/*.js) ─────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
/** Options accepted by every wrap_* constructor. */
|
|
10
|
+
export interface WrapOptions {
|
|
11
|
+
/** Logical database name (default "default"). */
|
|
12
|
+
dbName?: string;
|
|
13
|
+
/** HTTP nedbd server (v1 AOF, `--dag` v2, `--dag-v3` v3). Overrides embedded DAG. */
|
|
14
|
+
nedbdUrl?: string;
|
|
15
|
+
/** Bearer token for nedbd (NEDBD_TOKEN on the server). */
|
|
16
|
+
nedbdToken?: string;
|
|
17
|
+
/** Durable DAG store directory (embedded mode). */
|
|
18
|
+
dagPath?: string;
|
|
19
|
+
/** 64-hex TMK → AES-256-GCM at-rest encryption (embedded DAG mode). */
|
|
20
|
+
dagTmk?: string;
|
|
21
|
+
/** Explicit NedbCore class (testing / custom builds). */
|
|
22
|
+
native?: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The `.nedb` attribute — full NEDB layer-2 API. */
|
|
26
|
+
export interface NedbSurface {
|
|
27
|
+
register(pattern: string, collection: string, opts?: {
|
|
28
|
+
idExtractor?: (key: string) => string;
|
|
29
|
+
valueParser?: (raw: unknown) => Record<string, unknown>;
|
|
30
|
+
valueType?: 'string' | 'hash' | 'json';
|
|
31
|
+
}): NedbSurface;
|
|
32
|
+
backfill(opts?: { pattern?: string; collection?: string; batchSize?: number }): number;
|
|
33
|
+
shadowWrites: boolean;
|
|
34
|
+
readonly engineKind: 'dag-embedded' | 'nedbd-http' | 'aof-embedded';
|
|
35
|
+
|
|
36
|
+
put(coll: string, id: string, doc: Record<string, unknown>): Record<string, unknown>;
|
|
37
|
+
get(coll: string, id: string, asOf?: number): Record<string, unknown> | null;
|
|
38
|
+
query(nql: string): Array<Record<string, unknown>>;
|
|
39
|
+
createIndex(coll: string, field: string, kind?: string): void;
|
|
40
|
+
delete(coll: string, id: string): void;
|
|
41
|
+
link(frm: string, rel: string, to: string): void;
|
|
42
|
+
unlink(frm: string, rel: string, to: string): void;
|
|
43
|
+
neighbors(frm: string, rel: string, asOf?: number): string[];
|
|
44
|
+
inbound(to: string, rel: string, asOf?: number): string[];
|
|
45
|
+
verify(): boolean;
|
|
46
|
+
readonly head: string;
|
|
47
|
+
readonly seq: number;
|
|
48
|
+
checkpoint(): string;
|
|
49
|
+
/** DAG-native: latest node (null on non-DAG backends). */
|
|
50
|
+
tip(): Record<string, unknown> | null;
|
|
51
|
+
/** DAG-native: changefeed page, after_seq exclusive. */
|
|
52
|
+
since(afterSeq: number | bigint, limit?: number): {
|
|
53
|
+
nodes: Array<Record<string, unknown>>; from_seq: number; to_seq: number;
|
|
54
|
+
head_seq: number; has_more: boolean;
|
|
55
|
+
};
|
|
56
|
+
/** DAG-native: replication readiness. */
|
|
57
|
+
scanStatus(): { scan_complete: boolean; tip_seq: number; indexed_count: number; [k: string]: unknown };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export declare function wrapRedis<T extends object = object>(client: T, opts?: WrapOptions): T & { nedb: NedbSurface };
|
|
61
|
+
export declare function wrapSqlite<T extends object = object>(conn: T, opts?: WrapOptions): T & { nedb: NedbSurface };
|
|
62
|
+
export declare function wrapMysql<T extends object = object>(conn: T, opts?: WrapOptions): T & { nedb: NedbSurface };
|
|
63
|
+
export declare function wrapPg<T extends object = object>(conn: T, opts?: WrapOptions): T & { nedb: NedbSurface };
|
|
64
|
+
export declare function wrapMongo<T extends object = object>(client: T, opts?: WrapOptions): T & { nedb: NedbSurface };
|
package/nedb.darwin-arm64.node
CHANGED
|
Binary file
|
package/nedb.darwin-x64.node
CHANGED
|
Binary file
|
package/nedb.linux-x64-gnu.node
CHANGED
|
Binary file
|
package/nedb.win32-x64-msvc.node
CHANGED
|
Binary file
|
package/nedbd-v2-darwin-arm64
CHANGED
|
Binary file
|
package/nedbd-v2-darwin-x64
CHANGED
|
Binary file
|
package/nedbd-v2-linux-x64
CHANGED
|
Binary file
|
package/nedbd-v2-win-x64.exe
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nedb-engine",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "NEDB — hash-chained, time-traveling, bi-temporal embedded database with Rust native core. SQL, Redis, MongoDB adapters. Causal Write Provenance. RESP2 wire protocol.",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./index.js",
|
|
9
|
+
"require": "./index.js"
|
|
10
|
+
},
|
|
11
|
+
"./wrap": "./wrap/index.js"
|
|
12
|
+
},
|
|
6
13
|
"types": "index.d.ts",
|
|
7
14
|
"bin": {
|
|
8
15
|
"nedbd-v2": "./nedbd-v2.js",
|
|
@@ -14,6 +21,7 @@
|
|
|
14
21
|
"index.d.ts",
|
|
15
22
|
"native.js",
|
|
16
23
|
"native.d.ts",
|
|
24
|
+
"wrap/",
|
|
17
25
|
"nedbd-v2.js",
|
|
18
26
|
"nedb-inspector.mjs",
|
|
19
27
|
"*.node",
|
|
@@ -23,7 +31,7 @@
|
|
|
23
31
|
"README.md",
|
|
24
32
|
"LICENSE"
|
|
25
33
|
],
|
|
26
|
-
"license": "
|
|
34
|
+
"license": "MIT",
|
|
27
35
|
"homepage": "https://github.com/aiassistsecure/nedb#readme",
|
|
28
36
|
"repository": {
|
|
29
37
|
"type": "git",
|
package/wrap/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// nedb/wrap/index.js — the JS wrap adapter family.
|
|
3
|
+
//
|
|
4
|
+
// const { wrapRedis, wrapSqlite, wrapMysql, wrapPg, wrapMongo } =
|
|
5
|
+
// require('nedb-engine/wrap');
|
|
6
|
+
//
|
|
7
|
+
// Every wrapper: register → backfill → shadowWrites=true → full NEDB API on
|
|
8
|
+
// `.nedb`, with the embedded v2/v3 DAG (Rust napi core) as the default engine.
|
|
9
|
+
//
|
|
10
|
+
// © INTERCHAINED LLC × Claude Sonnet 4.6
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const { WrapSurface, openEngine, CollectionMapping, NedbdProxy, NativeEngine } = require('./surface');
|
|
14
|
+
const { wrapRedis, WRITE_CMDS } = require('./redis');
|
|
15
|
+
const { wrapSqlite, wrapMysql, wrapPg, SqlSurface } = require('./sql');
|
|
16
|
+
const { wrapMongo, MongoSurface } = require('./mongo');
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
// adapters
|
|
20
|
+
wrapRedis, wrapSqlite, wrapMysql, wrapPg, wrapMongo,
|
|
21
|
+
// surface primitives (for custom adapters)
|
|
22
|
+
WrapSurface, openEngine, CollectionMapping, NedbdProxy, NativeEngine,
|
|
23
|
+
SqlSurface, MongoSurface, WRITE_CMDS,
|
|
24
|
+
};
|
package/wrap/mongo.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// nedb/wrap/mongo.js — wrapMongo: causal provenance for MongoDB (JS).
|
|
3
|
+
//
|
|
4
|
+
// const { wrapMongo } = require('nedb-engine/wrap');
|
|
5
|
+
// const client = wrapMongo(mongoClient, { dbName: 'app' });
|
|
6
|
+
//
|
|
7
|
+
// client.nedb.register('app.drivers', 'driver'); // "db.collection" ns
|
|
8
|
+
// client.nedb.backfill();
|
|
9
|
+
// client.nedb.shadowWrites = true;
|
|
10
|
+
//
|
|
11
|
+
// // your code unchanged; after each write, chain it:
|
|
12
|
+
// await client.db().collection('drivers').insertOne({ name: 'Bob' });
|
|
13
|
+
// client.nedb.shadowRow('app.drivers', { _id: r.insertedId, name: 'Bob' });
|
|
14
|
+
//
|
|
15
|
+
// © INTERCHAINED LLC × Claude Sonnet 4.6
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const { WrapSurface, openEngine } = require('./surface');
|
|
19
|
+
|
|
20
|
+
class MongoSurface extends WrapSurface {
|
|
21
|
+
constructor(client, dbName, engine) {
|
|
22
|
+
super(dbName, engine);
|
|
23
|
+
this.client = client;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** hostScan: iterate docs of a "db.collection" namespace (sync only —
|
|
27
|
+
* pymongo-style cursor.toArray is async, so JS backfill for the native
|
|
28
|
+
* driver is async; provide scanAsync). */
|
|
29
|
+
hostScan(mapping) { return []; }
|
|
30
|
+
|
|
31
|
+
async hostScanAsync(mapping, batchSize) {
|
|
32
|
+
const [dbName, collName] = mapping.pattern.split('.');
|
|
33
|
+
const out = [];
|
|
34
|
+
try {
|
|
35
|
+
const coll = this.client.db(dbName).collection(collName);
|
|
36
|
+
const cursor = coll.find({});
|
|
37
|
+
while (await cursor.hasNext()) {
|
|
38
|
+
const doc = await cursor.next();
|
|
39
|
+
const { _id, ...rest } = doc;
|
|
40
|
+
out.push([String(_id), rest]);
|
|
41
|
+
if (out.length >= (batchSize || 200)) break;
|
|
42
|
+
}
|
|
43
|
+
} catch (_) { /* skip */ }
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
shadowDoc() { return null; }
|
|
48
|
+
|
|
49
|
+
/** Explicit row shadow: routes into the registered collection. */
|
|
50
|
+
shadowRow(ns, doc, op = 'UPSERT') {
|
|
51
|
+
if (!this.shadowWrites) return;
|
|
52
|
+
try {
|
|
53
|
+
const m = this.mappings.find((x) => x.pattern === ns);
|
|
54
|
+
const coll = m ? m.collection : ns.split('.').pop();
|
|
55
|
+
if (doc === null || doc === undefined) {
|
|
56
|
+
this.engine.put('__mongo_shadow__', `${ns}:del:${op}`,
|
|
57
|
+
{ ns, _op: 'DELETE' });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const { _id, ...rest } = doc;
|
|
61
|
+
this.engine.put(coll, String(_id ?? op), { ...rest, _ns: ns, _op: op });
|
|
62
|
+
} catch (_) { /* never break the host */ }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class WrappedMongoClient {
|
|
67
|
+
constructor(client, opts = {}) {
|
|
68
|
+
const dbName = opts.dbName || 'default';
|
|
69
|
+
const engine = openEngine({
|
|
70
|
+
dbName, nedbdUrl: opts.nedbdUrl, nedbdToken: opts.nedbdToken,
|
|
71
|
+
dagPath: opts.dagPath, dagTmk: opts.dagTmk, native: opts.native,
|
|
72
|
+
});
|
|
73
|
+
this._client = client;
|
|
74
|
+
this.nedb = new MongoSurface(client, dbName, engine);
|
|
75
|
+
}
|
|
76
|
+
_raw() { return this._client; }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function wrapMongo(client, opts) {
|
|
80
|
+
const w = new WrappedMongoClient(client, opts);
|
|
81
|
+
return new Proxy(w, {
|
|
82
|
+
get(target, prop, receiver) {
|
|
83
|
+
if (prop in target || prop === 'nedb') return Reflect.get(target, prop, receiver);
|
|
84
|
+
const v = Reflect.get(target._client, prop);
|
|
85
|
+
return typeof v === 'function' ? v.bind(target._client) : v;
|
|
86
|
+
},
|
|
87
|
+
set(target, prop, value) { target._client[prop] = value; return true; },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { wrapMongo, WrappedMongoClient, MongoSurface };
|
package/wrap/redis.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// nedb/wrap/redis.js — wrapRedis: one-line causal provenance for Redis (JS).
|
|
3
|
+
//
|
|
4
|
+
// const { wrapRedis } = require('nedb-engine/wrap');
|
|
5
|
+
// const redis = require('redis');
|
|
6
|
+
//
|
|
7
|
+
// const r = wrapRedis(redis.createClient(), { dbName: 'rideshare' });
|
|
8
|
+
//
|
|
9
|
+
// r.nedb.register('driver:*', 'driver');
|
|
10
|
+
// r.nedb.backfill();
|
|
11
|
+
// r.nedb.shadowWrites = true;
|
|
12
|
+
//
|
|
13
|
+
// await r.set('driver:d1', JSON.stringify({ name: 'Bob' })); // shadowed
|
|
14
|
+
// r.nedb.query('FROM driver'); // NQL on top
|
|
15
|
+
// r.nedb.verify(); // → true
|
|
16
|
+
//
|
|
17
|
+
// Works with any client whose write methods are functions on the connection
|
|
18
|
+
// (node-redis v4, ioredis, redis-mock). Write commands are intercepted by
|
|
19
|
+
// wrapping the method — the surface-1 behavior is unchanged.
|
|
20
|
+
//
|
|
21
|
+
// © INTERCHAINED LLC × Claude Sonnet 4.6
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const { WrapSurface, openEngine } = require('./surface');
|
|
25
|
+
|
|
26
|
+
// Redis write commands we shadow (mirrors the Python _WRITE_CMDS set).
|
|
27
|
+
const WRITE_CMDS = new Set([
|
|
28
|
+
'set', 'setnx', 'setex', 'psetex', 'getset', 'getdel', 'getex',
|
|
29
|
+
'mset', 'msetnx', 'hset', 'hmset', 'hsetnx', 'hincrby', 'hincrbyfloat', 'hdel',
|
|
30
|
+
'lpush', 'rpush', 'lset', 'linsert', 'ltrim', 'lpop', 'rpop',
|
|
31
|
+
'sadd', 'srem', 'smove', 'zadd', 'zincrby', 'zrem',
|
|
32
|
+
'del', 'unlink', 'rename', 'renamenx', 'append', 'incr', 'incrby', 'decr', 'decrby',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
class RedisSurface extends WrapSurface {
|
|
36
|
+
constructor(client, dbName, engine) {
|
|
37
|
+
super(dbName, engine);
|
|
38
|
+
this.client = client;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── host scan: iterate keys matching the pattern, read their values ──────
|
|
42
|
+
hostScan(mapping, batchSize) {
|
|
43
|
+
// node-redis v4 / ioredis both expose scan + get/hgetall; a sync-scan
|
|
44
|
+
// fallback covers in-memory fakes.
|
|
45
|
+
const out = [];
|
|
46
|
+
const scanSync = typeof this.client.scanKeys === 'function'
|
|
47
|
+
? this.client.scanKeys(mapping.pattern)
|
|
48
|
+
: (this.client.keys ? this.client.keys(mapping.pattern) : []);
|
|
49
|
+
for (const key of scanSync || []) {
|
|
50
|
+
try {
|
|
51
|
+
const raw = mapping.valueType === 'hash'
|
|
52
|
+
? this.client.hgetall(key)
|
|
53
|
+
: this.client.get(key);
|
|
54
|
+
if (raw !== null && raw !== undefined) out.push([key, raw]);
|
|
55
|
+
} catch (_) { /* skip unreadable */ }
|
|
56
|
+
if (out.length >= (batchSize || 200)) break;
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── host write → NEDB doc ──────────────────────────────────────────────────
|
|
62
|
+
// args = the host call's arguments AFTER the key.
|
|
63
|
+
// __replace: true → surface puts doc as-is (full replace); default merges.
|
|
64
|
+
shadowDoc(mapping, cmd, args) {
|
|
65
|
+
if (cmd === 'hset') {
|
|
66
|
+
// hset key field value | hset key {obj}
|
|
67
|
+
if (typeof args[0] === 'object' && args[0] !== null) return mapping.parseValue(args[0]);
|
|
68
|
+
return { [args[0]]: args[1] }; // merged over existing by the surface
|
|
69
|
+
}
|
|
70
|
+
if (cmd === 'set' || cmd === 'setex' || cmd === 'psetex' || cmd === 'setnx' || cmd === 'getset') {
|
|
71
|
+
return { ...mapping.parseValue(args[0]), __replace: true };
|
|
72
|
+
}
|
|
73
|
+
if (cmd === 'incr' || cmd === 'incrby' || cmd === 'decr' || cmd === 'decrby') {
|
|
74
|
+
return { _v: String(args[0] === undefined ? '' : args[0]) };
|
|
75
|
+
}
|
|
76
|
+
if (cmd === 'del' || cmd === 'unlink') {
|
|
77
|
+
return { _deleted: true, __replace: true };
|
|
78
|
+
}
|
|
79
|
+
// other write types: store the command as metadata (merged)
|
|
80
|
+
return { [`_redis_${cmd}`]: String(args[0] === undefined ? '' : args[0]) };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
class WrappedRedis {
|
|
85
|
+
constructor(client, opts = {}) {
|
|
86
|
+
const dbName = opts.dbName || 'default';
|
|
87
|
+
const engine = openEngine({
|
|
88
|
+
dbName, nedbdUrl: opts.nedbdUrl, nedbdToken: opts.nedbdToken,
|
|
89
|
+
dagPath: opts.dagPath, dagTmk: opts.dagTmk, native: opts.native,
|
|
90
|
+
});
|
|
91
|
+
this._client = client;
|
|
92
|
+
this.nedb = new RedisSurface(client, dbName, engine);
|
|
93
|
+
this._installInterception();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
_installInterception() {
|
|
97
|
+
const surface = this.nedb;
|
|
98
|
+
const client = this._client;
|
|
99
|
+
for (const cmd of WRITE_CMDS) {
|
|
100
|
+
const orig = client[cmd];
|
|
101
|
+
if (typeof orig !== 'function' || orig.__nedbWrapped) continue;
|
|
102
|
+
const wrapped = function (...args) {
|
|
103
|
+
const result = orig.apply(client, args);
|
|
104
|
+
try {
|
|
105
|
+
const key = args[0];
|
|
106
|
+
if (typeof key === 'string' && surface.shadowWrites) surface.shadow(cmd, key, ...args.slice(1));
|
|
107
|
+
} catch (_) { /* never break the host call */ }
|
|
108
|
+
return result;
|
|
109
|
+
};
|
|
110
|
+
wrapped.__nedbWrapped = true;
|
|
111
|
+
try { client[cmd] = wrapped; } catch (_) { /* frozen client — skip */ }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// passthrough for everything else
|
|
116
|
+
__getattrPrivate(name) { return this._client[name]; }
|
|
117
|
+
get _raw() { return this._client; }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function wrapRedis(client, opts) {
|
|
121
|
+
const w = new WrappedRedis(client, opts);
|
|
122
|
+
// Proxy property access to the underlying client so `await r.get(...)`
|
|
123
|
+
// works naturally, while `.nedb` stays on the wrapper.
|
|
124
|
+
return new Proxy(w, {
|
|
125
|
+
get(target, prop, receiver) {
|
|
126
|
+
if (prop in target || prop === 'nedb' || prop === '_client') return Reflect.get(target, prop, receiver);
|
|
127
|
+
const v = target._client[prop];
|
|
128
|
+
return typeof v === 'function' ? v.bind(target._client) : v;
|
|
129
|
+
},
|
|
130
|
+
set(target, prop, value) { target._client[prop] = value; return true; },
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { wrapRedis, WrappedRedis, RedisSurface, WRITE_CMDS };
|
package/wrap/sql.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// nedb/wrap/sql.js — wrapSqlite / wrapMysql / wrapPg: causal provenance for
|
|
3
|
+
// SQL databases (JS).
|
|
4
|
+
//
|
|
5
|
+
// const { wrapSqlite } = require('nedb-engine/wrap');
|
|
6
|
+
// const db = wrapSqlite(mySqliteConnection, { dbName: 'app' });
|
|
7
|
+
//
|
|
8
|
+
// db.nedb.register('drivers', 'driver');
|
|
9
|
+
// db.nedb.backfill();
|
|
10
|
+
// db.nedb.shadowWrites = true;
|
|
11
|
+
//
|
|
12
|
+
// SQLite: execute() is intercepted — INSERT/UPDATE/DELETE on registered
|
|
13
|
+
// tables are shadowed automatically after they succeed.
|
|
14
|
+
// MySQL/Postgres (DB-API style or promise clients): shadowing is explicit —
|
|
15
|
+
// after your INSERT/UPDATE call db.nedb.shadowRow(table, pk, rowObj);
|
|
16
|
+
// row=null chains a DELETE tombstone.
|
|
17
|
+
//
|
|
18
|
+
// © INTERCHAINED LLC × Claude Sonnet 4.6
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const { WrapSurface, openEngine } = require('./surface');
|
|
22
|
+
|
|
23
|
+
class SqlSurface extends WrapSurface {
|
|
24
|
+
constructor(conn, dbName, engine, kind) {
|
|
25
|
+
super(dbName, engine);
|
|
26
|
+
this.conn = conn;
|
|
27
|
+
this.kind = kind;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Default host scan: SELECT * FROM <table> — works on sync sqlite3 and
|
|
31
|
+
* promise clients that expose .all/.query. Override for exotic drivers. */
|
|
32
|
+
hostScan(mapping, batchSize) {
|
|
33
|
+
const table = mapping.pattern;
|
|
34
|
+
const out = [];
|
|
35
|
+
const push = (cols, rows) => {
|
|
36
|
+
for (const row of rows || []) {
|
|
37
|
+
if (Array.isArray(row)) out.push([String(out.length + 1), Object.fromEntries(cols.map((c, i) => [c, row[i]]))]);
|
|
38
|
+
else out.push([String(row[cols[0]] ?? out.length + 1), row]);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
if (typeof this.conn.all === 'function') { // node-sqlite3 style
|
|
42
|
+
// sync reads aren't available; caller should backfill via callback mode.
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
if (typeof this.conn.prepare === 'function') { // better-sqlite3
|
|
46
|
+
const stmt = this.conn.prepare(`SELECT rowid AS __nedb_rowid, * FROM "${table}"`);
|
|
47
|
+
for (const row of stmt.iterate()) {
|
|
48
|
+
const { __nedb_rowid, ...rest } = row;
|
|
49
|
+
out.push([String(__nedb_rowid), rest]);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
if (typeof this.conn.exec === 'function' && this.conn.prepare === undefined) {
|
|
54
|
+
// node:sqlite (built-in) — similar to better-sqlite3
|
|
55
|
+
try {
|
|
56
|
+
const stmt = this.conn.prepare(`SELECT rowid AS __nedb_rowid, * FROM "${table}"`);
|
|
57
|
+
for (const row of stmt.iterate ? stmt.iterate() : []) {
|
|
58
|
+
const { __nedb_rowid, ...rest } = row;
|
|
59
|
+
out.push([String(__nedb_rowid), rest]);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
} catch (_) { /* fall through */ }
|
|
63
|
+
}
|
|
64
|
+
if (typeof this.conn.query === 'function') { // mysql2 / pg promise
|
|
65
|
+
// handled by host adapters with async backfill — sync scan unsupported
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
shadowDoc() { return null; }
|
|
72
|
+
|
|
73
|
+
/** Explicit row shadow (mysql/pg): routes into the registered collection. */
|
|
74
|
+
shadowRow(table, pk, row, op = 'UPSERT') {
|
|
75
|
+
if (!this.shadowWrites) return;
|
|
76
|
+
try {
|
|
77
|
+
if (row === null || row === undefined) {
|
|
78
|
+
this.engine.put('__sql_shadow__', `${table}:del:${pk}`,
|
|
79
|
+
{ table, pk: String(pk), _op: 'DELETE' });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const m = this.mappings.find((x) => x.pattern === table);
|
|
83
|
+
const coll = m ? m.collection : '__sql_shadow__';
|
|
84
|
+
this.engine.put(coll, String(pk), { ...row, _table: table, _op: op });
|
|
85
|
+
} catch (_) { /* never break the host */ }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
class WrappedSqlConn {
|
|
90
|
+
constructor(conn, opts, kind) {
|
|
91
|
+
const dbName = opts.dbName || 'default';
|
|
92
|
+
const engine = openEngine({
|
|
93
|
+
dbName, nedbdUrl: opts.nedbdUrl, nedbdToken: opts.nedbdToken,
|
|
94
|
+
dagPath: opts.dagPath, dagTmk: opts.dagTmk, native: opts.native,
|
|
95
|
+
});
|
|
96
|
+
this._conn = conn;
|
|
97
|
+
this.nedb = new SqlSurface(conn, dbName, engine, kind);
|
|
98
|
+
if (kind === 'sqlite') this._installSqliteInterception();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** sqlite3/better-sqlite/node:sqlite — wrap exec/run/execute for shadowing. */
|
|
102
|
+
_installSqliteInterception() {
|
|
103
|
+
const surface = this.nedb;
|
|
104
|
+
const conn = this._conn;
|
|
105
|
+
const methodNames = ['execute', 'run', 'exec'].filter((m) => typeof conn[m] === 'function');
|
|
106
|
+
for (const name of methodNames) {
|
|
107
|
+
const orig = conn[name];
|
|
108
|
+
if (orig.__nedbWrapped) continue;
|
|
109
|
+
const wrapped = function (sql, ...rest) {
|
|
110
|
+
const result = orig.call(conn, sql, ...rest);
|
|
111
|
+
try {
|
|
112
|
+
if (surface.shadowWrites && /^\s*(INSERT|UPDATE|DELETE|REPLACE)/i.test(String(sql))) {
|
|
113
|
+
surface.shadow('sql', sql, 'sql', sql, result);
|
|
114
|
+
}
|
|
115
|
+
} catch (_) {}
|
|
116
|
+
return result;
|
|
117
|
+
};
|
|
118
|
+
wrapped.__nedbWrapped = true;
|
|
119
|
+
try { conn[name] = wrapped; } catch (_) {}
|
|
120
|
+
}
|
|
121
|
+
// transactional helpers pass through
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_raw() { return this._conn; }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function _wrapWithProxy(wrapper, conn) {
|
|
128
|
+
return new Proxy(wrapper, {
|
|
129
|
+
get(target, prop, receiver) {
|
|
130
|
+
if (prop in target || prop === 'nedb') return Reflect.get(target, prop, receiver);
|
|
131
|
+
const v = Reflect.get(conn, prop);
|
|
132
|
+
return typeof v === 'function' ? v.bind(conn) : v;
|
|
133
|
+
},
|
|
134
|
+
set(target, prop, value) { conn[prop] = value; return true; },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function wrapSqlite(conn, opts) {
|
|
139
|
+
return _wrapWithProxy(new WrappedSqlConn(conn, opts || {}, 'sqlite'), conn);
|
|
140
|
+
}
|
|
141
|
+
function wrapMysql(conn, opts) {
|
|
142
|
+
return _wrapWithProxy(new WrappedSqlConn(conn, opts || {}, 'mysql'), conn);
|
|
143
|
+
}
|
|
144
|
+
function wrapPg(conn, opts) {
|
|
145
|
+
const w = new WrappedSqlConn(conn, opts || {}, 'pg');
|
|
146
|
+
const p = _wrapWithProxy(w, conn);
|
|
147
|
+
// pg uses .query — shadowRow is explicit on the surface
|
|
148
|
+
return p;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = { wrapSqlite, wrapMysql, wrapPg, WrappedSqlConn, SqlSurface };
|
package/wrap/surface.js
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// nedb/wrap/surface.js — the engine-agnostic NEDB layer-2 surface (JS).
|
|
3
|
+
//
|
|
4
|
+
// One contract, three engines:
|
|
5
|
+
// dag — embedded Rust core (NedbCore from the napi-rs addon)
|
|
6
|
+
// nedbd — HTTP nedbd server (v1 AOF, v2 DAG, v3 --dag-v3)
|
|
7
|
+
// memory — a caller-supplied engine object (tests, custom backends)
|
|
8
|
+
//
|
|
9
|
+
// Host adapters (wrapRedis/wrapSqlite/wrapMysql/wrapMongo/wrapPg) supply:
|
|
10
|
+
// hostScan(mapping) → iterate existing host records
|
|
11
|
+
// shadowDoc(mapping, args) → host write → NEDB doc (or null)
|
|
12
|
+
//
|
|
13
|
+
// © INTERCHAINED LLC × Claude Sonnet 4.6
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
// ── engine resolution ────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/** Acquire the NedbCore class: caller-supplied, native addon, or throw. */
|
|
22
|
+
function resolveNative(provided) {
|
|
23
|
+
if (provided) return provided;
|
|
24
|
+
try {
|
|
25
|
+
return require('../index.js').NedbCore;
|
|
26
|
+
} catch (_) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
'nedb-engine native addon not found — rebuild with `npm run build` ' +
|
|
29
|
+
'or pass an engine explicitly (wrap(x, { engine }) / { nedbdUrl })');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Open an engine handle for a wrap_* surface.
|
|
35
|
+
* openEngine({ nedbdUrl, nedbdToken, dagPath, engine }) → handle
|
|
36
|
+
* The handle exposes: put/get/query/createIndex/delete/link/unlink/
|
|
37
|
+
* neighbors/inbound/verify()/head/seq + engineKind.
|
|
38
|
+
*/
|
|
39
|
+
function openEngine(opts = {}) {
|
|
40
|
+
if (opts.engine) return normalizeEngine(opts.engine, 'supplied');
|
|
41
|
+
|
|
42
|
+
if (opts.nedbdUrl) return new NedbdProxy(opts.nedbdUrl, opts.dbName, opts.nedbdToken);
|
|
43
|
+
|
|
44
|
+
const NedbCore = resolveNative(opts.native);
|
|
45
|
+
const core = opts.dagPath
|
|
46
|
+
? NedbCore.open(opts.dagPath, opts.dagTmk || null)
|
|
47
|
+
: new NedbCore();
|
|
48
|
+
return normalizeEngine(core, 'dag-embedded');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Normalize any engine-ish object into the canonical duck-typed contract. */
|
|
52
|
+
function normalizeEngine(core, kind) {
|
|
53
|
+
// HTTP proxy shape → pass through as-is
|
|
54
|
+
if (core instanceof NedbdProxy) return core;
|
|
55
|
+
return new NativeEngine(core, kind);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Native engine handle (embedded Rust DAG) ────────────────────────────────
|
|
59
|
+
|
|
60
|
+
class NativeEngine {
|
|
61
|
+
constructor(core, kind) {
|
|
62
|
+
this.core = core;
|
|
63
|
+
this.kind = kind || 'dag-embedded';
|
|
64
|
+
}
|
|
65
|
+
put(coll, id, doc) {
|
|
66
|
+
const body = { ...doc };
|
|
67
|
+
for (const k of ['caused_by', 'valid_from', 'valid_to']) {
|
|
68
|
+
if (body[k] === undefined) delete body[k];
|
|
69
|
+
}
|
|
70
|
+
const node = this.core.put(coll, id, JSON.stringify(body));
|
|
71
|
+
return typeof node === 'string' ? JSON.parse(node) : node;
|
|
72
|
+
}
|
|
73
|
+
get(coll, id, asOf) {
|
|
74
|
+
const node = asOf === undefined || asOf === null
|
|
75
|
+
? this.core.get(coll, id)
|
|
76
|
+
: this.core.get(coll, id, asOf);
|
|
77
|
+
return node ? (typeof node === 'string' ? JSON.parse(node) : node) : null;
|
|
78
|
+
}
|
|
79
|
+
query(nql) {
|
|
80
|
+
const rows = this.core.query(nql);
|
|
81
|
+
return rows.map((r) => (typeof r === 'string' ? JSON.parse(r) : r));
|
|
82
|
+
}
|
|
83
|
+
createIndex(coll, field, kind) { this.core.createIndex(coll, field, kind || 'eq'); }
|
|
84
|
+
delete(coll, id) { this.core.delete(coll, id); }
|
|
85
|
+
link(frm, rel, to) { this.core.link(frm, rel, to); }
|
|
86
|
+
unlink(frm, rel, to) { this.core.unlink(frm, rel, to); }
|
|
87
|
+
neighbors(frm, rel, asOf) {
|
|
88
|
+
return asOf === undefined ? this.core.neighbors(frm, rel)
|
|
89
|
+
: this.core.neighbors(frm, rel, asOf);
|
|
90
|
+
}
|
|
91
|
+
inbound(to, rel, asOf) {
|
|
92
|
+
return asOf === undefined ? this.core.inbound(to, rel)
|
|
93
|
+
: this.core.inbound(to, rel, asOf);
|
|
94
|
+
}
|
|
95
|
+
verify() { return this.core.verify() === true; }
|
|
96
|
+
get head() { return this.core.head(); }
|
|
97
|
+
get seq() { return this.core.seq(); }
|
|
98
|
+
checkpoint() { this.core.flush(); return this.head; }
|
|
99
|
+
tip() { const t = this.core.tip ? this.core.tip() : null; return t ? JSON.parse(t) : null; }
|
|
100
|
+
since(afterSeq, limit) {
|
|
101
|
+
if (!this.core.since) throw new Error('changefeed requires the DAG backend');
|
|
102
|
+
// napi binding: after_seq is u64 (BigInt), limit is usize (Number)
|
|
103
|
+
const after = typeof afterSeq === 'bigint' ? afterSeq : BigInt(afterSeq);
|
|
104
|
+
const lim = limit === undefined ? 0 : Number(limit);
|
|
105
|
+
return JSON.parse(this.core.since(after, lim));
|
|
106
|
+
}
|
|
107
|
+
scanStatus() {
|
|
108
|
+
if (!this.core.scanStatus) throw new Error('scanStatus requires the DAG backend');
|
|
109
|
+
return JSON.parse(this.core.scanStatus());
|
|
110
|
+
}
|
|
111
|
+
flush() { if (this.core.flush) this.core.flush(); }
|
|
112
|
+
get engineKind() { return this.kind; }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── HTTP nedbd handle ────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
class NedbdProxy {
|
|
118
|
+
constructor(baseUrl, dbName, token) {
|
|
119
|
+
this.base = baseUrl.replace(/\/$/, '');
|
|
120
|
+
this.name = dbName;
|
|
121
|
+
this.token = token || null;
|
|
122
|
+
this._ensureDb();
|
|
123
|
+
}
|
|
124
|
+
_headers() {
|
|
125
|
+
const h = { 'Content-Type': 'application/json', Accept: 'application/json' };
|
|
126
|
+
if (this.token) h.Authorization = `Bearer ${this.token}`;
|
|
127
|
+
return h;
|
|
128
|
+
}
|
|
129
|
+
_req(method, p, body) {
|
|
130
|
+
const http = require('http');
|
|
131
|
+
const url = new URL(this.base + p);
|
|
132
|
+
const payload = body === undefined ? null : JSON.stringify(body);
|
|
133
|
+
const res = http.request({
|
|
134
|
+
hostname: url.hostname, port: url.port || 80, path: url.pathname + url.search,
|
|
135
|
+
method, headers: { ...this._headers(), ...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}) },
|
|
136
|
+
});
|
|
137
|
+
// synchronous-feeling via Atomics.wait on a SharedArrayBuffer flag
|
|
138
|
+
const flag = new Int32Array(new SharedArrayBuffer(4));
|
|
139
|
+
let out = { status: 0, text: '' };
|
|
140
|
+
const r2 = res;
|
|
141
|
+
r2.on('response', (r) => {
|
|
142
|
+
let t = '';
|
|
143
|
+
r.on('data', (c) => { t += c; });
|
|
144
|
+
r.on('end', () => { out = { status: r.statusCode, text: t }; Atomics.store(flag, 0, 1); Atomics.notify(flag, 0); });
|
|
145
|
+
});
|
|
146
|
+
r2.on('error', (e) => { out = { status: 0, text: String(e) }; Atomics.store(flag, 0, 1); Atomics.notify(flag, 0); });
|
|
147
|
+
if (payload) r2.write(payload);
|
|
148
|
+
r2.end();
|
|
149
|
+
Atomics.wait(flag, 0, 0);
|
|
150
|
+
if (out.status === 0) throw new Error(`nedbd ${method} ${p} failed: ${out.text}`);
|
|
151
|
+
let parsed;
|
|
152
|
+
try { parsed = JSON.parse(out.text); } catch { parsed = { raw: out.text }; }
|
|
153
|
+
if (out.status >= 400) throw new Error(`nedbd ${method} ${p} → HTTP ${out.status}: ${out.text.slice(0, 200)}`);
|
|
154
|
+
return parsed;
|
|
155
|
+
}
|
|
156
|
+
_db(suffix) { return `/v1/databases/${this.name}${suffix || ''}`; }
|
|
157
|
+
_ensureDb() {
|
|
158
|
+
try { this._req('GET', this._db()); } catch (e) {
|
|
159
|
+
if (String(e).includes('404')) this._req('POST', '/v1/databases', { name: this.name });
|
|
160
|
+
else throw e;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
put(coll, id, doc, kw = {}) {
|
|
164
|
+
const payload = { coll, id, doc };
|
|
165
|
+
for (const k of ['client', 'nonce', 'idem', 'evidence', 'confidence', 'valid_from', 'valid_to', 'caused_by']) {
|
|
166
|
+
if (kw[k] !== undefined && kw[k] !== null) payload[k] = kw[k];
|
|
167
|
+
}
|
|
168
|
+
const r = this._req('POST', this._db('/put'), payload);
|
|
169
|
+
return r.doc !== undefined ? r.doc : doc;
|
|
170
|
+
}
|
|
171
|
+
get(coll, id, asOf) {
|
|
172
|
+
const clause = asOf !== undefined && asOf !== null ? ` AS OF ${asOf}` : '';
|
|
173
|
+
const rows = this.query(`FROM ${coll}${clause} WHERE _id = "${id}"`);
|
|
174
|
+
return rows.length ? rows[0] : null;
|
|
175
|
+
}
|
|
176
|
+
query(nql) { return this._req('POST', this._db('/query'), { nql }).rows || []; }
|
|
177
|
+
createIndex(coll, field, kind) { this._req('POST', this._db('/index'), { coll, field, kind: kind || 'eq' }); }
|
|
178
|
+
delete(coll, id) { this._req('DELETE', `/v1/databases/${this.name}/rows/${coll}/${id}`); }
|
|
179
|
+
link(frm, rel, to) {
|
|
180
|
+
try { this._req('POST', this._db('/link'), { frm, rel, to }); }
|
|
181
|
+
catch (e) {
|
|
182
|
+
if (String(e).includes('404') || String(e).toLowerCase().includes('not found')) {
|
|
183
|
+
this.put('__links__', `${frm}|${rel}|${to}`, { _from: frm, _rel: rel, _to: to });
|
|
184
|
+
} else throw e;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
unlink(frm, rel, to) {
|
|
188
|
+
try { this._req('DELETE', `/v1/databases/${this.name}/links/${frm}/${rel}/${to}`); }
|
|
189
|
+
catch (_) { try { this._req('DELETE', `/v1/databases/${this.name}/rows/__links__/${frm}|${rel}|${to}`); } catch (_) {} }
|
|
190
|
+
}
|
|
191
|
+
neighbors(frm, rel, asOf) {
|
|
192
|
+
const clause = asOf !== undefined && asOf !== null ? ` AS OF ${asOf}` : '';
|
|
193
|
+
const [c] = frm.split(':');
|
|
194
|
+
const rows = this.query(`FROM ${c}${clause} WHERE _id = "${frm.split(':')[1] || ''}" TRAVERSE ${rel}`);
|
|
195
|
+
return rows.filter((r) => r._id).map((r) => `${r._coll || c}:${r._id}`);
|
|
196
|
+
}
|
|
197
|
+
inbound(to, rel, asOf) {
|
|
198
|
+
const clause = asOf !== undefined && asOf !== null ? ` AS OF ${asOf}` : '';
|
|
199
|
+
const [c] = to.split(':');
|
|
200
|
+
try {
|
|
201
|
+
const rows = this.query(`FROM ${c} WHERE _id = "${to.split(':')[1] || ''}" TRAVERSE ${rel} REVERSE`);
|
|
202
|
+
return rows.filter((r) => r._id).map((r) => `${r._coll || c}:${r._id}`);
|
|
203
|
+
} catch (_) { return []; }
|
|
204
|
+
}
|
|
205
|
+
verify() { return this._req('GET', this._db('/verify')).ok === true; }
|
|
206
|
+
get head() { return this._req('GET', this._db()).head || '0'.repeat(64); }
|
|
207
|
+
get seq() { return this._req('GET', this._db()).seq || 0; }
|
|
208
|
+
checkpoint() { return this._req('POST', this._db('/checkpoint')).head || this.head; }
|
|
209
|
+
get engineKind() { return 'nedbd-http'; }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── collection mapping (key/table glob → NEDB collection) ───────────────────
|
|
213
|
+
|
|
214
|
+
class CollectionMapping {
|
|
215
|
+
constructor(pattern, collection, opts = {}) {
|
|
216
|
+
this.pattern = pattern;
|
|
217
|
+
this.collection = collection;
|
|
218
|
+
this.idExtractor = opts.idExtractor || ((k) => k.split(':').pop());
|
|
219
|
+
this.valueParser = opts.valueParser || CollectionMapping.defaultParse;
|
|
220
|
+
this.valueType = opts.valueType || 'string';
|
|
221
|
+
}
|
|
222
|
+
static defaultParse(v) {
|
|
223
|
+
if (v === null || v === undefined) return { _v: null };
|
|
224
|
+
if (typeof v === 'object') return v;
|
|
225
|
+
const s = String(v);
|
|
226
|
+
try {
|
|
227
|
+
const p = JSON.parse(s);
|
|
228
|
+
return (p && typeof p === 'object') ? p : { _v: p };
|
|
229
|
+
} catch { return { _v: s }; }
|
|
230
|
+
}
|
|
231
|
+
matches(key) {
|
|
232
|
+
// translate a glob to a RegExp (tiny, no deps)
|
|
233
|
+
const rx = new RegExp('^' + this.pattern
|
|
234
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
235
|
+
.replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
|
|
236
|
+
return rx.test(key);
|
|
237
|
+
}
|
|
238
|
+
extractId(key) { return this.idExtractor(key); }
|
|
239
|
+
parseValue(v) { return this.valueParser(v); }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── the shared surface (the `.nedb` attribute) ──────────────────────────────
|
|
243
|
+
|
|
244
|
+
class WrapSurface {
|
|
245
|
+
constructor(dbName, engine) {
|
|
246
|
+
this.dbName = dbName;
|
|
247
|
+
this.engine = engine;
|
|
248
|
+
this.mappings = [];
|
|
249
|
+
this.shadowWrites = false;
|
|
250
|
+
this.backfilled = false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// host hooks — override in host adapters
|
|
254
|
+
hostScan(/* mapping, batchSize */) { return []; }
|
|
255
|
+
shadowDoc(/* mapping, cmd, args */) { return null; }
|
|
256
|
+
|
|
257
|
+
register(pattern, collection, opts) {
|
|
258
|
+
this.mappings.push(new CollectionMapping(pattern, collection, opts));
|
|
259
|
+
return this;
|
|
260
|
+
}
|
|
261
|
+
mappingFor(key) { return this.mappings.find((m) => m.matches(key)) || null; }
|
|
262
|
+
|
|
263
|
+
backfill(opts = {}) {
|
|
264
|
+
const mappings = opts.pattern
|
|
265
|
+
? [new CollectionMapping(opts.pattern, opts.collection || opts.pattern.split(':')[0], opts)]
|
|
266
|
+
: this.mappings;
|
|
267
|
+
let total = 0;
|
|
268
|
+
for (const m of mappings) {
|
|
269
|
+
for (const [key, raw] of this.hostScan(m, opts.batchSize || 200)) {
|
|
270
|
+
try {
|
|
271
|
+
const doc = m.parseValue(raw);
|
|
272
|
+
doc._source = 'backfill';
|
|
273
|
+
this.engine.put(m.collection, m.extractId(key), doc);
|
|
274
|
+
total += 1;
|
|
275
|
+
} catch (_) { /* skip unreadable */ }
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
this.backfilled = true;
|
|
279
|
+
return total;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
shadow(cmd, key, ...rest) {
|
|
283
|
+
if (!this.shadowWrites) return;
|
|
284
|
+
try {
|
|
285
|
+
const m = this.mappingFor(key);
|
|
286
|
+
if (!m) {
|
|
287
|
+
this.engine.put('__shadow_raw__', key, { cmd, key, _source: 'shadow_raw' });
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
// rest = the host call's arguments AFTER the key (value(s) / fields)
|
|
291
|
+
const doc = this.shadowDoc(m, cmd, rest);
|
|
292
|
+
if (!doc) return;
|
|
293
|
+
doc._source = 'shadow';
|
|
294
|
+
const id = m.extractId(key);
|
|
295
|
+
// merge over the existing doc (hset/incr are incremental by nature;
|
|
296
|
+
// set replaces — the adapter marks replacement with doc.__replace)
|
|
297
|
+
const prev = this.engine.get(m.collection, id);
|
|
298
|
+
const merged = doc.__replace || !prev ? doc : { ...prev, ...doc };
|
|
299
|
+
delete merged.__replace;
|
|
300
|
+
this.engine.put(m.collection, id, merged);
|
|
301
|
+
} catch (e) { if (process.env.NEDB_WRAP_DEBUG) console.error('[nedb shadow]', e); }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── full NEDB API ──────────────────────────────────────────────────────────
|
|
305
|
+
put(coll, id, doc, kw) { return this.engine.put(coll, id, doc, kw); }
|
|
306
|
+
get(coll, id, asOf) { return this.engine.get(coll, id, asOf); }
|
|
307
|
+
query(nql) { return this.engine.query(nql); }
|
|
308
|
+
createIndex(coll, field, kind) { this.engine.createIndex(coll, field, kind); }
|
|
309
|
+
delete(coll, id) { return this.engine.delete(coll, id); }
|
|
310
|
+
link(frm, rel, to) { return this.engine.link(frm, rel, to); }
|
|
311
|
+
unlink(frm, rel, to) { return this.engine.unlink(frm, rel, to); }
|
|
312
|
+
neighbors(frm, rel, asOf) { return this.engine.neighbors(frm, rel, asOf); }
|
|
313
|
+
inbound(to, rel, asOf) { return this.engine.inbound(to, rel, asOf); }
|
|
314
|
+
verify() { return this.engine.verify(); }
|
|
315
|
+
get head() { return this.engine.head; }
|
|
316
|
+
get seq() { return this.engine.seq; }
|
|
317
|
+
checkpoint() { return this.engine.checkpoint(); }
|
|
318
|
+
tip() { return this.engine.tip ? this.engine.tip() : null; }
|
|
319
|
+
since(afterSeq, limit) { return this.engine.since(afterSeq, limit); }
|
|
320
|
+
scanStatus() { return this.engine.scanStatus(); }
|
|
321
|
+
get engineKind() { return this.engine.engineKind; }
|
|
322
|
+
|
|
323
|
+
[Symbol.for('nodejs.util.inspect.custom')]() {
|
|
324
|
+
return `<WrapSurface db=${JSON.stringify(this.dbName)} engine=${this.engineKind} mappings=${this.mappings.length}>`;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
module.exports = {
|
|
329
|
+
openEngine, NativeEngine, NedbdProxy, CollectionMapping, WrapSurface, resolveNative,
|
|
330
|
+
};
|