@cqlite/node 0.3.1-rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +252 -0
- package/cqlite-node.darwin-arm64.node +0 -0
- package/index.d.ts +0 -0
- package/index.js +318 -0
- package/lib/error-wrapper.js +375 -0
- package/lib/index.d.ts +874 -0
- package/lib/index.js +102 -0
- package/package.json +68 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error wrapper module for CQLite Node.js bindings.
|
|
3
|
+
*
|
|
4
|
+
* This module provides utilities for parsing error metadata from
|
|
5
|
+
* native error messages and attaching them as properties.
|
|
6
|
+
*
|
|
7
|
+
* Issue #297: Error Mapping Implementation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse error metadata from a message string.
|
|
12
|
+
*
|
|
13
|
+
* The Rust layer encodes metadata in the message using null-byte separators:
|
|
14
|
+
* "Human-readable message\0code=IO\0category=System\0isRecoverable=true"
|
|
15
|
+
*
|
|
16
|
+
* @param {string} message - The error message from native code
|
|
17
|
+
* @returns {Object} Parsed metadata with code, category, isRecoverable, and message
|
|
18
|
+
*/
|
|
19
|
+
function parseErrorMetadata(message) {
|
|
20
|
+
if (!message || typeof message !== 'string') {
|
|
21
|
+
return {
|
|
22
|
+
code: 'INTERNAL',
|
|
23
|
+
category: 'Internal',
|
|
24
|
+
isRecoverable: false,
|
|
25
|
+
message: String(message || 'Unknown error'),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Split by null bytes
|
|
30
|
+
const parts = message.split('\0');
|
|
31
|
+
const humanMessage = parts[0];
|
|
32
|
+
|
|
33
|
+
// Default values
|
|
34
|
+
let code = 'INTERNAL';
|
|
35
|
+
let category = 'Internal';
|
|
36
|
+
let isRecoverable = false;
|
|
37
|
+
|
|
38
|
+
// Parse metadata from remaining parts
|
|
39
|
+
for (let i = 1; i < parts.length; i++) {
|
|
40
|
+
const part = parts[i];
|
|
41
|
+
if (part.startsWith('code=')) {
|
|
42
|
+
code = part.slice(5);
|
|
43
|
+
} else if (part.startsWith('category=')) {
|
|
44
|
+
category = part.slice(9);
|
|
45
|
+
} else if (part.startsWith('isRecoverable=')) {
|
|
46
|
+
isRecoverable = part.slice(14) === 'true';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
code,
|
|
52
|
+
category,
|
|
53
|
+
isRecoverable,
|
|
54
|
+
message: humanMessage,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Enhance an Error object with CQLite metadata properties.
|
|
60
|
+
*
|
|
61
|
+
* @param {Error} error - The error to enhance
|
|
62
|
+
* @returns {Error} The enhanced error with code, category, and isRecoverable properties
|
|
63
|
+
*/
|
|
64
|
+
function enhanceError(error) {
|
|
65
|
+
if (!error || typeof error.message !== 'string') {
|
|
66
|
+
return error;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const metadata = parseErrorMetadata(error.message);
|
|
70
|
+
|
|
71
|
+
// Update the message to the human-readable part only
|
|
72
|
+
error.message = metadata.message;
|
|
73
|
+
|
|
74
|
+
// Add properties
|
|
75
|
+
error.code = metadata.code;
|
|
76
|
+
error.category = metadata.category;
|
|
77
|
+
error.isRecoverable = metadata.isRecoverable;
|
|
78
|
+
|
|
79
|
+
return error;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Wrap an async function to enhance any thrown errors.
|
|
84
|
+
*
|
|
85
|
+
* @param {Function} fn - The async function to wrap
|
|
86
|
+
* @returns {Function} A wrapped function that enhances errors
|
|
87
|
+
*/
|
|
88
|
+
function wrapAsync(fn) {
|
|
89
|
+
return async function (...args) {
|
|
90
|
+
try {
|
|
91
|
+
return await fn.apply(this, args);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
throw enhanceError(error);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Create an async iterable wrapper around a native StreamingResult.
|
|
100
|
+
*
|
|
101
|
+
* Implements JavaScript's AsyncIterable protocol for use with `for await...of`.
|
|
102
|
+
* Provides automatic resource cleanup on loop completion or early termination.
|
|
103
|
+
*
|
|
104
|
+
* @param {Object} nativeStream - The native StreamingResult from Rust
|
|
105
|
+
* @returns {Object} An async iterable object with streaming functionality
|
|
106
|
+
*/
|
|
107
|
+
function createAsyncIterator(nativeStream) {
|
|
108
|
+
return {
|
|
109
|
+
/**
|
|
110
|
+
* Number of rows received so far.
|
|
111
|
+
* @returns {number}
|
|
112
|
+
*/
|
|
113
|
+
get rowsReceived() {
|
|
114
|
+
return nativeStream.rowsReceived;
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Column metadata for the result set.
|
|
119
|
+
* @returns {Array<Object>}
|
|
120
|
+
*/
|
|
121
|
+
get columns() {
|
|
122
|
+
return nativeStream.columns;
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Release resources early.
|
|
127
|
+
* Safe to call multiple times.
|
|
128
|
+
*/
|
|
129
|
+
close() {
|
|
130
|
+
return nativeStream.close();
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Implement Symbol.asyncIterator for `for await...of` support.
|
|
135
|
+
* @returns {AsyncIterator}
|
|
136
|
+
*/
|
|
137
|
+
[Symbol.asyncIterator]() {
|
|
138
|
+
return {
|
|
139
|
+
/**
|
|
140
|
+
* Get the next row from the stream.
|
|
141
|
+
* @returns {Promise<{value: Object|undefined, done: boolean}>}
|
|
142
|
+
*/
|
|
143
|
+
async next() {
|
|
144
|
+
try {
|
|
145
|
+
const result = await nativeStream.next();
|
|
146
|
+
return result;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
throw enhanceError(error);
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Handle early termination (e.g., break from loop).
|
|
154
|
+
* Called automatically by JavaScript runtime.
|
|
155
|
+
* @returns {Promise<{value: undefined, done: true}>}
|
|
156
|
+
*/
|
|
157
|
+
async return() {
|
|
158
|
+
nativeStream.close();
|
|
159
|
+
return { value: undefined, done: true };
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Create a wrapped Database class with enhanced error handling.
|
|
168
|
+
*
|
|
169
|
+
* @param {Function} NativeDatabase - The native Database class
|
|
170
|
+
* @param {Function} wrapPreparedStatement - Function to wrap PreparedStatement for type consistency
|
|
171
|
+
* @returns {Function} A wrapped Database class
|
|
172
|
+
*/
|
|
173
|
+
function createWrappedDatabase(NativeDatabase, wrapPreparedStatement) {
|
|
174
|
+
class Database {
|
|
175
|
+
constructor(native, preparedStatementWrapper) {
|
|
176
|
+
this._native = native;
|
|
177
|
+
this._wrapPreparedStatement = preparedStatementWrapper;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
static async open(dataDir, options) {
|
|
181
|
+
try {
|
|
182
|
+
const native = await NativeDatabase.open(dataDir, options);
|
|
183
|
+
return new Database(native, wrapPreparedStatement);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
throw enhanceError(error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async execute(query) {
|
|
190
|
+
try {
|
|
191
|
+
const result = await this._native.execute(query);
|
|
192
|
+
result.rowsAffected = result.rowCount; // M4 spec alias (Issue #348)
|
|
193
|
+
return result;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
throw enhanceError(error);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async executeNative(query) {
|
|
200
|
+
try {
|
|
201
|
+
const result = await this._native.executeNative(query);
|
|
202
|
+
result.rowsAffected = result.rowCount; // M4 spec alias (Issue #348)
|
|
203
|
+
return result;
|
|
204
|
+
} catch (error) {
|
|
205
|
+
throw enhanceError(error);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async getStats() {
|
|
210
|
+
try {
|
|
211
|
+
const stats = await this._native.getStats();
|
|
212
|
+
// Coerce to BigInt to ensure TypeScript type guarantees hold (Issue #351)
|
|
213
|
+
// napi-rs returns i64 as number for small values, but TS declares bigint
|
|
214
|
+
return {
|
|
215
|
+
totalSstables: stats.totalSstables,
|
|
216
|
+
totalRows: BigInt(stats.totalRows),
|
|
217
|
+
memoryUsedBytes: BigInt(stats.memoryUsedBytes),
|
|
218
|
+
};
|
|
219
|
+
} catch (error) {
|
|
220
|
+
throw enhanceError(error);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async close() {
|
|
225
|
+
try {
|
|
226
|
+
return await this._native.close();
|
|
227
|
+
} catch (error) {
|
|
228
|
+
throw enhanceError(error);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
get isClosed() {
|
|
233
|
+
return this._native.isClosed;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Execute a CQL query with streaming results.
|
|
238
|
+
*
|
|
239
|
+
* Returns an async iterable that yields rows one at a time for
|
|
240
|
+
* memory-efficient processing of large result sets.
|
|
241
|
+
*
|
|
242
|
+
* @param {string} query - CQL SELECT statement to execute
|
|
243
|
+
* @param {Object} [config] - Optional streaming configuration
|
|
244
|
+
* @param {number} [config.bufferSize=1024] - Rows to buffer in memory
|
|
245
|
+
* @param {number} [config.chunkSize=10000] - Rows per fetch chunk
|
|
246
|
+
* @returns {AsyncIterable<Object>} Async iterable of rows
|
|
247
|
+
* @throws {CqliteError} If the query fails (on first iteration)
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* for await (const row of db.executeStreaming('SELECT * FROM large_table')) {
|
|
251
|
+
* console.log(row.name);
|
|
252
|
+
* }
|
|
253
|
+
*/
|
|
254
|
+
executeStreaming(query, config) {
|
|
255
|
+
const self = this;
|
|
256
|
+
let nativeStreamPromise = null;
|
|
257
|
+
let nativeStream = null;
|
|
258
|
+
let initError = null;
|
|
259
|
+
let closed = false;
|
|
260
|
+
|
|
261
|
+
// Lazy initialization - called on first iteration
|
|
262
|
+
const ensureInitialized = async () => {
|
|
263
|
+
// Check if stream was closed before initialization
|
|
264
|
+
if (closed) {
|
|
265
|
+
return { next: async () => ({ value: undefined, done: true }) };
|
|
266
|
+
}
|
|
267
|
+
if (initError) throw initError;
|
|
268
|
+
if (nativeStream) return nativeStream;
|
|
269
|
+
if (!nativeStreamPromise) {
|
|
270
|
+
nativeStreamPromise = self._native.executeStreaming(query, config)
|
|
271
|
+
.then(stream => {
|
|
272
|
+
// Check if close() was called while we were initializing
|
|
273
|
+
if (closed) {
|
|
274
|
+
stream.close();
|
|
275
|
+
return { next: async () => ({ value: undefined, done: true }) };
|
|
276
|
+
}
|
|
277
|
+
nativeStream = stream;
|
|
278
|
+
return stream;
|
|
279
|
+
})
|
|
280
|
+
.catch(err => {
|
|
281
|
+
initError = enhanceError(err);
|
|
282
|
+
throw initError;
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
return nativeStreamPromise;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
/**
|
|
290
|
+
* Number of rows received so far.
|
|
291
|
+
* Returns 0 before iteration begins.
|
|
292
|
+
* @returns {number}
|
|
293
|
+
*/
|
|
294
|
+
get rowsReceived() {
|
|
295
|
+
return nativeStream?.rowsReceived ?? 0;
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Column metadata for the result set.
|
|
300
|
+
* Returns empty array before iteration begins.
|
|
301
|
+
* @returns {Array<Object>}
|
|
302
|
+
*/
|
|
303
|
+
get columns() {
|
|
304
|
+
return nativeStream?.columns ?? [];
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Release resources early.
|
|
309
|
+
* Safe to call multiple times.
|
|
310
|
+
* If called before initialization completes, prevents initialization
|
|
311
|
+
* from creating a zombie stream.
|
|
312
|
+
*/
|
|
313
|
+
close() {
|
|
314
|
+
closed = true;
|
|
315
|
+
// Clear the promise to prevent initialization from completing
|
|
316
|
+
// after close() is called (prevents zombie streams)
|
|
317
|
+
nativeStreamPromise = null;
|
|
318
|
+
nativeStream?.close();
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Implement Symbol.asyncIterator for `for await...of` support.
|
|
323
|
+
* @returns {AsyncIterator}
|
|
324
|
+
*/
|
|
325
|
+
[Symbol.asyncIterator]() {
|
|
326
|
+
return {
|
|
327
|
+
async next() {
|
|
328
|
+
try {
|
|
329
|
+
const stream = await ensureInitialized();
|
|
330
|
+
return await stream.next();
|
|
331
|
+
} catch (error) {
|
|
332
|
+
throw enhanceError(error);
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
async return() {
|
|
336
|
+
closed = true;
|
|
337
|
+
nativeStreamPromise = null;
|
|
338
|
+
nativeStream?.close();
|
|
339
|
+
return { value: undefined, done: true };
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Prepare a CQL query for analysis.
|
|
348
|
+
*
|
|
349
|
+
* Returns a PreparedStatement that can be inspected for query plan
|
|
350
|
+
* information and statistics.
|
|
351
|
+
*
|
|
352
|
+
* @param {string} query - CQL SELECT statement to prepare
|
|
353
|
+
* @returns {Promise<PreparedStatement>} PreparedStatement with query plan info
|
|
354
|
+
* @throws {CqliteError} If the query cannot be prepared
|
|
355
|
+
*/
|
|
356
|
+
async prepare(query) {
|
|
357
|
+
try {
|
|
358
|
+
const nativeStmt = await this._native.prepare(query);
|
|
359
|
+
// Wrap to ensure type consistency (Issue #351)
|
|
360
|
+
return this._wrapPreparedStatement(nativeStmt);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
throw enhanceError(error);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return Database;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
module.exports = {
|
|
371
|
+
parseErrorMetadata,
|
|
372
|
+
enhanceError,
|
|
373
|
+
wrapAsync,
|
|
374
|
+
createWrappedDatabase,
|
|
375
|
+
};
|