cypherlite 2.0.0 → 2.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.
Binary file
Binary file
Binary file
Binary file
Binary file
package/index.js CHANGED
@@ -1,20 +1,320 @@
1
- // Auto-generated loader for the CypherLite native addon.
2
- // This file loads the platform-specific .node binary.
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+ /* prettier-ignore */
3
4
 
4
- const { existsSync } = require('node:fs');
5
- const { join } = require('node:path');
5
+ /* auto-generated by NAPI-RS */
6
6
 
7
- // Try to load the .node binary from the current directory.
8
- const localPath = join(__dirname, 'cypherlite.node');
7
+ const { existsSync, readFileSync } = require('fs')
8
+ const { join } = require('path')
9
9
 
10
- let nativeBinding;
10
+ const { platform, arch } = process
11
11
 
12
- if (existsSync(localPath)) {
13
- nativeBinding = require(localPath);
14
- } else {
15
- throw new Error(
16
- `Failed to load cypherlite native module. Expected at: ${localPath}`
17
- );
12
+ let nativeBinding = null
13
+ let localFileExisted = false
14
+ let loadError = null
15
+
16
+ function isMusl() {
17
+ // For Node 10
18
+ if (!process.report || typeof process.report.getReport !== 'function') {
19
+ try {
20
+ const lddPath = require('child_process').execSync('which ldd').toString().trim()
21
+ return readFileSync(lddPath, 'utf8').includes('musl')
22
+ } catch (e) {
23
+ return true
24
+ }
25
+ } else {
26
+ const { glibcVersionRuntime } = process.report.getReport().header
27
+ return !glibcVersionRuntime
28
+ }
29
+ }
30
+
31
+ switch (platform) {
32
+ case 'android':
33
+ switch (arch) {
34
+ case 'arm64':
35
+ localFileExisted = existsSync(join(__dirname, 'cypherlite.android-arm64.node'))
36
+ try {
37
+ if (localFileExisted) {
38
+ nativeBinding = require('./cypherlite.android-arm64.node')
39
+ } else {
40
+ nativeBinding = require('cypherlite-android-arm64')
41
+ }
42
+ } catch (e) {
43
+ loadError = e
44
+ }
45
+ break
46
+ case 'arm':
47
+ localFileExisted = existsSync(join(__dirname, 'cypherlite.android-arm-eabi.node'))
48
+ try {
49
+ if (localFileExisted) {
50
+ nativeBinding = require('./cypherlite.android-arm-eabi.node')
51
+ } else {
52
+ nativeBinding = require('cypherlite-android-arm-eabi')
53
+ }
54
+ } catch (e) {
55
+ loadError = e
56
+ }
57
+ break
58
+ default:
59
+ throw new Error(`Unsupported architecture on Android ${arch}`)
60
+ }
61
+ break
62
+ case 'win32':
63
+ switch (arch) {
64
+ case 'x64':
65
+ localFileExisted = existsSync(
66
+ join(__dirname, 'cypherlite.win32-x64-msvc.node')
67
+ )
68
+ try {
69
+ if (localFileExisted) {
70
+ nativeBinding = require('./cypherlite.win32-x64-msvc.node')
71
+ } else {
72
+ nativeBinding = require('cypherlite-win32-x64-msvc')
73
+ }
74
+ } catch (e) {
75
+ loadError = e
76
+ }
77
+ break
78
+ case 'ia32':
79
+ localFileExisted = existsSync(
80
+ join(__dirname, 'cypherlite.win32-ia32-msvc.node')
81
+ )
82
+ try {
83
+ if (localFileExisted) {
84
+ nativeBinding = require('./cypherlite.win32-ia32-msvc.node')
85
+ } else {
86
+ nativeBinding = require('cypherlite-win32-ia32-msvc')
87
+ }
88
+ } catch (e) {
89
+ loadError = e
90
+ }
91
+ break
92
+ case 'arm64':
93
+ localFileExisted = existsSync(
94
+ join(__dirname, 'cypherlite.win32-arm64-msvc.node')
95
+ )
96
+ try {
97
+ if (localFileExisted) {
98
+ nativeBinding = require('./cypherlite.win32-arm64-msvc.node')
99
+ } else {
100
+ nativeBinding = require('cypherlite-win32-arm64-msvc')
101
+ }
102
+ } catch (e) {
103
+ loadError = e
104
+ }
105
+ break
106
+ default:
107
+ throw new Error(`Unsupported architecture on Windows: ${arch}`)
108
+ }
109
+ break
110
+ case 'darwin':
111
+ localFileExisted = existsSync(join(__dirname, 'cypherlite.darwin-universal.node'))
112
+ try {
113
+ if (localFileExisted) {
114
+ nativeBinding = require('./cypherlite.darwin-universal.node')
115
+ } else {
116
+ nativeBinding = require('cypherlite-darwin-universal')
117
+ }
118
+ break
119
+ } catch {}
120
+ switch (arch) {
121
+ case 'x64':
122
+ localFileExisted = existsSync(join(__dirname, 'cypherlite.darwin-x64.node'))
123
+ try {
124
+ if (localFileExisted) {
125
+ nativeBinding = require('./cypherlite.darwin-x64.node')
126
+ } else {
127
+ nativeBinding = require('cypherlite-darwin-x64')
128
+ }
129
+ } catch (e) {
130
+ loadError = e
131
+ }
132
+ break
133
+ case 'arm64':
134
+ localFileExisted = existsSync(
135
+ join(__dirname, 'cypherlite.darwin-arm64.node')
136
+ )
137
+ try {
138
+ if (localFileExisted) {
139
+ nativeBinding = require('./cypherlite.darwin-arm64.node')
140
+ } else {
141
+ nativeBinding = require('cypherlite-darwin-arm64')
142
+ }
143
+ } catch (e) {
144
+ loadError = e
145
+ }
146
+ break
147
+ default:
148
+ throw new Error(`Unsupported architecture on macOS: ${arch}`)
149
+ }
150
+ break
151
+ case 'freebsd':
152
+ if (arch !== 'x64') {
153
+ throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
154
+ }
155
+ localFileExisted = existsSync(join(__dirname, 'cypherlite.freebsd-x64.node'))
156
+ try {
157
+ if (localFileExisted) {
158
+ nativeBinding = require('./cypherlite.freebsd-x64.node')
159
+ } else {
160
+ nativeBinding = require('cypherlite-freebsd-x64')
161
+ }
162
+ } catch (e) {
163
+ loadError = e
164
+ }
165
+ break
166
+ case 'linux':
167
+ switch (arch) {
168
+ case 'x64':
169
+ if (isMusl()) {
170
+ localFileExisted = existsSync(
171
+ join(__dirname, 'cypherlite.linux-x64-musl.node')
172
+ )
173
+ try {
174
+ if (localFileExisted) {
175
+ nativeBinding = require('./cypherlite.linux-x64-musl.node')
176
+ } else {
177
+ nativeBinding = require('cypherlite-linux-x64-musl')
178
+ }
179
+ } catch (e) {
180
+ loadError = e
181
+ }
182
+ } else {
183
+ localFileExisted = existsSync(
184
+ join(__dirname, 'cypherlite.linux-x64-gnu.node')
185
+ )
186
+ try {
187
+ if (localFileExisted) {
188
+ nativeBinding = require('./cypherlite.linux-x64-gnu.node')
189
+ } else {
190
+ nativeBinding = require('cypherlite-linux-x64-gnu')
191
+ }
192
+ } catch (e) {
193
+ loadError = e
194
+ }
195
+ }
196
+ break
197
+ case 'arm64':
198
+ if (isMusl()) {
199
+ localFileExisted = existsSync(
200
+ join(__dirname, 'cypherlite.linux-arm64-musl.node')
201
+ )
202
+ try {
203
+ if (localFileExisted) {
204
+ nativeBinding = require('./cypherlite.linux-arm64-musl.node')
205
+ } else {
206
+ nativeBinding = require('cypherlite-linux-arm64-musl')
207
+ }
208
+ } catch (e) {
209
+ loadError = e
210
+ }
211
+ } else {
212
+ localFileExisted = existsSync(
213
+ join(__dirname, 'cypherlite.linux-arm64-gnu.node')
214
+ )
215
+ try {
216
+ if (localFileExisted) {
217
+ nativeBinding = require('./cypherlite.linux-arm64-gnu.node')
218
+ } else {
219
+ nativeBinding = require('cypherlite-linux-arm64-gnu')
220
+ }
221
+ } catch (e) {
222
+ loadError = e
223
+ }
224
+ }
225
+ break
226
+ case 'arm':
227
+ if (isMusl()) {
228
+ localFileExisted = existsSync(
229
+ join(__dirname, 'cypherlite.linux-arm-musleabihf.node')
230
+ )
231
+ try {
232
+ if (localFileExisted) {
233
+ nativeBinding = require('./cypherlite.linux-arm-musleabihf.node')
234
+ } else {
235
+ nativeBinding = require('cypherlite-linux-arm-musleabihf')
236
+ }
237
+ } catch (e) {
238
+ loadError = e
239
+ }
240
+ } else {
241
+ localFileExisted = existsSync(
242
+ join(__dirname, 'cypherlite.linux-arm-gnueabihf.node')
243
+ )
244
+ try {
245
+ if (localFileExisted) {
246
+ nativeBinding = require('./cypherlite.linux-arm-gnueabihf.node')
247
+ } else {
248
+ nativeBinding = require('cypherlite-linux-arm-gnueabihf')
249
+ }
250
+ } catch (e) {
251
+ loadError = e
252
+ }
253
+ }
254
+ break
255
+ case 'riscv64':
256
+ if (isMusl()) {
257
+ localFileExisted = existsSync(
258
+ join(__dirname, 'cypherlite.linux-riscv64-musl.node')
259
+ )
260
+ try {
261
+ if (localFileExisted) {
262
+ nativeBinding = require('./cypherlite.linux-riscv64-musl.node')
263
+ } else {
264
+ nativeBinding = require('cypherlite-linux-riscv64-musl')
265
+ }
266
+ } catch (e) {
267
+ loadError = e
268
+ }
269
+ } else {
270
+ localFileExisted = existsSync(
271
+ join(__dirname, 'cypherlite.linux-riscv64-gnu.node')
272
+ )
273
+ try {
274
+ if (localFileExisted) {
275
+ nativeBinding = require('./cypherlite.linux-riscv64-gnu.node')
276
+ } else {
277
+ nativeBinding = require('cypherlite-linux-riscv64-gnu')
278
+ }
279
+ } catch (e) {
280
+ loadError = e
281
+ }
282
+ }
283
+ break
284
+ case 's390x':
285
+ localFileExisted = existsSync(
286
+ join(__dirname, 'cypherlite.linux-s390x-gnu.node')
287
+ )
288
+ try {
289
+ if (localFileExisted) {
290
+ nativeBinding = require('./cypherlite.linux-s390x-gnu.node')
291
+ } else {
292
+ nativeBinding = require('cypherlite-linux-s390x-gnu')
293
+ }
294
+ } catch (e) {
295
+ loadError = e
296
+ }
297
+ break
298
+ default:
299
+ throw new Error(`Unsupported architecture on Linux: ${arch}`)
300
+ }
301
+ break
302
+ default:
303
+ throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
18
304
  }
19
305
 
20
- module.exports = nativeBinding;
306
+ if (!nativeBinding) {
307
+ if (loadError) {
308
+ throw loadError
309
+ }
310
+ throw new Error(`Failed to load native binding`)
311
+ }
312
+
313
+ const { Database, open, CylResult, Transaction, version, features } = nativeBinding
314
+
315
+ module.exports.Database = Database
316
+ module.exports.open = open
317
+ module.exports.CylResult = CylResult
318
+ module.exports.Transaction = Transaction
319
+ module.exports.version = version
320
+ module.exports.features = features
package/lib.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ // Augmented type definitions for CypherLite Node.js bindings.
2
+ // index.d.ts is auto-generated by napi-rs on each build;
3
+ // this file adds the Symbol.iterator type applied at runtime by lib.js.
4
+
5
+ export * from './index.js';
6
+
7
+ declare module './index.js' {
8
+ interface CylResult extends Iterable<Record<string, any>> {
9
+ [Symbol.iterator](): Iterator<Record<string, any>>;
10
+ }
11
+ }
package/lib.js ADDED
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ // Wrapper around the napi-rs auto-generated index.js entry point.
4
+ // index.js is regenerated by `napi build --platform` and cannot be
5
+ // modified directly. This file patches runtime behavior (Symbol.iterator)
6
+ // and serves as the package entry point ("main" in package.json).
7
+ // CJS is required here because index.js is CommonJS.
8
+
9
+ const binding = require('./index.js');
10
+
11
+ // Add Symbol.iterator to CylResult so for...of, spread, and
12
+ // Array.from work on query results.
13
+ binding.CylResult.prototype[Symbol.iterator] = function* () {
14
+ for (let i = 0; i < this.length; i++) {
15
+ yield this.row(i);
16
+ }
17
+ };
18
+
19
+ module.exports = binding;
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "cypherlite",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Lightweight embedded graph database with Cypher query support",
5
- "main": "index.js",
6
- "types": "index.d.ts",
5
+ "main": "lib.js",
6
+ "types": "lib.d.ts",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "https://github.com/Epsilondelta-ai/CypherLite.git"
@@ -21,13 +21,24 @@
21
21
  "graph-database",
22
22
  "napi-rs"
23
23
  ],
