Skip to content

Commit a79a284

Browse files
committed
wip
1 parent c09e8da commit a79a284

5 files changed

Lines changed: 253 additions & 17 deletions

File tree

build.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ EMCC_FLAGS=(
9292
"-s SAFE_HEAP=0"
9393

9494
# Output
95-
"-s SINGLE_FILE=0"
95+
"-s MODULARIZE=1"
96+
"-s EXPORT_NAME='initSqlcipher'"
9697

9798
# Optimization
9899
"--closure 0"

lib/sqlite-api.cjs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -240,14 +240,12 @@ class SQLiteAPI {
240240
* Initialize the SQLite API from a WASM module
241241
*/
242242
async function initSQLite(modulePath) {
243-
const wasmModule = require(modulePath);
244-
245-
return new Promise((resolve, reject) => {
243+
const loader = await import(modulePath);
244+
const wasmModule = await loader.default();
245+
return new Promise((resolve) => {
246246
const initialize = () => {
247-
// OpenSSL is initialized automatically by SQLCipher
248247
resolve(new SQLiteAPI(wasmModule));
249248
};
250-
251249
if (wasmModule.calledRun) {
252250
initialize();
253251
} else {

lib/sqlite-api.d.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
2+
declare module '@7mind.io/sqlcipher-wasm' {
3+
export class SQLiteDatabase {
4+
constructor(module: any, dbPtr: number);
5+
setKey(key: string): void;
6+
rekey(newKey: string): void;
7+
exec(sql: string): void;
8+
query(sql: string, params?: (string | number | null)[]): any[];
9+
bindParameters(stmt: number, params: (string | number | null)[]): void;
10+
getErrorMessage(): string;
11+
getChanges(): number;
12+
close(): void;
13+
}
14+
15+
export class SQLiteAPI {
16+
constructor(module: any);
17+
open(filename?: string, key?: string): SQLiteDatabase;
18+
}
19+
20+
export function init(): Promise<SQLiteAPI>;
21+
}

lib/sqlite-api.mjs

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
2+
import initSqlcipher from '../dist/sqlcipher.mjs';
3+
4+
const SQLITE_OK = 0;
5+
const SQLITE_ROW = 100;
6+
const SQLITE_DONE = 101;
7+
8+
export class SQLiteDatabase {
9+
constructor(module, dbPtr) {
10+
this.module = module;
11+
this.dbPtr = dbPtr;
12+
this.closed = false;
13+
}
14+
15+
setKey(key) {
16+
if (this.closed) throw new Error('Database is closed');
17+
try {
18+
this.exec(`PRAGMA key = '${key.replace(/'/g, "''")}'`);
19+
} catch (e) {
20+
throw new Error(`Failed to set encryption key: ${e.message}`);
21+
}
22+
}
23+
24+
rekey(newKey) {
25+
if (this.closed) throw new Error('Database is closed');
26+
this.exec(`PRAGMA rekey = '${newKey.replace(/'/g, "''")}'`);
27+
}
28+
29+
exec(sql) {
30+
if (this.closed) throw new Error('Database is closed');
31+
32+
const sqlPtr = this.module.allocateUTF8(sql);
33+
try {
34+
const errMsgPtr = this.module._malloc(4);
35+
36+
const result = this.module._sqlite3_exec(
37+
this.dbPtr,
38+
sqlPtr,
39+
0,
40+
0,
41+
errMsgPtr
42+
);
43+
44+
if (result !== SQLITE_OK) {
45+
const errPtr = this.module.getValue(errMsgPtr, 'i32');
46+
const errMsg = errPtr ? this.module.UTF8ToString(errPtr) : 'Unknown error';
47+
this.module._free(errMsgPtr);
48+
throw new Error(`SQLite error: ${errMsg}`);
49+
}
50+
51+
this.module._free(errMsgPtr);
52+
} finally {
53+
this.module._free(sqlPtr);
54+
}
55+
}
56+
57+
query(sql, params = []) {
58+
if (this.closed) throw new Error('Database is closed');
59+
60+
const sqlPtr = this.module.allocateUTF8(sql);
61+
const stmtPtr = this.module._malloc(4);
62+
63+
try {
64+
const result = this.module._sqlite3_prepare_v2(
65+
this.dbPtr,
66+
sqlPtr,
67+
-1,
68+
stmtPtr,
69+
0
70+
);
71+
72+
if (result !== SQLITE_OK) {
73+
const errMsg = this.getErrorMessage();
74+
throw new Error(`Failed to prepare statement: ${errMsg}`);
75+
}
76+
77+
const stmt = this.module.getValue(stmtPtr, 'i32');
78+
if (!stmt) {
79+
throw new Error('Failed to prepare statement: null statement');
80+
}
81+
82+
try {
83+
this.bindParameters(stmt, params);
84+
85+
const columnCount = this.module._sqlite3_column_count(stmt);
86+
const columns = [];
87+
for (let i = 0; i < columnCount; i++) {
88+
const namePtr = this.module._sqlite3_column_name(stmt, i);
89+
columns.push(this.module.UTF8ToString(namePtr));
90+
}
91+
92+
const rows = [];
93+
while (true) {
94+
const stepResult = this.module._sqlite3_step(stmt);
95+
96+
if (stepResult === SQLITE_DONE) {
97+
break;
98+
}
99+
100+
if (stepResult !== SQLITE_ROW) {
101+
const errMsg = this.getErrorMessage();
102+
throw new Error(`Step failed: ${errMsg}`);
103+
}
104+
105+
const row = {};
106+
for (let i = 0; i < columnCount; i++) {
107+
const valuePtr = this.module._sqlite3_column_text(stmt, i);
108+
row[columns[i]] = valuePtr ? this.module.UTF8ToString(valuePtr) : null;
109+
}
110+
rows.push(row);
111+
}
112+
113+
return rows;
114+
} finally {
115+
this.module._sqlite3_finalize(stmt);
116+
}
117+
} finally {
118+
this.module._free(stmtPtr);
119+
this.module._free(sqlPtr);
120+
}
121+
}
122+
123+
bindParameters(stmt, params) {
124+
for (let i = 0; i < params.length; i++) {
125+
const param = params[i];
126+
const index = i + 1;
127+
128+
if (param === null || param === undefined) {
129+
continue;
130+
} else if (typeof param === 'number') {
131+
if (Number.isInteger(param)) {
132+
this.module._sqlite3_bind_int(stmt, index, param);
133+
} else {
134+
this.module._sqlite3_bind_double(stmt, index, param);
135+
}
136+
} else if (typeof param === 'string') {
137+
const strPtr = this.module.allocateUTF8(param);
138+
this.module._sqlite3_bind_text(stmt, index, strPtr, -1, 0);
139+
} else {
140+
throw new Error(`Unsupported parameter type: ${typeof param}`);
141+
}
142+
}
143+
}
144+
145+
getErrorMessage() {
146+
const errPtr = this.module._sqlite3_errmsg(this.dbPtr);
147+
return errPtr ? this.module.UTF8ToString(errPtr) : 'Unknown error';
148+
}
149+
150+
getChanges() {
151+
return this.module._sqlite3_changes(this.dbPtr);
152+
}
153+
154+
close() {
155+
if (this.closed) return;
156+
157+
this.module._sqlite3_close(this.dbPtr);
158+
this.closed = true;
159+
}
160+
}
161+
162+
export class SQLiteAPI {
163+
constructor(module) {
164+
this.module = module;
165+
}
166+
167+
open(filename = ':memory:', key = null) {
168+
const filenamePtr = this.module.allocateUTF8(filename);
169+
const dbPtrPtr = this.module._malloc(4);
170+
171+
try {
172+
const result = this.module._sqlite3_open(filenamePtr, dbPtrPtr);
173+
174+
if (result !== SQLITE_OK) {
175+
throw new Error(`Failed to open database: ${result}`);
176+
}
177+
178+
const dbPtr = this.module.getValue(dbPtrPtr, 'i32');
179+
if (!dbPtr) {
180+
throw new Error('Failed to open database: null pointer');
181+
}
182+
183+
const db = new SQLiteDatabase(this.module, dbPtr);
184+
185+
if (key) {
186+
db.setKey(key);
187+
}
188+
189+
return db;
190+
} finally {
191+
this.module._free(dbPtrPtr);
192+
this.module._free(filenamePtr);
193+
}
194+
}
195+
}
196+
197+
export async function init() {
198+
const wasmModule = await initSqlcipher();
199+
return new SQLiteAPI(wasmModule);
200+
}

package.json

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,34 @@
11
{
22
"name": "@7mind.io/sqlcipher-wasm",
3-
"version": "1.0.7",
4-
"description": "SQLite/SQLCipher WebAssembly build with high-level JavaScript API, comprehensive tests, and cross-platform compatibility",
5-
"main": "dist/sqlcipher.js",
6-
"types": "lib/sqlite-api.d.ts",
3+
"version": "1.0.8",
4+
"description": "A production-ready WebAssembly build of SQLCipher with real OpenSSL-based encryption, a high-level isomorphic API, and comprehensive test coverage.",
5+
"type": "module",
6+
"main": "./lib/sqlite-api.cjs",
7+
"module": "./lib/sqlite-api.mjs",
8+
"types": "./lib/sqlite-api.d.ts",
9+
"exports": {
10+
".": {
11+
"import": "./lib/sqlite-api.mjs",
12+
"require": "./lib/sqlite-api.cjs"
13+
},
14+
"./dist/sqlcipher.mjs": "./dist/sqlcipher.mjs"
15+
},
716
"files": [
8-
"dist/",
9-
"lib/",
17+
"dist/sqlcipher.mjs",
18+
"lib/sqlite-api.mjs",
19+
"lib/sqlite-api.cjs",
20+
"lib/sqlite-api.d.ts",
1021
"README.md",
1122
"LICENSE"
1223
],
1324
"engines": {
14-
"node": ">=14.0.0"
25+
"node": ">=16.0.0"
1526
},
1627
"scripts": {
17-
"prepublishOnly": "./build.sh",
28+
"prepublishOnly": "npm run build",
1829
"build": "./build.sh",
1930
"test": "node test/run-all-tests.cjs",
20-
"test:watch": "nodemon --watch test --watch dist test/run-all-tests.cjs",
31+
"test:watch": "nodemon --watch test --watch lib --watch dist test/run-all-tests.cjs",
2132
"bench": "node bench/benchmark.cjs"
2233
},
2334
"keywords": [
@@ -28,11 +39,13 @@
2839
"database",
2940
"sql",
3041
"browser",
42+
"node",
43+
"isomorphic",
3144
"offline",
3245
"cross-platform",
3346
"emscripten"
3447
],
35-
"author": "Your Name <your.email@example.com>",
48+
"author": "7Mind",
3649
"license": "MIT",
3750
"repository": {
3851
"type": "git",
@@ -42,5 +55,8 @@
4255
"url": "https://github.com/7mind/sqlcipher-wasm/issues"
4356
},
4457
"homepage": "https://github.com/7mind/sqlcipher-wasm#readme",
45-
"devDependencies": {}
58+
"devDependencies": {
59+
"nodemon": "^3.0.0"
60+
}
4661
}
62+

0 commit comments

Comments
 (0)