24
+ "files": [
25
+ "lib.js",
26
+ "lib.d.ts",
27
+ "index.js",
28
+ "index.d.ts",
29
+ "README.md",
30
+ "*.node"
31
+ ],
24
32
  "napi": {
25
33
  "name": "cypherlite",
26
- "triples": {}
34
+ "triples": {
35
+ "defaults": true,
36
+ "additional": ["aarch64-unknown-linux-gnu"]
37
+ }
27
38
  },
28
39
  "scripts": {
29
- "build": "napi build --release",
30
- "build:debug": "napi build",
40
+ "build": "napi build --platform --release",
41
+ "build:debug": "napi build --platform",
31
42
  "test": "vitest run"
32
43
  },
33
44
  "devDependencies": {
package/Cargo.toml DELETED
@@ -1,28 +0,0 @@
1
- [package]
2
- name = "cypherlite-node"
3
- version = "2.0.0"
4
- edition = "2021"
5
- rust-version = "1.84"
6
- description = "Node.js bindings for CypherLite embedded graph database via napi-rs"
7
- publish = false
8
-
9
- [lib]
10
- crate-type = ["cdylib"]
11
-
12
- [features]
13
- default = ["temporal-core"]
14
- temporal-core = ["cypherlite-query/temporal-core", "cypherlite-core/temporal-core"]
15
- temporal-edge = ["temporal-core", "cypherlite-query/temporal-edge", "cypherlite-core/temporal-edge"]
16
- subgraph = ["temporal-edge", "cypherlite-query/subgraph", "cypherlite-core/subgraph"]
17
- hypergraph = ["subgraph", "cypherlite-query/hypergraph", "cypherlite-core/hypergraph"]
18
- full-temporal = ["hypergraph", "cypherlite-query/full-temporal", "cypherlite-core/full-temporal"]
19
- plugin = ["cypherlite-query/plugin", "cypherlite-core/plugin"]
20
-
21
- [dependencies]
22
- napi = { version = "2", features = ["napi9"] }
23
- napi-derive = "2"
24
- cypherlite-query = { version = "2.0.0", path = "../cypherlite-query" }
25
- cypherlite-core = { version = "2.0.0", path = "../cypherlite-core" }
26
-
27
- [build-dependencies]
28
- napi-build = "2"
@@ -1,405 +0,0 @@
1
- // CypherLite Node.js bindings specification tests.
2
- //
3
- // These tests define the expected behavior of the napi-rs bindings.
4
- // They cover: version/features, database lifecycle, query execution,
5
- // parameter binding, transactions, result access, value types, and errors.
6
-
7
- import { describe, it, expect, afterEach } from 'vitest';
8
- import { tmpdir } from 'node:os';
9
- import { mkdtemp, rm } from 'node:fs/promises';
10
- import { join } from 'node:path';
11
-
12
- // The native module is loaded from the build output.
13
- import {
14
- version,
15
- features,
16
- open,
17
- Database,
18
- CylResult,
19
- Transaction,
20
- } from '../index.js';
21
-
22
- // Helper: create a temporary directory for each test.
23
- async function createTempDir() {
24
- return mkdtemp(join(tmpdir(), 'cypherlite-node-test-'));
25
- }
26
-
27
- // Track temp dirs for cleanup.
28
- const tempDirs = [];
29
-
30
- afterEach(async () => {
31
- for (const dir of tempDirs) {
32
- await rm(dir, { recursive: true, force: true }).catch(() => {});
33
- }
34
- tempDirs.length = 0;
35
- });
36
-
37
- async function openTempDb(options) {
38
- const dir = await createTempDir();
39
- tempDirs.push(dir);
40
- const dbPath = join(dir, 'test.cyl');
41
- return open(dbPath, options);
42
- }
43
-
44
- // ============================================================
45
- // M1: Version and Features
46
- // ============================================================
47
-
48
- describe('version and features', () => {
49
- it('should return a version string', () => {
50
- const v = version();
51
- expect(typeof v).toBe('string');
52
- expect(v).toMatch(/^\d+\.\d+\.\d+$/);
53
- });
54
-
55
- it('should return a features string', () => {
56
- const f = features();
57
- expect(typeof f).toBe('string');
58
- // At minimum, temporal-core should be present (default feature).
59
- expect(f).toContain('temporal-core');
60
- });
61
- });
62
-
63
- // ============================================================
64
- // M2: Database Lifecycle
65
- // ============================================================
66
-
67
- describe('database lifecycle', () => {
68
- it('should open a database', async () => {
69
- const db = await openTempDb();
70
- expect(db).toBeInstanceOf(Database);
71
- db.close();
72
- });
73
-
74
- it('should open with custom options', async () => {
75
- const db = await openTempDb({ pageSize: 4096, cacheCapacity: 128 });
76
- expect(db).toBeInstanceOf(Database);
77
- db.close();
78
- });
79
-
80
- it('should report isClosed correctly', async () => {
81
- const db = await openTempDb();
82
- expect(db.isClosed).toBe(false);
83
- db.close();
84
- expect(db.isClosed).toBe(true);
85
- });
86
-
87
- it('should allow close to be called multiple times', async () => {
88
- const db = await openTempDb();
89
- db.close();
90
- expect(() => db.close()).not.toThrow();
91
- });
92
-
93
- it('should throw on execute after close', async () => {
94
- const db = await openTempDb();
95
- db.close();
96
- expect(() => db.execute("MATCH (n) RETURN n")).toThrow(/closed/i);
97
- });
98
- });
99
-
100
- // ============================================================
101
- // M3: Query Execution
102
- // ============================================================
103
-
104
- describe('query execution', () => {
105
- it('should execute CREATE and MATCH', async () => {
106
- const db = await openTempDb();
107
- db.execute("CREATE (n:Person {name: 'Alice', age: 30})");
108
- const result = db.execute("MATCH (n:Person) RETURN n.name, n.age");
109
- expect(result).toBeInstanceOf(CylResult);
110
- expect(result.length).toBe(1);
111
- const row = result.row(0);
112
- expect(row['n.name']).toBe('Alice');
113
- expect(row['n.age']).toBe(30);
114
- db.close();
115
- });
116
-
117
- it('should return empty result for non-existent label', async () => {
118
- const db = await openTempDb();
119
- const result = db.execute("MATCH (n:Ghost) RETURN n");
120
- expect(result.length).toBe(0);
121
- db.close();
122
- });
123
-
124
- it('should execute with parameters', async () => {
125
- const db = await openTempDb();
126
- db.execute("CREATE (n:Person {name: 'Alice'})");
127
- const result = db.execute(
128
- "MATCH (n:Person) WHERE n.name = $name RETURN n.name",
129
- { name: 'Alice' }
130
- );
131
- expect(result.length).toBe(1);
132
- expect(result.row(0)['n.name']).toBe('Alice');
133
- db.close();
134
- });
135
-
136
- it('should handle multiple rows', async () => {
137
- const db = await openTempDb();
138
- db.execute("CREATE (n:Person {name: 'Alice'})");
139
- db.execute("CREATE (n:Person {name: 'Bob'})");
140
- db.execute("CREATE (n:Person {name: 'Charlie'})");
141
- const result = db.execute("MATCH (n:Person) RETURN n.name");
142
- expect(result.length).toBe(3);
143
- const names = result.toArray().map(r => r['n.name']).sort();
144
- expect(names).toEqual(['Alice', 'Bob', 'Charlie']);
145
- db.close();
146
- });
147
-
148
- it('should handle SET then MATCH', async () => {
149
- const db = await openTempDb();
150
- db.execute("CREATE (n:Person {name: 'Alice', age: 25})");
151
- db.execute("MATCH (n:Person) SET n.age = 30");
152
- const result = db.execute("MATCH (n:Person) RETURN n.age");
153
- expect(result.row(0)['n.age']).toBe(30);
154
- db.close();
155
- });
156
-
157
- it('should handle DETACH DELETE', async () => {
158
- const db = await openTempDb();
159
- db.execute("CREATE (a:Person {name: 'Alice'})-[:KNOWS]->(b:Person {name: 'Bob'})");
160
- db.execute("MATCH (n:Person) DETACH DELETE n");
161
- const result = db.execute("MATCH (n:Person) RETURN n");
162
- expect(result.length).toBe(0);
163
- db.close();
164
- });
165
- });
166
-
167
- // ============================================================
168
- // M4: Transaction Support
169
- // ============================================================
170
-
171
- describe('transactions', () => {
172
- it('should begin and commit a transaction', async () => {
173
- const db = await openTempDb();
174
- const tx = db.begin();
175
- expect(tx).toBeInstanceOf(Transaction);
176
- tx.execute("CREATE (n:Person {name: 'Alice'})");
177
- tx.commit();
178
- const result = db.execute("MATCH (n:Person) RETURN n.name");
179
- expect(result.length).toBe(1);
180
- expect(result.row(0)['n.name']).toBe('Alice');
181
- db.close();
182
- });
183
-
184
- it('should begin and rollback a transaction', async () => {
185
- const db = await openTempDb();
186
- const tx = db.begin();
187
- tx.execute("CREATE (n:Person {name: 'Bob'})");
188
- tx.rollback();
189
- // After rollback (Phase 2 no-op), data may still exist.
190
- // The important thing is no error is thrown.
191
- db.close();
192
- });
193
-
194
- it('should throw on execute after commit', async () => {
195
- const db = await openTempDb();
196
- const tx = db.begin();
197
- tx.commit();
198
- expect(() => tx.execute("CREATE (n:Person {name: 'X'})")).toThrow(/finished/i);
199
- db.close();
200
- });
201
-
202
- it('should throw on execute after rollback', async () => {
203
- const db = await openTempDb();
204
- const tx = db.begin();
205
- tx.rollback();
206
- expect(() => tx.execute("CREATE (n:Person {name: 'X'})")).toThrow(/finished/i);
207
- db.close();
208
- });
209
-
210
- it('should throw on double commit', async () => {
211
- const db = await openTempDb();
212
- const tx = db.begin();
213
- tx.commit();
214
- expect(() => tx.commit()).toThrow(/finished/i);
215
- db.close();
216
- });
217
-
218
- it('should execute with params in transaction', async () => {
219
- const db = await openTempDb();
220
- const tx = db.begin();
221
- tx.execute("CREATE (n:Person {name: 'Alice'})");
222
- const result = tx.execute(
223
- "MATCH (n:Person) WHERE n.name = $name RETURN n.name",
224
- { name: 'Alice' }
225
- );
226
- expect(result.length).toBe(1);
227
- tx.commit();
228
- db.close();
229
- });
230
- });
231
-
232
- // ============================================================
233
- // M5: Result and Row Access
234
- // ============================================================
235
-
236
- describe('result access', () => {
237
- it('should expose column names', async () => {
238
- const db = await openTempDb();
239
- db.execute("CREATE (n:Person {name: 'Alice', age: 30})");
240
- const result = db.execute("MATCH (n:Person) RETURN n.name, n.age");
241
- const cols = result.columns;
242
- expect(Array.isArray(cols)).toBe(true);
243
- expect(cols).toContain('n.name');
244
- expect(cols).toContain('n.age');
245
- db.close();
246
- });
247
-
248
- it('should expose row count via length', async () => {
249
- const db = await openTempDb();
250
- db.execute("CREATE (n:Person {name: 'Alice'})");
251
- db.execute("CREATE (n:Person {name: 'Bob'})");
252
- const result = db.execute("MATCH (n:Person) RETURN n.name");
253
- expect(result.length).toBe(2);
254
- db.close();
255
- });
256
-
257
- it('should access individual row by index', async () => {
258
- const db = await openTempDb();
259
- db.execute("CREATE (n:Person {name: 'Alice', age: 30})");
260
- const result = db.execute("MATCH (n:Person) RETURN n.name, n.age");
261
- const row = result.row(0);
262
- expect(typeof row).toBe('object');
263
- expect(row['n.name']).toBe('Alice');
264
- expect(row['n.age']).toBe(30);
265
- db.close();
266
- });
267
-
268
- it('should throw on out-of-range row index', async () => {
269
- const db = await openTempDb();
270
- const result = db.execute("MATCH (n:Ghost) RETURN n");
271
- expect(() => result.row(0)).toThrow(/out of range/i);
272
- db.close();
273
- });
274
-
275
- it('should convert all rows via toArray', async () => {
276
- const db = await openTempDb();
277
- db.execute("CREATE (n:Person {name: 'Alice'})");
278
- db.execute("CREATE (n:Person {name: 'Bob'})");
279
- const result = db.execute("MATCH (n:Person) RETURN n.name");
280
- const rows = result.toArray();
281
- expect(Array.isArray(rows)).toBe(true);
282
- expect(rows.length).toBe(2);
283
- const names = rows.map(r => r['n.name']).sort();
284
- expect(names).toEqual(['Alice', 'Bob']);
285
- db.close();
286
- });
287
- });
288
-
289
- // ============================================================
290
- // M6: Value Type Conversions
291
- // ============================================================
292
-
293
- describe('value types', () => {
294
- it('should handle null values', async () => {
295
- const db = await openTempDb();
296
- db.execute("CREATE (n:Person {name: 'Alice'})");
297
- const result = db.execute("MATCH (n:Person) RETURN n.name, n.email");
298
- const row = result.row(0);
299
- expect(row['n.name']).toBe('Alice');
300
- // Missing property should be null.
301
- expect(row['n.email']).toBeNull();
302
- db.close();
303
- });
304
-
305
- it('should handle boolean values', async () => {
306
- const db = await openTempDb();
307
- db.execute("CREATE (n:Flag {active: true})");
308
- const result = db.execute("MATCH (n:Flag) RETURN n.active");
309
- expect(result.row(0)['n.active']).toBe(true);
310
- db.close();
311
- });
312
-
313
- it('should handle integer values', async () => {
314
- const db = await openTempDb();
315
- db.execute("CREATE (n:Num {val: 42})");
316
- const result = db.execute("MATCH (n:Num) RETURN n.val");
317
- expect(result.row(0)['n.val']).toBe(42);
318
- db.close();
319
- });
320
-
321
- it('should handle float values', async () => {
322
- const db = await openTempDb();
323
- db.execute("CREATE (n:Num {val: 3.14})");
324
- const result = db.execute("MATCH (n:Num) RETURN n.val");
325
- expect(result.row(0)['n.val']).toBeCloseTo(3.14);
326
- db.close();
327
- });
328
-
329
- it('should handle string values', async () => {
330
- const db = await openTempDb();
331
- db.execute("CREATE (n:Text {val: 'hello world'})");
332
- const result = db.execute("MATCH (n:Text) RETURN n.val");
333
- expect(result.row(0)['n.val']).toBe('hello world');
334
- db.close();
335
- });
336
-
337
- it('should handle node ID as BigInt', async () => {
338
- const db = await openTempDb();
339
- db.execute("CREATE (n:Person {name: 'Alice'})");
340
- const result = db.execute("MATCH (n:Person) RETURN n");
341
- const row = result.row(0);
342
- // Node value should be a BigInt (node ID).
343
- expect(typeof row['n']).toBe('bigint');
344
- db.close();
345
- });
346
-
347
- it('should handle edge ID as BigInt', async () => {
348
- const db = await openTempDb();
349
- db.execute("CREATE (a:Person {name: 'A'})-[:KNOWS]->(b:Person {name: 'B'})");
350
- const result = db.execute("MATCH ()-[r:KNOWS]->() RETURN r");
351
- const row = result.row(0);
352
- // Edge value should be a BigInt (edge ID).
353
- expect(typeof row['r']).toBe('bigint');
354
- db.close();
355
- });
356
-
357
- it('should handle parameter types (string, int, float, bool, null)', async () => {
358
- const db = await openTempDb();
359
- db.execute("CREATE (n:Item {name: 'test'})");
360
- // String param
361
- let result = db.execute(
362
- "MATCH (n:Item) WHERE n.name = $v RETURN n.name",
363
- { v: 'test' }
364
- );
365
- expect(result.length).toBe(1);
366
- // Int param
367
- db.execute("CREATE (n:Num {val: 42})");
368
- result = db.execute(
369
- "MATCH (n:Num) WHERE n.val = $v RETURN n.val",
370
- { v: 42 }
371
- );
372
- expect(result.length).toBe(1);
373
- db.close();
374
- });
375
- });
376
-
377
- // ============================================================
378
- // Error Handling
379
- // ============================================================
380
-
381
- describe('error handling', () => {
382
- it('should throw on invalid Cypher syntax', async () => {
383
- const db = await openTempDb();
384
- expect(() => db.execute("INVALID QUERY @#$")).toThrow();
385
- db.close();
386
- });
387
-
388
- it('should throw on semantic error (undefined variable)', async () => {
389
- const db = await openTempDb();
390
- expect(() => db.execute("MATCH (n:Person) RETURN m.name")).toThrow();
391
- db.close();
392
- });
393
-
394
- it('should include error message in thrown error', async () => {
395
- const db = await openTempDb();
396
- try {
397
- db.execute("MATCH (n:Person RETURN n");
398
- expect.unreachable('should have thrown');
399
- } catch (e) {
400
- expect(e.message).toBeTruthy();
401
- expect(typeof e.message).toBe('string');
402
- }
403
- db.close();
404
- });
405
- });
package/build.rs DELETED
@@ -1,5 +0,0 @@
1
- extern crate napi_build;
2
-
3
- fn main() {
4
- napi_build::setup();
5
- }
package/src/database.rs DELETED
@@ -1,121 +0,0 @@
1
- // Database lifecycle and query execution for Node.js.
2
-
3
- use std::sync::atomic::{AtomicBool, Ordering};
4
- use std::sync::{Arc, Mutex};
5
-
6
- use napi::Env;
7
-
8
- use cypherlite_core::DatabaseConfig;
9
- use cypherlite_query::api::CypherLite;
10
-
11
- use crate::error::{db_closed, mutex_poisoned, to_napi_error};
12
- use crate::result::CylResult;
13
- use crate::transaction::Transaction;
14
- use crate::value::convert_params;
15
-
16
- /// Options for opening a database.
17
- #[napi(object)]
18
- pub struct OpenOptions {
19
- /// Page size in bytes (default: 4096).
20
- pub page_size: Option<u32>,
21
- /// Number of pages in the buffer pool cache (default: 256).
22
- pub cache_capacity: Option<u32>,
23
- }
24
-
25
- /// The main CypherLite database handle for Node.js.
26
- #[napi]
27
- pub struct Database {
28
- inner: Arc<Mutex<Option<CypherLite>>>,
29
- in_transaction: Arc<AtomicBool>,
30
- }
31
-
32
- /// Open a CypherLite database at the given path.
33
- #[napi]
34
- pub fn open(path: String, options: Option<OpenOptions>) -> napi::Result<Database> {
35
- let config = DatabaseConfig {
36
- path: std::path::PathBuf::from(&path),
37
- page_size: options.as_ref().and_then(|o| o.page_size).unwrap_or(4096),
38
- cache_capacity: options
39
- .as_ref()
40
- .and_then(|o| o.cache_capacity)
41
- .unwrap_or(256) as usize,
42
- ..Default::default()
43
- };
44
- let db = CypherLite::open(config).map_err(to_napi_error)?;
45
- Ok(Database {
46
- inner: Arc::new(Mutex::new(Some(db))),
47
- in_transaction: Arc::new(AtomicBool::new(false)),
48
- })
49
- }
50
-
51
- #[napi]
52
- impl Database {
53
- /// Execute a Cypher query string.
54
- ///
55
- /// Optional second argument provides named parameters as a plain object.
56
- #[napi]
57
- pub fn execute(
58
- &self,
59
- env: Env,
60
- query: String,
61
- params: Option<napi::JsObject>,
62
- ) -> napi::Result<CylResult> {
63
- if self.in_transaction.load(Ordering::SeqCst) {
64
- return Err(napi::Error::from_reason(
65
- "cannot execute on database while a transaction is active",
66
- ));
67
- }
68
- let rust_params = convert_params(&env, params)?;
69
- let mut guard = self.inner.lock().map_err(|_| mutex_poisoned())?;
70
- let db = guard.as_mut().ok_or_else(db_closed)?;
71
- let qr = if rust_params.is_empty() {
72
- db.execute(&query).map_err(to_napi_error)?
73
- } else {
74
- db.execute_with_params(&query, rust_params)
75
- .map_err(to_napi_error)?
76
- };
77
- Ok(CylResult::from_query_result(qr))
78
- }
79
-
80
- /// Close the database. Safe to call multiple times.
81
- #[napi]
82
- pub fn close(&self) -> napi::Result<()> {
83
- let mut guard = self.inner.lock().map_err(|_| mutex_poisoned())?;
84
- let _ = guard.take();
85
- Ok(())
86
- }
87
-
88
- /// Check if the database is closed.
89
- #[napi(getter, js_name = "isClosed")]
90
- pub fn is_closed(&self) -> napi::Result<bool> {
91
- let guard = self.inner.lock().map_err(|_| mutex_poisoned())?;
92
- Ok(guard.is_none())
93
- }
94
-
95
- /// Begin a new transaction.
96
- #[napi]
97
- pub fn begin(&self) -> napi::Result<Transaction> {
98
- // Check database is open.
99
- {
100
- let guard = self.inner.lock().map_err(|_| mutex_poisoned())?;
101
- if guard.is_none() {
102
- return Err(db_closed());
103
- }
104
- }
105
-
106
- // Check no transaction is already active.
107
- if self
108
- .in_transaction
109
- .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
110
- .is_err()
111
- {
112
- return Err(napi::Error::from_reason("a transaction is already active"));
113
- }
114
-
115
- Ok(Transaction {
116
- inner: Arc::clone(&self.inner),
117
- in_transaction: Arc::clone(&self.in_transaction),
118
- finished: false,
119
- })
120
- }
121
- }
package/src/error.rs DELETED
@@ -1,18 +0,0 @@
1
- // Error conversion from CypherLiteError to napi::Error.
2
-
3
- use cypherlite_core::CypherLiteError;
4
-
5
- /// Convert a CypherLiteError into a napi::Error suitable for throwing in JS.
6
- pub fn to_napi_error(e: CypherLiteError) -> napi::Error {
7
- napi::Error::from_reason(e.to_string())
8
- }
9
-
10
- /// Create an error for a poisoned mutex.
11
- pub fn mutex_poisoned() -> napi::Error {
12
- napi::Error::from_reason("internal error: mutex poisoned")
13
- }
14
-
15
- /// Create an error for a closed database.
16
- pub fn db_closed() -> napi::Error {
17
- napi::Error::from_reason("database is closed")
18
- }
package/src/lib.rs DELETED
@@ -1,44 +0,0 @@
1
- // CypherLite Node.js Bindings via napi-rs.
2
- //
3
- // This crate exposes the CypherLite embedded graph database as a native
4
- // Node.js addon built with @napi-rs/cli.
5
-
6
- #[macro_use]
7
- extern crate napi_derive;
8
-
9
- pub mod database;
10
- pub mod error;
11
- pub mod result;
12
- pub mod transaction;
13
- pub mod value;
14
-
15
- /// Return the library version string.
16
- #[napi]
17
- pub fn version() -> String {
18
- env!("CARGO_PKG_VERSION").to_string()
19
- }
20
-
21
- /// Return a comma-separated string of compiled feature flags.
22
- #[napi]
23
- pub fn features() -> String {
24
- let mut flags = Vec::new();
25
- if cfg!(feature = "temporal-core") {
26
- flags.push("temporal-core");
27
- }
28
- if cfg!(feature = "temporal-edge") {
29
- flags.push("temporal-edge");
30
- }
31
- if cfg!(feature = "subgraph") {
32
- flags.push("subgraph");
33
- }
34
- if cfg!(feature = "hypergraph") {
35
- flags.push("hypergraph");
36
- }
37
- if cfg!(feature = "full-temporal") {
38
- flags.push("full-temporal");
39
- }
40
- if cfg!(feature = "plugin") {
41
- flags.push("plugin");
42
- }
43
- flags.join(",")
44
- }
package/src/result.rs DELETED
@@ -1,87 +0,0 @@
1
- // Query result wrapper for Node.js.
2
-
3
- use cypherlite_query::api::QueryResult;
4
- use cypherlite_query::executor::Value;
5
- use napi::Env;
6
-
7
- use crate::value::rust_to_js;
8
-
9
- /// A query result containing columns and rows.
10
- ///
11
- /// Row data is stored as Rust types and converted to JS on access.
12
- #[napi]
13
- pub struct CylResult {
14
- columns: Vec<String>,
15
- /// Each row is a Vec of Values in column order.
16
- rows: Vec<Vec<Value>>,
17
- }
18
-
19
- impl CylResult {
20
- /// Create from a Rust QueryResult.
21
- pub fn from_query_result(qr: QueryResult) -> Self {
22
- let columns = qr.columns;
23
- let rows: Vec<Vec<Value>> = qr
24
- .rows
25
- .iter()
26
- .map(|row| {
27
- columns
28
- .iter()
29
- .map(|col| row.get(col).cloned().unwrap_or(Value::Null))
30
- .collect()
31
- })
32
- .collect();
33
- Self { columns, rows }
34
- }
35
- }
36
-
37
- #[napi]
38
- impl CylResult {
39
- /// Column names as an array of strings.
40
- #[napi(getter)]
41
- pub fn columns(&self) -> Vec<String> {
42
- self.columns.clone()
43
- }
44
-
45
- /// Number of rows in the result.
46
- #[napi(getter)]
47
- pub fn length(&self) -> u32 {
48
- self.rows.len() as u32
49
- }
50
-
51
- /// Get a single row by index as a plain JS object.
52
- #[napi]
53
- pub fn row(&self, env: Env, index: u32) -> napi::Result<napi::JsObject> {
54
- let idx = index as usize;
55
- if idx >= self.rows.len() {
56
- return Err(napi::Error::from_reason(format!(
57
- "row index out of range: {} >= {}",
58
- idx,
59
- self.rows.len()
60
- )));
61
- }
62
- self.row_to_object(&env, idx)
63
- }
64
-
65
- /// Convert all rows to an array of plain JS objects.
66
- #[napi(js_name = "toArray")]
67
- pub fn to_array(&self, env: Env) -> napi::Result<Vec<napi::JsObject>> {
68
- let mut result = Vec::with_capacity(self.rows.len());
69
- for i in 0..self.rows.len() {
70
- result.push(self.row_to_object(&env, i)?);
71
- }
72
- Ok(result)
73
- }
74
- }
75
-
76
- impl CylResult {
77
- /// Build a JS object for a single row.
78
- fn row_to_object(&self, env: &Env, row_idx: usize) -> napi::Result<napi::JsObject> {
79
- let mut obj = env.create_object()?;
80
- let row = &self.rows[row_idx];
81
- for (i, col) in self.columns.iter().enumerate() {
82
- let js_val = rust_to_js(env, &row[i])?;
83
- obj.set_named_property(col, js_val)?;
84
- }
85
- Ok(obj)
86
- }
87
- }
@@ -1,85 +0,0 @@
1
- // Transaction wrapper for Node.js.
2
-
3
- use std::sync::atomic::{AtomicBool, Ordering};
4
- use std::sync::{Arc, Mutex};
5
-
6
- use napi::Env;
7
-
8
- use cypherlite_query::api::CypherLite;
9
-
10
- use crate::error::{db_closed, mutex_poisoned, to_napi_error};
11
- use crate::result::CylResult;
12
- use crate::value::convert_params;
13
-
14
- /// A transaction wrapping CypherLite execute calls.
15
- ///
16
- /// Shares the database Mutex with the parent Database object.
17
- #[napi]
18
- pub struct Transaction {
19
- pub(crate) inner: Arc<Mutex<Option<CypherLite>>>,
20
- pub(crate) in_transaction: Arc<AtomicBool>,
21
- pub(crate) finished: bool,
22
- }
23
-
24
- impl Drop for Transaction {
25
- fn drop(&mut self) {
26
- // Auto-rollback: clear the transaction flag.
27
- self.finish();
28
- }
29
- }
30
-
31
- impl Transaction {
32
- /// Mark this transaction as finished and clear the in_transaction flag.
33
- fn finish(&mut self) {
34
- if !self.finished {
35
- self.finished = true;
36
- self.in_transaction.store(false, Ordering::SeqCst);
37
- }
38
- }
39
- }
40
-
41
- #[napi]
42
- impl Transaction {
43
- /// Execute a Cypher query within this transaction.
44
- #[napi]
45
- pub fn execute(
46
- &mut self,
47
- env: Env,
48
- query: String,
49
- params: Option<napi::JsObject>,
50
- ) -> napi::Result<CylResult> {
51
- if self.finished {
52
- return Err(napi::Error::from_reason("transaction is already finished"));
53
- }
54
- let rust_params = convert_params(&env, params)?;
55
- let mut guard = self.inner.lock().map_err(|_| mutex_poisoned())?;
56
- let db = guard.as_mut().ok_or_else(db_closed)?;
57
- let qr = if rust_params.is_empty() {
58
- db.execute(&query).map_err(to_napi_error)?
59
- } else {
60
- db.execute_with_params(&query, rust_params)
61
- .map_err(to_napi_error)?
62
- };
63
- Ok(CylResult::from_query_result(qr))
64
- }
65
-
66
- /// Commit the transaction.
67
- #[napi]
68
- pub fn commit(&mut self) -> napi::Result<()> {
69
- if self.finished {
70
- return Err(napi::Error::from_reason("transaction is already finished"));
71
- }
72
- self.finish();
73
- Ok(())
74
- }
75
-
76
- /// Rollback the transaction (Phase 2: no-op at storage level).
77
- #[napi]
78
- pub fn rollback(&mut self) -> napi::Result<()> {
79
- if self.finished {
80
- return Err(napi::Error::from_reason("transaction is already finished"));
81
- }
82
- self.finish();
83
- Ok(())
84
- }
85
- }
package/src/value.rs DELETED
@@ -1,136 +0,0 @@
1
- // Value conversion between Rust executor::Value and JavaScript values.
2
-
3
- use cypherlite_query::executor::Value;
4
- use napi::{Env, JsObject, JsUnknown, ValueType};
5
-
6
- /// Create a JS BigInt from a u64 entity ID.
7
- fn u64_to_bigint(env: &Env, id: u64) -> napi::Result<JsUnknown> {
8
- // Node IDs are unsigned; create_bigint_from_words takes (sign, words).
9
- let (sign, words) = if (id as i64) < 0 {
10
- (true, vec![id])
11
- } else {
12
- (false, vec![id])
13
- };
14
- env.create_bigint_from_words(sign, words)?.into_unknown()
15
- }
16
-
17
- /// Convert a Rust Value to a JavaScript value.
18
- pub fn rust_to_js(env: &Env, val: &Value) -> napi::Result<JsUnknown> {
19
- match val {
20
- Value::Null => Ok(env.get_null()?.into_unknown()),
21
- Value::Bool(b) => Ok(env.get_boolean(*b)?.into_unknown()),
22
- Value::Int64(i) => Ok(env.create_int64(*i)?.into_unknown()),
23
- Value::Float64(f) => Ok(env.create_double(*f)?.into_unknown()),
24
- Value::String(s) => Ok(env.create_string(s)?.into_unknown()),
25
- Value::Bytes(b) => {
26
- let buf = env.create_buffer_with_data(b.clone())?;
27
- Ok(buf.into_unknown())
28
- }
29
- Value::List(items) => {
30
- let mut arr = env.create_array_with_length(items.len())?;
31
- for (i, item) in items.iter().enumerate() {
32
- let js_val = rust_to_js(env, item)?;
33
- arr.set_element(i as u32, js_val)?;
34
- }
35
- Ok(arr.into_unknown())
36
- }
37
- Value::Node(id) => u64_to_bigint(env, id.0),
38
- Value::Edge(id) => u64_to_bigint(env, id.0),
39
- Value::DateTime(ms) => Ok(env.create_int64(*ms)?.into_unknown()),
40
- #[cfg(feature = "subgraph")]
41
- Value::Subgraph(id) => u64_to_bigint(env, id.0),
42
- #[cfg(feature = "hypergraph")]
43
- Value::Hyperedge(id) => u64_to_bigint(env, id.0),
44
- #[cfg(feature = "hypergraph")]
45
- Value::TemporalNode(id, ms) => {
46
- // Return as a plain object with nodeId and timestamp fields.
47
- let mut obj = env.create_object()?;
48
- let bigint = u64_to_bigint(env, id.0)?;
49
- obj.set_named_property("nodeId", bigint)?;
50
- obj.set_named_property("timestamp", env.create_int64(*ms)?)?;
51
- Ok(obj.into_unknown())
52
- }
53
- }
54
- }
55
-
56
- /// Convert a JavaScript value to a Rust Value.
57
- #[allow(clippy::only_used_in_recursion)]
58
- pub fn js_to_rust(env: &Env, val: JsUnknown) -> napi::Result<Value> {
59
- match val.get_type()? {
60
- ValueType::Null | ValueType::Undefined => Ok(Value::Null),
61
- ValueType::Boolean => {
62
- let b = val.coerce_to_bool()?.get_value()?;
63
- Ok(Value::Bool(b))
64
- }
65
- ValueType::Number => {
66
- let n = val.coerce_to_number()?;
67
- let f = n.get_double()?;
68
- // If the number is an integer (no fractional part and within i64 range),
69
- // store as Int64 for better compatibility with Cypher integer semantics.
70
- if f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 {
71
- Ok(Value::Int64(f as i64))
72
- } else {
73
- Ok(Value::Float64(f))
74
- }
75
- }
76
- ValueType::String => {
77
- let s = val.coerce_to_string()?.into_utf8()?;
78
- Ok(Value::String(s.as_str()?.to_string()))
79
- }
80
- ValueType::BigInt => {
81
- let mut bigint = unsafe { val.cast::<napi::JsBigInt>() };
82
- let (signed, value) = bigint.get_words()?;
83
- // Simple conversion: take the first word as u64, apply sign.
84
- let raw = value.first().copied().unwrap_or(0);
85
- let result = if signed { -(raw as i64) } else { raw as i64 };
86
- Ok(Value::Int64(result))
87
- }
88
- ValueType::Object => {
89
- // Check if it is an array.
90
- let obj: JsObject = unsafe { val.cast() };
91
- if obj.is_array()? {
92
- let len = obj.get_array_length()?;
93
- let mut items = Vec::with_capacity(len as usize);
94
- for i in 0..len {
95
- let elem: JsUnknown = obj.get_element(i)?;
96
- items.push(js_to_rust(env, elem)?);
97
- }
98
- return Ok(Value::List(items));
99
- }
100
- // Check if it is a Buffer.
101
- if obj.is_buffer()? {
102
- let unknown = obj.into_unknown();
103
- let buf = napi::JsBuffer::try_from(unknown)?;
104
- let data = buf.into_value()?;
105
- return Ok(Value::Bytes(data.to_vec()));
106
- }
107
- Err(napi::Error::from_reason(
108
- "cannot convert JS object to CypherLite value",
109
- ))
110
- }
111
- _ => Err(napi::Error::from_reason(format!(
112
- "cannot convert JS type {:?} to CypherLite value",
113
- val.get_type()?
114
- ))),
115
- }
116
- }
117
-
118
- /// Convert a JS object of params ({ key: value, ... }) to a HashMap.
119
- pub fn convert_params(
120
- env: &Env,
121
- params: Option<JsObject>,
122
- ) -> napi::Result<std::collections::HashMap<String, Value>> {
123
- let Some(obj) = params else {
124
- return Ok(std::collections::HashMap::new());
125
- };
126
- let keys = obj.get_property_names()?;
127
- let len = keys.get_array_length()?;
128
- let mut map = std::collections::HashMap::with_capacity(len as usize);
129
- for i in 0..len {
130
- let key: napi::JsString = keys.get_element(i)?;
131
- let key_str = key.into_utf8()?.as_str()?.to_string();
132
- let val: JsUnknown = obj.get_named_property(&key_str)?;
133
- map.insert(key_str, js_to_rust(env, val)?);
134
- }
135
- Ok(map)
136
- }
package/vitest.config.mjs DELETED
@@ -1,8 +0,0 @@
1
- import { defineConfig } from 'vitest/config';
2
-
3
- export default defineConfig({
4
- test: {
5
- include: ['__test__/**/*.spec.{js,mjs,ts}'],
6
- testTimeout: 30000,
7
- },
8
- });