diff --git a/README.md b/README.md index 0a427f75..814c1896 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ https://discord.gg/W9XmqCQCKP Some of the big supported features: +- iOS, Android, macOS and web support - Vanilla sqlite - Libsql is supported as a compilation target - SQLCipher is supported as a compilation target diff --git a/docs/docs/api.md b/docs/docs/api.md index 4882d6e3..ee1c99d3 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -16,6 +16,20 @@ export const db = open({ }); ``` +### Open Async (Web) + +On web, opening is async-only and requires OPFS. + +```tsx +import { openAsync } from '@op-engineering/op-sqlite'; + +export const db = await openAsync({ + name: 'myDb.sqlite', +}); +``` + +On web, `open()` intentionally throws. Use `openAsync()`. + ### SQLCipher Open If you are using SQLCipher all the methods are the same with the exception of the open method which needs an extra `encryptionKey` to encrypt/decrypt the database. @@ -51,6 +65,12 @@ try { } ``` +### Web note + +On web, `execute()` runs the full SQL string passed to it. +On native, `execute()` currently runs only the first prepared statement. +If you need identical behavior across platforms, avoid multi-statement SQL strings. + ### Execute with Host Objects It’s possible to return HostObjects when using a query. The benefit is that HostObjects are only created in C++ and only when you try to access a value inside of them a C++ value β†’ JS value conversion happens. This means creation is fast, property access is slow. The use case is clear if you are returning **massive** amount of objects but only displaying/accessing a few of them at the time. @@ -109,6 +129,8 @@ You can do sync queries via the `executeSync` functions. Not available in transa let res = db.executeSync('SELECT 1'); ``` +On web, sync APIs intentionally throw. Use async methods only. + ## Transactions Wraps the code inside in a transaction. Any error thrown inside of the transaction body function will ROLLBACK the transaction. diff --git a/docs/docs/installation.md b/docs/docs/installation.md index 99482299..e3d74b4b 100644 --- a/docs/docs/installation.md +++ b/docs/docs/installation.md @@ -17,7 +17,32 @@ npx expo install @op-engineering/op-sqlite npx expo prebuild --clean ``` -This package only runs on `iOS`, `Android` and `macOS`. +This package runs on `iOS`, `Android`, `macOS` and `web`. + +## Web requirements + +Web support is async-only and uses the sqlite wasm worker API with OPFS persistence. + +`@sqlite.org/sqlite-wasm` is an optional dependency. You only need to install it if you use op-sqlite on web. + +```bash +yarn add @sqlite.org/sqlite-wasm +``` + +If your app does not target web, you do not need to install this package. + +Required runtime behavior on web: + +- Use `openAsync()` to open the database. +- Use async DB methods such as `execute()` and `closeAsync()`. +- Synchronous APIs (for example `open()` and `executeSync()`) intentionally throw on web. +- SQLCipher and libsql are not supported on web. As well as loading extensions. +- OPFS is required for the web backend. If OPFS is unavailable, `openAsync()` throws. + +Required response headers (for worker/OPFS setup): + +- `Cross-Origin-Opener-Policy: same-origin` +- `Cross-Origin-Embedder-Policy: require-corp` # Configuration diff --git a/example/.gitignore b/example/.gitignore index f704db64..87b5eea0 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -52,6 +52,7 @@ yarn-error.log # Bundle artifact *.jsbundle +web-build/ # Ruby / CocoaPods /ios/Pods/ diff --git a/example/index.web.tsx b/example/index.web.tsx new file mode 100644 index 00000000..7ec2d5f6 --- /dev/null +++ b/example/index.web.tsx @@ -0,0 +1,15 @@ +import { AppRegistry } from "react-native"; +import { name as appName } from "./app.json"; +import App from "./src/AppWeb"; + +AppRegistry.registerComponent(appName, () => App); + +const rootTag = (globalThis as any).document?.getElementById("root"); +if (!rootTag) { + throw new Error("Root element not found"); +} + +AppRegistry.runApplication(appName, { + rootTag, + initialProps: {}, +}); diff --git a/example/package.json b/example/package.json index 3e48179d..a8cdae4e 100644 --- a/example/package.json +++ b/example/package.json @@ -13,16 +13,22 @@ "pods:nuke": "cd ios && rm -rf Pods && rm -rf Podfile.lock && bundle exec pod install", "run:android:release": "cd android && ./gradlew assembleRelease && adb install -r app/build/outputs/apk/release/app-release.apk && adb shell am start -n com.op.sqlite.example/.MainActivity", "build:android": "cd android && ./gradlew assembleDebug --no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a", - "build:ios": "cd ios && xcodebuild -workspace OPSQLiteExample.xcworkspace -scheme debug -configuration Debug -sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO" + "build:ios": "cd ios && xcodebuild -workspace OPSQLiteExample.xcworkspace -scheme debug -configuration Debug -sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO", + "web": "vite", + "web:build": "vite build", + "web:preview": "vite preview" }, "dependencies": { "@op-engineering/op-test": "^0.2.5", + "@sqlite.org/sqlite-wasm": "^3.51.2-build8", "chance": "^1.1.9", "clsx": "^2.0.0", "events": "^3.3.0", "react": "19.1.1", + "react-dom": "19.1.1", "react-native": "0.82.1", - "react-native-safe-area-context": "^5.6.2" + "react-native-safe-area-context": "^5.6.2", + "react-native-web": "^0.21.2" }, "devDependencies": { "@babel/core": "^7.25.2", @@ -36,11 +42,13 @@ "@react-native/typescript-config": "0.81.5", "@types/chance": "^1.1.7", "@types/react": "^19.1.1", + "@vitejs/plugin-react": "^5.1.0", "patch-package": "^8.0.1", "react-native-builder-bob": "^0.40.13", "react-native-monorepo-config": "^0.1.9", "react-native-restart": "^0.0.27", - "tailwindcss": "3.3.2" + "tailwindcss": "3.3.2", + "vite": "^7.1.9" }, "engines": { "node": ">=18" diff --git a/example/src/App.tsx b/example/src/App.tsx index 61eb4579..f542f0ab 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,108 +1,108 @@ -import {useEffect, useState} from 'react'; import { - displayResults, - runTests, - allTestsPassed, -} from '@op-engineering/op-test'; -import './tests'; // import all tests to register them -import {SafeAreaProvider, SafeAreaView} from 'react-native-safe-area-context'; + allTestsPassed, + displayResults, + runTests, +} from "@op-engineering/op-test"; +import { useEffect, useState } from "react"; +import "./tests"; // import all tests to register them // import {performanceTest} from './performance_test'; -import {StyleSheet, Text, View} from 'react-native'; +import { StyleSheet, Text, View } from "react-native"; +import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context"; // import {open} from '@op-engineering/op-sqlite'; export default function App() { - const [results, setResults] = useState(null); - const [perfResult, setPerfResult] = useState(0); - const [openTime, setOpenTime] = useState(0); + const [results, setResults] = useState(null); + const [perfResult, setPerfResult] = useState(0); + const [openTime, setOpenTime] = useState(0); - useEffect(() => { - console.log("App has started 🟒") - const work = async () => { - // let start = performance.now(); - // open({ - // name: 'dummyDb.sqlite', - // }); - // setOpenTime(performance.now() - start); + useEffect(() => { + console.log("App has started 🟒"); + const work = async () => { + // let start = performance.now(); + // open({ + // name: 'dummyDb.sqlite', + // }); + // setOpenTime(performance.now() - start); - try { - console.log("TESTS STARTED 🟠"); - const results = await runTests(); - const passed = allTestsPassed(results); - console.log("TESTS FINISHED 🟒") - console.log(`OPSQLITE_TEST_RESULT:${passed ? 'PASS' : 'FAIL'}`); - setResults(results); - } catch (e) { - console.log(`TEST FAILED πŸŸ₯ ${e}`) - console.log('OPSQLITE_TEST_RESULT:FAIL'); - } + try { + console.log("TESTS STARTED 🟠"); + const results = await runTests(); + const passed = allTestsPassed(results); + console.log("TESTS FINISHED 🟒"); + console.log(`OPSQLITE_TEST_RESULT:${passed ? "PASS" : "FAIL"}`); + setResults(results); + } catch (e) { + console.log(`TEST FAILED πŸŸ₯ ${e}`); + console.log("OPSQLITE_TEST_RESULT:FAIL"); + } - // setTimeout(() => { - // try { - // global?.gc?.(); - // let perfRes = performanceTest(); - // setPerfResult(perfRes); - // } catch (e) { - // // intentionally left blank - // } - // }, 1000); - }; + // setTimeout(() => { + // try { + // global?.gc?.(); + // let perfRes = performanceTest(); + // setPerfResult(perfRes); + // } catch (e) { + // // intentionally left blank + // } + // }, 1000); + }; - work(); + work(); - return () => {}; - }, []); + return () => {}; + }, []); - // const shareDb = async () => { - // try { - // const db = open({ - // name: 'shareableDb.sqlite', - // }); - // await db.execute( - // 'CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, name TEXT)', - // ); - // await db.execute("INSERT INTO test (name) VALUES ('test')"); - // const res = await db.execute('SELECT * FROM test'); - // console.log(res); - // await db.close(); - // await Share.open({ - // url: 'file://' + db.getDbPath(), - // failOnCancel: false, - // type: 'application/x-sqlite3', - // }); - // } catch (e) { - // console.log(e); - // } - // }; + // const shareDb = async () => { + // try { + // const db = open({ + // name: 'shareableDb.sqlite', + // }); + // await db.execute( + // 'CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, name TEXT)', + // ); + // await db.execute("INSERT INTO test (name) VALUES ('test')"); + // const res = await db.execute('SELECT * FROM test'); + // console.log(res); + // await db.close(); + // await Share.open({ + // url: 'file://' + db.getDbPath(), + // failOnCancel: false, + // type: 'application/x-sqlite3', + // }); + // } catch (e) { + // console.log(e); + // } + // }; - return ( - - - - - Open DB time: {openTime.toFixed(0)} ms - - - 100_000 query time: {perfResult.toFixed(0)} ms - - - {displayResults(results)} - - - ); + return ( + + + + + Open DB time: {openTime.toFixed(0)} ms + + + 100_000 query time: {perfResult.toFixed(0)} ms + + + {displayResults(results)} + + + ); } const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#222', - gap: 4, - padding: 10, - }, - results: { - flex: 1, - }, - performanceText: { - color: 'white', - fontSize: 16, - }, + container: { + flex: 1, + backgroundColor: "#222", + gap: 4, + padding: 10, + }, + results: { + flex: 1, + }, + performanceText: { + color: "white", + fontSize: 16, + }, }); diff --git a/example/src/AppWeb.tsx b/example/src/AppWeb.tsx new file mode 100644 index 00000000..292111a1 --- /dev/null +++ b/example/src/AppWeb.tsx @@ -0,0 +1,406 @@ +import { openAsync } from "@op-engineering/op-sqlite"; +import { useCallback, useEffect, useState } from "react"; +import { + Pressable, + ScrollView, + StyleSheet, + Text, + TextInput, + View, +} from "react-native"; + +type TableColumn = { + cid: number; + name: string; + type: string; + notnull: number; + pk: number; +}; + +type Note = { + id: number; + label: string; + created_at: string; +}; + +export default function AppWeb() { + const [status, setStatus] = useState("Initializing sqlite web backend..."); + const [columns, setColumns] = useState([]); + const [notes, setNotes] = useState([]); + const [inputValue, setInputValue] = useState("First note from OPFS web db"); + const [loading, setLoading] = useState(false); + + const withDb = useCallback( + async ( + work: (db: Awaited>) => Promise, + ) => { + const db = await openAsync({ + name: "example-web.sqlite", + }); + + try { + return await work(db); + } finally { + await db.closeAsync(); + } + }, + [], + ); + + const refreshTableInfo = useCallback(async () => { + return withDb(async (db) => { + const [schemaResult, rowsResult] = await Promise.all([ + db.execute("PRAGMA table_info(web_notes)"), + db.execute( + "SELECT id, label, created_at FROM web_notes ORDER BY id DESC LIMIT 20", + ), + ]); + + setColumns( + schemaResult.rows.map((row) => ({ + cid: Number(row.cid), + name: String(row.name), + type: String(row.type), + notnull: Number(row.notnull), + pk: Number(row.pk), + })), + ); + + setNotes( + rowsResult.rows.map((row) => ({ + id: Number(row.id), + label: String(row.label), + created_at: String(row.created_at), + })), + ); + }); + }, [withDb]); + + const ensureSchema = useCallback(async () => { + return withDb(async (db) => { + await db.execute( + "CREATE TABLE IF NOT EXISTS web_notes (id INTEGER PRIMARY KEY AUTOINCREMENT, label TEXT NOT NULL, created_at TEXT NOT NULL)", + ); + }); + }, [withDb]); + + const insertNote = useCallback(async () => { + const value = inputValue.trim(); + if (!value) { + setStatus("Type a value before inserting."); + return; + } + + setLoading(true); + try { + await withDb(async (db) => { + await db.execute( + "INSERT INTO web_notes (label, created_at) VALUES (?, ?)", + [value, new Date().toISOString()], + ); + }); + + setStatus("Insert succeeded. Data is persisted in OPFS."); + await refreshTableInfo(); + setInputValue(""); + } catch (error) { + setStatus(`Insert failed: ${(error as Error).message}`); + } finally { + setLoading(false); + } + }, [inputValue, refreshTableInfo, withDb]); + + const insertSampleData = useCallback(async () => { + setLoading(true); + try { + await withDb(async (db) => { + const now = new Date().toISOString(); + + await db.execute( + "INSERT INTO web_notes (label, created_at) VALUES (?, ?)", + ["Sample: OPFS persistence check", now], + ); + await db.execute( + "INSERT INTO web_notes (label, created_at) VALUES (?, ?)", + ["Sample: Query path works", now], + ); + await db.execute( + "INSERT INTO web_notes (label, created_at) VALUES (?, ?)", + ["Sample: closeAsync completed", now], + ); + }); + + setStatus("Sample rows inserted successfully."); + await refreshTableInfo(); + } catch (error) { + setStatus(`Sample insert failed: ${(error as Error).message}`); + } finally { + setLoading(false); + } + }, [refreshTableInfo, withDb]); + + const clearRows = useCallback(async () => { + setLoading(true); + try { + await withDb(async (db) => { + await db.execute("DELETE FROM web_notes"); + }); + + setStatus("Table cleared."); + await refreshTableInfo(); + } catch (error) { + setStatus(`Clear failed: ${(error as Error).message}`); + } finally { + setLoading(false); + } + }, [refreshTableInfo, withDb]); + + useEffect(() => { + const work = async () => { + try { + await ensureSchema(); + await refreshTableInfo(); + setStatus("SQLite web backend initialized with OPFS persistence."); + } catch (error) { + setStatus( + `Failed to initialize web sqlite backend: ${(error as Error).message}`, + ); + } + }; + + work(); + }, [ensureSchema, refreshTableInfo]); + + return ( + + OP-SQLite Web Example + {status} + + + 1) Insert data + + + + Insert one row + + + Insert sample rows + + + + + + Table: web_notes + + + Refresh table + + + Clear table + + + + Schema + + cid + name + type + nn + pk + + {columns.map((column) => ( + + {column.cid} + {column.name} + {column.type} + + {column.notnull} + + {column.pk} + + ))} + + Rows ({notes.length}) + + id + label + created_at + + {notes.length === 0 ? ( + No rows in web_notes. + ) : ( + notes.map((entry) => ( + + {entry.id} + + {entry.label} + + + {entry.created_at} + + + )) + )} + + + ); +} + +const styles = StyleSheet.create({ + container: { + minHeight: "100%", + padding: 24, + backgroundColor: "#101418", + gap: 12, + }, + title: { + color: "#f9fafb", + fontWeight: "700", + fontSize: 24, + marginBottom: 2, + }, + status: { + color: "#9ca3af", + marginBottom: 8, + }, + panel: { + borderWidth: 1, + borderColor: "#273043", + borderRadius: 12, + backgroundColor: "#151a22", + padding: 12, + gap: 8, + }, + panelTitle: { + color: "#f3f4f6", + fontWeight: "600", + fontSize: 16, + }, + sectionLabel: { + color: "#cbd5e1", + marginTop: 4, + marginBottom: 2, + fontWeight: "600", + }, + row: { + flexDirection: "row", + flexWrap: "wrap", + gap: 8, + }, + input: { + borderWidth: 1, + borderColor: "#334155", + borderRadius: 10, + color: "#f8fafc", + paddingHorizontal: 10, + paddingVertical: 9, + backgroundColor: "#0f172a", + }, + button: { + backgroundColor: "#2563eb", + borderRadius: 8, + paddingHorizontal: 12, + paddingVertical: 9, + }, + buttonSecondary: { + backgroundColor: "#0ea5e9", + borderRadius: 8, + paddingHorizontal: 12, + paddingVertical: 9, + }, + buttonDanger: { + backgroundColor: "#dc2626", + borderRadius: 8, + paddingHorizontal: 12, + paddingVertical: 9, + }, + buttonText: { + color: "#f8fafc", + fontWeight: "600", + }, + empty: { + color: "#9ca3af", + fontStyle: "italic", + }, + tableHeader: { + flexDirection: "row", + borderBottomWidth: 1, + borderBottomColor: "#334155", + paddingBottom: 6, + marginTop: 4, + }, + tableRow: { + flexDirection: "row", + borderBottomWidth: 1, + borderBottomColor: "#1f2937", + paddingVertical: 8, + }, + headerCell: { + color: "#93c5fd", + fontWeight: "700", + fontSize: 12, + }, + rowCell: { + color: "#e5e7eb", + fontSize: 13, + }, + cidCell: { + width: 40, + }, + nameCell: { + flex: 1, + paddingRight: 8, + }, + typeCell: { + width: 80, + }, + flagCell: { + width: 32, + }, + dateCell: { + flex: 1, + paddingLeft: 8, + }, + item: { + borderWidth: 1, + borderColor: "#334155", + borderRadius: 10, + padding: 9, + gap: 2, + backgroundColor: "#0f172a", + }, + itemTitle: { + color: "#93c5fd", + fontWeight: "700", + }, + itemText: { + color: "#f1f5f9", + }, + itemMeta: { + color: "#94a3b8", + fontSize: 14, + }, +}); diff --git a/example/src/tests/blob.ts b/example/src/tests/blob.ts index b1d8ca30..e3c018bc 100644 --- a/example/src/tests/blob.ts +++ b/example/src/tests/blob.ts @@ -1,112 +1,112 @@ -import {type DB, open} from '@op-engineering/op-sqlite'; +import { type DB, open } from "@op-engineering/op-sqlite"; import { - afterAll, - beforeEach, - describe, - it, - expect, -} from '@op-engineering/op-test'; + afterAll, + beforeEach, + describe, + expect, + it, +} from "@op-engineering/op-test"; let db: DB; -describe('Blobs', () => { - beforeEach(async () => { - try { - db = open({ - name: 'blobs', - encryptionKey: 'test', - }); +describe("Blobs", () => { + beforeEach(async () => { + try { + db = open({ + name: "blobs", + encryptionKey: "test", + }); - await db.execute('DROP TABLE IF EXISTS BlobTable;'); - await db.execute( - 'CREATE TABLE BlobTable ( id INT PRIMARY KEY, content BLOB) STRICT;', - ); - } catch (e) { - console.warn('error on before each', e); - } - }); + await db.execute("DROP TABLE IF EXISTS BlobTable;"); + await db.execute( + "CREATE TABLE BlobTable ( id INT PRIMARY KEY, content BLOB) STRICT;", + ); + } catch (e) { + console.warn("error on before each", e); + } + }); - afterAll(() => { - db.delete(); - }); + afterAll(() => { + db.delete(); + }); - it('ArrayBuffer', async () => { - const uint8 = new Uint8Array(2); - uint8[0] = 42; + it("ArrayBuffer", async () => { + const uint8 = new Uint8Array(2); + uint8[0] = 42; - await db.execute(`INSERT OR REPLACE INTO BlobTable VALUES (?, ?);`, [ - 1, - uint8.buffer, - ]); + await db.execute(`INSERT OR REPLACE INTO BlobTable VALUES (?, ?);`, [ + 1, + uint8.buffer, + ]); - const result = await db.execute('SELECT content FROM BlobTable;'); + const result = await db.execute("SELECT content FROM BlobTable;"); - const finalUint8 = new Uint8Array(result.rows[0]!.content as any); - expect(finalUint8[0]).toBe(42); - }); + const finalUint8 = new Uint8Array(result.rows[0]!.content as any); + expect(finalUint8[0]).toBe(42); + }); - it('Uint8Array', async () => { - const uint8 = new Uint8Array(2); - uint8[0] = 42; + it("Uint8Array", async () => { + const uint8 = new Uint8Array(2); + uint8[0] = 42; - await db.execute(`INSERT OR REPLACE INTO BlobTable VALUES (?, ?);`, [ - 1, - uint8, - ]); + await db.execute(`INSERT OR REPLACE INTO BlobTable VALUES (?, ?);`, [ + 1, + uint8, + ]); - const result = await db.execute('SELECT content FROM BlobTable'); + const result = await db.execute("SELECT content FROM BlobTable"); - const finalUint8 = new Uint8Array(result.rows[0]!.content as any); - expect(finalUint8[0]).toBe(42); - }); + const finalUint8 = new Uint8Array(result.rows[0]!.content as any); + expect(finalUint8[0]).toBe(42); + }); - it('Uint16Array', async () => { - const uint8 = new Uint16Array(2); - uint8[0] = 42; + it("Uint16Array", async () => { + const uint8 = new Uint16Array(2); + uint8[0] = 42; - await db.execute(`INSERT OR REPLACE INTO BlobTable VALUES (?, ?);`, [ - 1, - uint8, - ]); + await db.execute(`INSERT OR REPLACE INTO BlobTable VALUES (?, ?);`, [ + 1, + uint8, + ]); - const result = await db.execute('SELECT content FROM BlobTable'); + const result = await db.execute("SELECT content FROM BlobTable"); - const finalUint8 = new Uint8Array(result.rows[0]!.content as any); - expect(finalUint8[0]).toBe(42); - }); + const finalUint8 = new Uint8Array(result.rows[0]!.content as any); + expect(finalUint8[0]).toBe(42); + }); - it('Uint8Array in prepared statement', async () => { - const uint8 = new Uint8Array(2); - uint8[0] = 46; + it("Uint8Array in prepared statement", async () => { + const uint8 = new Uint8Array(2); + uint8[0] = 46; - const statement = db.prepareStatement( - 'INSERT OR REPLACE INTO BlobTable VALUES (?, ?);', - ); - await statement.bind([1, uint8]); + const statement = db.prepareStatement( + "INSERT OR REPLACE INTO BlobTable VALUES (?, ?);", + ); + await statement.bind([1, uint8]); - await statement.execute(); + await statement.execute(); - const result = await db.execute('SELECT content FROM BlobTable'); + const result = await db.execute("SELECT content FROM BlobTable"); - const finalUint8 = new Uint8Array(result.rows[0]!.content as any); - expect(finalUint8[0]).toBe(46); - }); + const finalUint8 = new Uint8Array(result.rows[0]!.content as any); + expect(finalUint8[0]).toBe(46); + }); - it('Buffer in prepared statement', async () => { - const uint8 = new Uint8Array(2); - uint8[0] = 52; + it("Buffer in prepared statement", async () => { + const uint8 = new Uint8Array(2); + uint8[0] = 52; - const statement = db.prepareStatement( - 'INSERT OR REPLACE INTO BlobTable VALUES (?, ?);', - ); + const statement = db.prepareStatement( + "INSERT OR REPLACE INTO BlobTable VALUES (?, ?);", + ); - await statement.bind([1, uint8.buffer]); + await statement.bind([1, uint8.buffer]); - await statement.execute(); + await statement.execute(); - const result = await db.execute('SELECT content FROM BlobTable'); + const result = await db.execute("SELECT content FROM BlobTable"); - const finalUint8 = new Uint8Array(result.rows[0]!.content as any); - expect(finalUint8[0]).toBe(52); - }); + const finalUint8 = new Uint8Array(result.rows[0]!.content as any); + expect(finalUint8[0]).toBe(52); + }); }); diff --git a/example/src/tests/constants.ts b/example/src/tests/constants.ts index 3ebdf9bc..45376dae 100644 --- a/example/src/tests/constants.ts +++ b/example/src/tests/constants.ts @@ -1,22 +1,22 @@ import { - IOS_DOCUMENT_PATH, - IOS_LIBRARY_PATH, - ANDROID_EXTERNAL_FILES_PATH, - ANDROID_DATABASE_PATH, - ANDROID_FILES_PATH, -} from '@op-engineering/op-sqlite'; -import {describe, it, expect} from '@op-engineering/op-test'; -import {Platform} from 'react-native'; + ANDROID_DATABASE_PATH, + ANDROID_EXTERNAL_FILES_PATH, + ANDROID_FILES_PATH, + IOS_DOCUMENT_PATH, + IOS_LIBRARY_PATH, +} from "@op-engineering/op-sqlite"; +import { describe, expect, it } from "@op-engineering/op-test"; +import { Platform } from "react-native"; -describe('Constants tests', () => { - it('Constants are there', async () => { - if (Platform.OS === 'ios') { - expect(!!IOS_DOCUMENT_PATH).toBeTruthy(); - expect(!!IOS_LIBRARY_PATH).toBeTruthy(); - } else { - expect(!!ANDROID_EXTERNAL_FILES_PATH).toBeTruthy(); - expect(!!ANDROID_DATABASE_PATH).toBeTruthy(); - expect(!!ANDROID_FILES_PATH).toBeTruthy(); - } - }); +describe("Constants tests", () => { + it("Constants are there", async () => { + if (Platform.OS === "ios") { + expect(!!IOS_DOCUMENT_PATH).toBeTruthy(); + expect(!!IOS_LIBRARY_PATH).toBeTruthy(); + } else { + expect(!!ANDROID_EXTERNAL_FILES_PATH).toBeTruthy(); + expect(!!ANDROID_DATABASE_PATH).toBeTruthy(); + expect(!!ANDROID_FILES_PATH).toBeTruthy(); + } + }); }); diff --git a/example/src/tests/dbsetup.ts b/example/src/tests/dbsetup.ts index c1a2457f..bf922a4a 100644 --- a/example/src/tests/dbsetup.ts +++ b/example/src/tests/dbsetup.ts @@ -1,290 +1,290 @@ import { - ANDROID_DATABASE_PATH, - ANDROID_EXTERNAL_FILES_PATH, - IOS_LIBRARY_PATH, - isIOSEmbeeded, - isLibsql, - isSQLCipher, - moveAssetsDatabase, - open, -} from '@op-engineering/op-sqlite'; -import {describe, it, expect} from '@op-engineering/op-test'; -import {Platform} from 'react-native'; + ANDROID_DATABASE_PATH, + ANDROID_EXTERNAL_FILES_PATH, + IOS_LIBRARY_PATH, + isIOSEmbeeded, + isLibsql, + isSQLCipher, + moveAssetsDatabase, + open, +} from "@op-engineering/op-sqlite"; +import { describe, expect, it } from "@op-engineering/op-test"; +import { Platform } from "react-native"; const expectedVersion = isLibsql() - ? '3.45.1' - : isSQLCipher() - ? '3.51.3' - : '3.51.3'; -const flavor = isLibsql() ? 'libsql' : isSQLCipher() ? 'sqlcipher' : 'sqlite'; + ? "3.45.1" + : isSQLCipher() + ? "3.51.3" + : "3.51.3"; +const flavor = isLibsql() ? "libsql" : isSQLCipher() ? "sqlcipher" : "sqlite"; // const expectedSqliteVecVersion = 'v0.1.2-alpha.7'; -describe('DB setup tests', () => { - // it('Should match the sqlite_vec version', async () => { - // let db = open({ - // name: 'versionTest.sqlite', - // }); - - // const res = db.execute('select vec_version();'); - - // expect(res.rows?._array[0]['vec_version()']).to.equal( - // expectedSqliteVecVersion, - // ); - - // db.close(); - // }); - - // Using the embedded version, you can never be sure which version is used - // It will change from OS version to version - if (!isIOSEmbeeded()) { - it(`Should match the sqlite flavor ${flavor} expected version ${expectedVersion}`, async () => { - let db = open({ - name: 'versionTest.sqlite', - encryptionKey: 'test', - }); - - const res = await db.execute('select sqlite_version();'); - - expect(res.rows[0]!['sqlite_version()']).toBe(expectedVersion); - db.close(); - }); - } - - it('Create in memory DB', async () => { - let inMemoryDb = open({ - name: 'inMemoryTest.sqlite', - location: ':memory:', - encryptionKey: 'test', - }); - - await inMemoryDb.execute('DROP TABLE IF EXISTS User;'); - await inMemoryDb.execute( - 'CREATE TABLE User ( id INT PRIMARY KEY, name TEXT NOT NULL, age INT, networth REAL) STRICT;', - ); - - inMemoryDb.close(); - }); - - if (Platform.OS === 'android') { - it('Create db in external directory Android', async () => { - let androidDb = open({ - name: 'AndroidSDCardDB.sqlite', - location: ANDROID_EXTERNAL_FILES_PATH, - encryptionKey: 'test', - }); - - await androidDb.execute('DROP TABLE IF EXISTS User;'); - await androidDb.execute( - 'CREATE TABLE User ( id INT PRIMARY KEY, name TEXT NOT NULL, age INT, networth REAL) STRICT;', - ); - - androidDb.close(); - }); - - it('Creates db in external nested directory on Android', async () => { - let androidDb = open({ - name: 'AndroidSDCardDB.sqlite', - location: `${ANDROID_EXTERNAL_FILES_PATH}/nested`, - encryptionKey: 'test', - }); - - await androidDb.execute('DROP TABLE IF EXISTS User;'); - await androidDb.execute( - 'CREATE TABLE User ( id INT PRIMARY KEY, name TEXT NOT NULL, age INT, networth REAL) STRICT;', - ); - - androidDb.close(); - }); - } - - // Currently this only tests the function is there - it('Should load extension', async () => { - let db = open({ - name: 'extensionDb', - encryptionKey: 'test', - }); - - try { - db.loadExtension('path'); - } catch (e) { - // TODO load a sample extension - expect(!!e).toEqual(true); - } finally { - db.delete(); - } - }); - - it('Should delete db', async () => { - let db = open({ - name: 'deleteTest', - encryptionKey: 'test', - }); - - db.delete(); - }); - - it('Should delete db with absolute path', async () => { - let location = - Platform.OS === 'ios' ? IOS_LIBRARY_PATH : ANDROID_DATABASE_PATH; - let db = open({ - name: 'deleteTest', - encryptionKey: 'test', - location, - }); - - expect(db.getDbPath().includes(location)).toEqual(true); - - db.delete(); - }); - - it('Should create db in custom folder', async () => { - let db = open({ - name: 'customFolderTest.sqlite', - encryptionKey: 'test', - location: 'myFolder', - }); - - let path = db.getDbPath(); - expect(path.includes('myFolder')).toEqual(true); - db.delete(); - }); - - it('Should create nested folders', async () => { - let db = open({ - name: 'nestedFolderTest.sqlite', - encryptionKey: 'test', - location: 'myFolder/nested', - }); - - let path = db.getDbPath(); - expect(path.includes('myFolder/nested')).toEqual(true); - db.delete(); - }); - - it('Moves assets database simple', async () => { - const copied = await moveAssetsDatabase({filename: 'sample.sqlite'}); - - expect(copied).toEqual(true); - }); - - it('Moves assets database with path', async () => { - const copied = await moveAssetsDatabase({ - filename: 'sample2.sqlite', - path: 'sqlite', - }); - - expect(copied).toEqual(true); - }); - - it('Moves assets database with path and overwrite', async () => { - const copied = await moveAssetsDatabase({ - filename: 'sample2.sqlite', - path: 'sqlite', - overwrite: true, - }); - - expect(copied).toEqual(true); - - let db = open({ - name: 'sample2.sqlite', - encryptionKey: 'test', - location: 'sqlite', - }); - - let path = db.getDbPath(); - expect(path.includes('sqlite/sample2.sqlite')).toEqual(true); - db.delete(); - }); - - it('Creates new connections per query and closes them', async () => { - for (let i = 0; i < 100; i++) { - let db = open({ - name: 'versionTest.sqlite', - encryptionKey: 'test', - }); - - await db.execute('select 1;'); - - db.close(); - } - }); - - it('Closes connections correctly', async () => { - try { - let db1 = open({ - name: 'closeTest.sqlite', - }); - expect(!!db1).toBe(true); - open({ - name: 'closeTest.sqlite', - }); - } catch (e) { - expect(!!e).toBe(true); - } - }); +describe("DB setup tests", () => { + // it('Should match the sqlite_vec version', async () => { + // let db = open({ + // name: 'versionTest.sqlite', + // }); + + // const res = db.execute('select vec_version();'); + + // expect(res.rows?._array[0]['vec_version()']).to.equal( + // expectedSqliteVecVersion, + // ); + + // db.close(); + // }); + + // Using the embedded version, you can never be sure which version is used + // It will change from OS version to version + if (!isIOSEmbeeded()) { + it(`Should match the sqlite flavor ${flavor} expected version ${expectedVersion}`, async () => { + const db = open({ + name: "versionTest.sqlite", + encryptionKey: "test", + }); + + const res = await db.execute("select sqlite_version();"); + + expect(res.rows[0]!["sqlite_version()"]).toBe(expectedVersion); + db.close(); + }); + } + + it("Create in memory DB", async () => { + const inMemoryDb = open({ + name: "inMemoryTest.sqlite", + location: ":memory:", + encryptionKey: "test", + }); + + await inMemoryDb.execute("DROP TABLE IF EXISTS User;"); + await inMemoryDb.execute( + "CREATE TABLE User ( id INT PRIMARY KEY, name TEXT NOT NULL, age INT, networth REAL) STRICT;", + ); + + inMemoryDb.close(); + }); + + if (Platform.OS === "android") { + it("Create db in external directory Android", async () => { + const androidDb = open({ + name: "AndroidSDCardDB.sqlite", + location: ANDROID_EXTERNAL_FILES_PATH, + encryptionKey: "test", + }); + + await androidDb.execute("DROP TABLE IF EXISTS User;"); + await androidDb.execute( + "CREATE TABLE User ( id INT PRIMARY KEY, name TEXT NOT NULL, age INT, networth REAL) STRICT;", + ); + + androidDb.close(); + }); + + it("Creates db in external nested directory on Android", async () => { + const androidDb = open({ + name: "AndroidSDCardDB.sqlite", + location: `${ANDROID_EXTERNAL_FILES_PATH}/nested`, + encryptionKey: "test", + }); + + await androidDb.execute("DROP TABLE IF EXISTS User;"); + await androidDb.execute( + "CREATE TABLE User ( id INT PRIMARY KEY, name TEXT NOT NULL, age INT, networth REAL) STRICT;", + ); + + androidDb.close(); + }); + } + + // Currently this only tests the function is there + it("Should load extension", async () => { + const db = open({ + name: "extensionDb", + encryptionKey: "test", + }); + + try { + db.loadExtension("path"); + } catch (e) { + // TODO load a sample extension + expect(!!e).toEqual(true); + } finally { + db.delete(); + } + }); + + it("Should delete db", async () => { + const db = open({ + name: "deleteTest", + encryptionKey: "test", + }); + + db.delete(); + }); + + it("Should delete db with absolute path", async () => { + const location = + Platform.OS === "ios" ? IOS_LIBRARY_PATH : ANDROID_DATABASE_PATH; + const db = open({ + name: "deleteTest", + encryptionKey: "test", + location, + }); + + expect(db.getDbPath().includes(location)).toEqual(true); + + db.delete(); + }); + + it("Should create db in custom folder", async () => { + const db = open({ + name: "customFolderTest.sqlite", + encryptionKey: "test", + location: "myFolder", + }); + + const path = db.getDbPath(); + expect(path.includes("myFolder")).toEqual(true); + db.delete(); + }); + + it("Should create nested folders", async () => { + const db = open({ + name: "nestedFolderTest.sqlite", + encryptionKey: "test", + location: "myFolder/nested", + }); + + const path = db.getDbPath(); + expect(path.includes("myFolder/nested")).toEqual(true); + db.delete(); + }); + + it("Moves assets database simple", async () => { + const copied = await moveAssetsDatabase({ filename: "sample.sqlite" }); + + expect(copied).toEqual(true); + }); + + it("Moves assets database with path", async () => { + const copied = await moveAssetsDatabase({ + filename: "sample2.sqlite", + path: "sqlite", + }); + + expect(copied).toEqual(true); + }); + + it("Moves assets database with path and overwrite", async () => { + const copied = await moveAssetsDatabase({ + filename: "sample2.sqlite", + path: "sqlite", + overwrite: true, + }); + + expect(copied).toEqual(true); + + const db = open({ + name: "sample2.sqlite", + encryptionKey: "test", + location: "sqlite", + }); + + const path = db.getDbPath(); + expect(path.includes("sqlite/sample2.sqlite")).toEqual(true); + db.delete(); + }); + + it("Creates new connections per query and closes them", async () => { + for (let i = 0; i < 100; i++) { + const db = open({ + name: "versionTest.sqlite", + encryptionKey: "test", + }); + + await db.execute("select 1;"); + + db.close(); + } + }); + + it("Closes connections correctly", async () => { + try { + const db1 = open({ + name: "closeTest.sqlite", + }); + expect(!!db1).toBe(true); + open({ + name: "closeTest.sqlite", + }); + } catch (e) { + expect(!!e).toBe(true); + } + }); }); -it('Can attach/dettach database', () => { - let db = open({ - name: 'attachTest.sqlite', - encryptionKey: 'test', - }); - let db2 = open({ - name: 'attachTest2.sqlite', - encryptionKey: 'test', - }); - db2.close(); - - db.attach({ - secondaryDbFileName: 'attachTest2.sqlite', - alias: 'attach2', - }); - - db.executeSync('DROP TABLE IF EXISTS attach2.test;'); - db.executeSync( - 'CREATE TABLE IF NOT EXISTS attach2.test (id INTEGER PRIMARY KEY);', - ); - let res = db.executeSync('INSERT INTO attach2.test (id) VALUES (1);'); - expect(!!res).toBe(true); - - db.detach('attach2'); - - db.delete(); - - db2 = open({ - name: 'attachTest2.sqlite', - encryptionKey: 'test', - }); - db2.delete(); +it("Can attach/dettach database", () => { + const db = open({ + name: "attachTest.sqlite", + encryptionKey: "test", + }); + let db2 = open({ + name: "attachTest2.sqlite", + encryptionKey: "test", + }); + db2.close(); + + db.attach({ + secondaryDbFileName: "attachTest2.sqlite", + alias: "attach2", + }); + + db.executeSync("DROP TABLE IF EXISTS attach2.test;"); + db.executeSync( + "CREATE TABLE IF NOT EXISTS attach2.test (id INTEGER PRIMARY KEY);", + ); + const res = db.executeSync("INSERT INTO attach2.test (id) VALUES (1);"); + expect(!!res).toBe(true); + + db.detach("attach2"); + + db.delete(); + + db2 = open({ + name: "attachTest2.sqlite", + encryptionKey: "test", + }); + db2.delete(); }); -it('Can get db path', () => { - let db = open({ - name: 'pathTest.sqlite', - encryptionKey: 'test', - }); +it("Can get db path", () => { + const db = open({ + name: "pathTest.sqlite", + encryptionKey: "test", + }); - let path = db.getDbPath(); - expect(!!path).toBe(true); - db.close(); + const path = db.getDbPath(); + expect(!!path).toBe(true); + db.close(); }); if (isLibsql()) { - it('Libsql can set reserved bytes', async () => { - const db = open({name: 'test.db'}); - db.setReservedBytes(28); - expect(db.getReservedBytes()).toEqual(28); - db.delete(); - }); + it("Libsql can set reserved bytes", async () => { + const db = open({ name: "test.db" }); + db.setReservedBytes(28); + expect(db.getReservedBytes()).toEqual(28); + db.delete(); + }); } if (isSQLCipher()) { - it('Can open SQLCipher db without encryption key', () => { - let db = open({ - name: 'pathTest.sqlite', - }); + it("Can open SQLCipher db without encryption key", () => { + const db = open({ + name: "pathTest.sqlite", + }); - db.close(); - }); + db.close(); + }); } diff --git a/example/src/tests/hooks.ts b/example/src/tests/hooks.ts index 1397b636..daa38e7d 100644 --- a/example/src/tests/hooks.ts +++ b/example/src/tests/hooks.ts @@ -1,250 +1,249 @@ -import Chance from 'chance'; - -import {type DB, open, isLibsql} from '@op-engineering/op-sqlite'; +import { type DB, isLibsql, open } from "@op-engineering/op-sqlite"; import { - expect, - describe, - it, - beforeEach, - afterEach, -} from '@op-engineering/op-test'; -import {sleep} from './utils'; + afterEach, + beforeEach, + describe, + expect, + it, +} from "@op-engineering/op-test"; +import Chance from "chance"; +import { sleep } from "./utils"; const DB_CONFIG = { - name: 'hooksDb', - encryptionKey: 'test', + name: "hooksDb", + encryptionKey: "test", }; const chance = new Chance(); -describe('Hooks', () => { - let db: DB; - if (isLibsql()) { - return; - } - - beforeEach(async () => { - try { - db = open(DB_CONFIG); - - await db.execute('DROP TABLE IF EXISTS User;'); - await db.execute( - 'CREATE TABLE User ( id INT PRIMARY KEY, name TEXT NOT NULL, age INT, networth REAL) STRICT;', - ); - } catch (e) { - console.warn('error on before each', e); - } - }); - - afterEach(() => { - if (db) { - db.delete(); - } - }); - - it('update hook', async () => { - let promiseResolve: any; - let promise = new Promise<{ - rowId: number; - row?: any; - operation: string; - table: string; - }>(resolve => { - promiseResolve = resolve; - }); - - db.updateHook(data => { - promiseResolve(data); - }); - - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - await db.transaction(async tx => { - await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - }); - - const data = await promise; - - expect(data.operation).toEqual('INSERT'); - expect(data.rowId).toEqual(1); - - db.updateHook(null); - }); - - it('Execute batch should trigger update hook', async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - db.executeSync( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - let promiseResolve: any; - let promise = new Promise<{ - rowId: number; - row?: any; - operation: string; - table: string; - }>(resolve => { - promiseResolve = resolve; - }); - - db.updateHook(data => { - promiseResolve(data); - }); - - await db.executeBatch([ - ['UPDATE "User" SET name = ? WHERE id = ?', ['foo', id]], - ]); - - const data = await promise; - - expect(data.operation).toEqual('UPDATE'); - expect(data.rowId).toEqual(1); - }); - - it('remove update hook', async () => { - const hookRes: string[] = []; - - db.updateHook(({operation}) => { - hookRes.push(operation); - }); - - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - await db.transaction(async tx => { - await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - }); - - db.updateHook(null); - - await db.transaction(async tx => { - await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id + 1, name, age, networth], - ); - }); - - await sleep(0); - - expect(hookRes.length).toEqual(1); - }); - - it('commit hook', async () => { - let promiseResolve: any; - let promise = new Promise(resolve => { - promiseResolve = resolve; - }); - - db.commitHook(() => { - promiseResolve?.(); - }); - - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - await db.transaction(async tx => { - await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - }); - - await promise; - db.commitHook(null); - }); - - it('remove commit hook', async () => { - const hookRes: string[] = []; - db.commitHook(() => { - hookRes.push('commit'); - }); - - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - await db.transaction(async tx => { - await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - }); - - db.commitHook(null); - - await db.transaction(async tx => { - await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id + 1, name, age, networth], - ); - }); - - await sleep(0); - - expect(hookRes.length).toEqual(1); - }); - - it('rollback hook', async () => { - let promiseResolve: any; - let promise = new Promise(resolve => { - promiseResolve = resolve; - }); - - db.rollbackHook(() => { - promiseResolve?.(); - }); - - try { - await db.transaction(async () => { - throw new Error('Blah'); - }); - } catch (e) { - // intentionally left blank - } - - await promise; - }); - - it('remove rollback hook', async () => { - const hookRes: string[] = []; - db.rollbackHook(() => { - hookRes.push('rollback'); - }); - - try { - await db.transaction(async () => { - throw new Error('Blah'); - }); - } catch (e) { - // intentionally left blank - } - - db.rollbackHook(null); - - try { - await db.transaction(async () => { - throw new Error('Blah'); - }); - } catch (e) { - // intentionally left blank - } - - await sleep(0); - - expect(hookRes.length).toEqual(1); - }); +describe("Hooks", () => { + let db: DB; + if (isLibsql()) { + return; + } + + beforeEach(async () => { + try { + db = open(DB_CONFIG); + + await db.execute("DROP TABLE IF EXISTS User;"); + await db.execute( + "CREATE TABLE User ( id INT PRIMARY KEY, name TEXT NOT NULL, age INT, networth REAL) STRICT;", + ); + } catch (e) { + console.warn("error on before each", e); + } + }); + + afterEach(() => { + if (db) { + db.delete(); + } + }); + + it("update hook", async () => { + let promiseResolve: any; + const promise = new Promise<{ + rowId: number; + row?: any; + operation: string; + table: string; + }>((resolve) => { + promiseResolve = resolve; + }); + + db.updateHook((data) => { + promiseResolve(data); + }); + + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + await db.transaction(async (tx) => { + await tx.execute( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id, name, age, networth], + ); + }); + + const data = await promise; + + expect(data.operation).toEqual("INSERT"); + expect(data.rowId).toEqual(1); + + db.updateHook(null); + }); + + it("Execute batch should trigger update hook", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + db.executeSync( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id, name, age, networth], + ); + + let promiseResolve: any; + const promise = new Promise<{ + rowId: number; + row?: any; + operation: string; + table: string; + }>((resolve) => { + promiseResolve = resolve; + }); + + db.updateHook((data) => { + promiseResolve(data); + }); + + await db.executeBatch([ + ['UPDATE "User" SET name = ? WHERE id = ?', ["foo", id]], + ]); + + const data = await promise; + + expect(data.operation).toEqual("UPDATE"); + expect(data.rowId).toEqual(1); + }); + + it("remove update hook", async () => { + const hookRes: string[] = []; + + db.updateHook(({ operation }) => { + hookRes.push(operation); + }); + + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + await db.transaction(async (tx) => { + await tx.execute( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id, name, age, networth], + ); + }); + + db.updateHook(null); + + await db.transaction(async (tx) => { + await tx.execute( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id + 1, name, age, networth], + ); + }); + + await sleep(0); + + expect(hookRes.length).toEqual(1); + }); + + it("commit hook", async () => { + let promiseResolve: any; + const promise = new Promise((resolve) => { + promiseResolve = resolve; + }); + + db.commitHook(() => { + promiseResolve?.(); + }); + + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + await db.transaction(async (tx) => { + await tx.execute( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id, name, age, networth], + ); + }); + + await promise; + db.commitHook(null); + }); + + it("remove commit hook", async () => { + const hookRes: string[] = []; + db.commitHook(() => { + hookRes.push("commit"); + }); + + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + await db.transaction(async (tx) => { + await tx.execute( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id, name, age, networth], + ); + }); + + db.commitHook(null); + + await db.transaction(async (tx) => { + await tx.execute( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id + 1, name, age, networth], + ); + }); + + await sleep(0); + + expect(hookRes.length).toEqual(1); + }); + + it("rollback hook", async () => { + let promiseResolve: any; + const promise = new Promise((resolve) => { + promiseResolve = resolve; + }); + + db.rollbackHook(() => { + promiseResolve?.(); + }); + + try { + await db.transaction(async () => { + throw new Error("Blah"); + }); + } catch (e) { + // intentionally left blank + } + + await promise; + }); + + it("remove rollback hook", async () => { + const hookRes: string[] = []; + db.rollbackHook(() => { + hookRes.push("rollback"); + }); + + try { + await db.transaction(async () => { + throw new Error("Blah"); + }); + } catch (e) { + // intentionally left blank + } + + db.rollbackHook(null); + + try { + await db.transaction(async () => { + throw new Error("Blah"); + }); + } catch (e) { + // intentionally left blank + } + + await sleep(0); + + expect(hookRes.length).toEqual(1); + }); }); diff --git a/example/src/tests/index.ts b/example/src/tests/index.ts index 0007b7a3..3e229734 100644 --- a/example/src/tests/index.ts +++ b/example/src/tests/index.ts @@ -1,9 +1,10 @@ -import './blob'; -import './constants'; -import './dbsetup'; -import './hooks'; -import './preparedStatements'; -import './queries'; -import './reactive'; -import './storage'; -import './tokenizer'; +import "./blob"; +import "./constants"; +import "./dbsetup"; +import "./hooks"; +import "./preparedStatements"; +import "./queries"; +import "./reactive"; +import "./storage"; +import "./tokenizer"; +import "./web"; diff --git a/example/src/tests/preparedStatements.ts b/example/src/tests/preparedStatements.ts index 36251882..f631abb8 100644 --- a/example/src/tests/preparedStatements.ts +++ b/example/src/tests/preparedStatements.ts @@ -1,102 +1,102 @@ -import {open, type DB} from '@op-engineering/op-sqlite'; +import { type DB, open } from "@op-engineering/op-sqlite"; import { - afterAll, - beforeEach, - describe, - it, - expect, -} from '@op-engineering/op-test'; - -describe('PreparedStatements', () => { - let db: DB; - beforeEach(async () => { - try { - if (db) { - db.close(); - db.delete(); - } - - db = open({ - name: 'statements', - encryptionKey: 'test', - }); - - await db.execute('DROP TABLE IF EXISTS User;'); - await db.execute( - 'CREATE TABLE User ( id INT PRIMARY KEY, name TEXT) STRICT;', - ); - await db.execute('INSERT INTO "User" (id, name) VALUES(?,?)', [ - 1, - 'Oscar', - ]); - await db.execute('INSERT INTO "User" (id, name) VALUES(?,?)', [ - 2, - 'Pablo', - ]); - await db.execute('INSERT INTO "User" (id, name) VALUES(?,?)', [ - 3, - 'Carlos', - ]); - } catch (e) { - console.warn('error on before each', e); - } - }); - - afterAll(() => { - if (db) { - db.close(); - db.delete(); - // @ts-ignore - db = null; - } - }); - - it('Creates a prepared statement and executes a prepared statement', async () => { - const statement = db.prepareStatement('SELECT * FROM User;'); - let results = await statement.execute(); - - expect(results.rows?.length).toEqual(3); - results = await statement.execute(); - - expect(results.rows.length).toEqual(3); - }); - - it('prepared statement, rebind select', async () => { - const statement = db.prepareStatement('SELECT * FROM User WHERE id = ?;'); - await statement.bind([1]); - - let results = await statement.execute(); - expect(results.rows[0]!.name === 'Oscar'); - - await statement.bind([2]); - results = await statement.execute(); - expect(results.rows[0]!.name === 'Pablo'); - }); - - it('prepared statement, rebind insert', async () => { - const statement = db.prepareStatement( - 'INSERT INTO "User" (id, name) VALUES(?,?);', - ); - await statement.bind([4, 'Juan']); - await statement.execute(); - - await statement.bind([5, 'Pedro']); - await statement.execute(); - }); - - it('prepared statement, bindsync', async () => { - const statement = db.prepareStatement( - 'INSERT INTO "User" (id, name) VALUES(?,?);', - ); - - statement.bindSync([4, 'Juan']); - await statement.execute(); - - statement.bind([5, 'Pedro']); - await statement.execute(); - - const selectStatement = db.prepareStatement('SELECT * FROM User;'); - let results = await selectStatement.execute(); - expect(results.rows.length).toEqual(5); - }); + afterAll, + beforeEach, + describe, + expect, + it, +} from "@op-engineering/op-test"; + +describe("PreparedStatements", () => { + let db: DB; + beforeEach(async () => { + try { + if (db) { + db.close(); + db.delete(); + } + + db = open({ + name: "statements", + encryptionKey: "test", + }); + + await db.execute("DROP TABLE IF EXISTS User;"); + await db.execute( + "CREATE TABLE User ( id INT PRIMARY KEY, name TEXT) STRICT;", + ); + await db.execute('INSERT INTO "User" (id, name) VALUES(?,?)', [ + 1, + "Oscar", + ]); + await db.execute('INSERT INTO "User" (id, name) VALUES(?,?)', [ + 2, + "Pablo", + ]); + await db.execute('INSERT INTO "User" (id, name) VALUES(?,?)', [ + 3, + "Carlos", + ]); + } catch (e) { + console.warn("error on before each", e); + } + }); + + afterAll(() => { + if (db) { + db.close(); + db.delete(); + // @ts-expect-error + db = null; + } + }); + + it("Creates a prepared statement and executes a prepared statement", async () => { + const statement = db.prepareStatement("SELECT * FROM User;"); + let results = await statement.execute(); + + expect(results.rows?.length).toEqual(3); + results = await statement.execute(); + + expect(results.rows.length).toEqual(3); + }); + + it("prepared statement, rebind select", async () => { + const statement = db.prepareStatement("SELECT * FROM User WHERE id = ?;"); + await statement.bind([1]); + + let results = await statement.execute(); + expect(results.rows[0]!.name === "Oscar"); + + await statement.bind([2]); + results = await statement.execute(); + expect(results.rows[0]!.name === "Pablo"); + }); + + it("prepared statement, rebind insert", async () => { + const statement = db.prepareStatement( + 'INSERT INTO "User" (id, name) VALUES(?,?);', + ); + await statement.bind([4, "Juan"]); + await statement.execute(); + + await statement.bind([5, "Pedro"]); + await statement.execute(); + }); + + it("prepared statement, bindsync", async () => { + const statement = db.prepareStatement( + 'INSERT INTO "User" (id, name) VALUES(?,?);', + ); + + statement.bindSync([4, "Juan"]); + await statement.execute(); + + statement.bind([5, "Pedro"]); + await statement.execute(); + + const selectStatement = db.prepareStatement("SELECT * FROM User;"); + const results = await selectStatement.execute(); + expect(results.rows.length).toEqual(5); + }); }); diff --git a/example/src/tests/queries.ts b/example/src/tests/queries.ts index 8fe2f997..e5cbcd47 100644 --- a/example/src/tests/queries.ts +++ b/example/src/tests/queries.ts @@ -1,394 +1,395 @@ -import {chance} from './utils'; import { - isLibsql, - open, - // openRemote, - // openSync, - type DB, - type SQLBatchTuple, -} from '@op-engineering/op-sqlite'; + // openRemote, + // openSync, + type DB, + isLibsql, + open, + type SQLBatchTuple, +} from "@op-engineering/op-sqlite"; import { - expect, - afterEach, - beforeEach, - describe, - it, -} from '@op-engineering/op-test'; + afterEach, + beforeEach, + describe, + expect, + it, +} from "@op-engineering/op-test"; +import { chance } from "./utils"; + // import pkg from '../../package.json' -describe('Queries tests', () => { - let db: DB; - - beforeEach(async () => { - db = open({ - name: 'queries.sqlite', - encryptionKey: 'test', - }); - - await db.execute('DROP TABLE IF EXISTS User;'); - await db.execute('DROP TABLE IF EXISTS T1;'); - await db.execute('DROP TABLE IF EXISTS T2;'); - await db.execute( - 'CREATE TABLE User (id INT PRIMARY KEY, name TEXT NOT NULL, age INT, networth REAL, nickname TEXT) STRICT;', - ); - }); - - afterEach(() => { - if (db) { - db.delete(); - // @ts-ignore - db = null; - } - }); - - if (isLibsql()) { - // itOnly('Remote open a turso database', async () => { - // const remoteDb = openRemote({ - // url: 'libsql://foo-ospfranco.turso.io', - // authToken: - // 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhIjoicnciLCJpYXQiOjE3MTY5NTc5OTUsImlkIjoiZmJkNzZmMjYtZTliYy00MGJiLTlmYmYtMDczZjFmMjdjOGY4In0.U3cAWBOvcdiqoPN3MB81sco7x8CGOjjtZ1ZEf30uo2iPcAmOuJzcnAznmDlZ6SpQd4qzuJxE4mAIoRlOkpzgBQ', - // }); - // console.log('Running select 1'); - // const res = await remoteDb.execute('SELECT 1'); - // console.log('after select 1;'); - // expect(res.rowsAffected).toEqual(0); - // }); - // it('Open a libsql database replicated to turso', async () => { - // const remoteDb = openSync({ - // url: 'libsql://foo-ospfranco.turso.io', - // authToken: - // 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhIjoicnciLCJpYXQiOjE3MTY5NTc5OTUsImlkIjoiZmJkNzZmMjYtZTliYy00MGJiLTlmYmYtMDczZjFmMjdjOGY4In0.U3cAWBOvcdiqoPN3MB81sco7x8CGOjjtZ1ZEf30uo2iPcAmOuJzcnAznmDlZ6SpQd4qzuJxE4mAIoRlOkpzgBQ', - // name: 'my replica', - // libsqlSyncInterval: 1000, - // encryptionKey: 'blah', - // }); - // const res = await remoteDb.execute('SELECT 1'); - // remoteDb.sync(); - // expect(res.rowsAffected).toEqual(0); - // }); - } - - it('Can create multiple connections to same db', async () => { - const db2 = open({ - name: 'queries.sqlite', - encryptionKey: 'test', - }); - - const db3 = open({ - name: 'queries.sqlite', - encryptionKey: 'test', - }); - - let promises = [ - db.execute('SELECT 1'), - db2.execute('SELECT 1'), - db3.execute('SELECT 1'), - ]; - - let res = await Promise.all(promises); - res.forEach(r => { - expect(r.rowsAffected).toEqual(0); - expect(r.rows[0]!['1']).toEqual(1); - }); - }); - - it('Trying to pass object as param should throw', async () => { - try { - // @ts-ignore - await db.execute('SELECT ?', [{foo: 'bar'}]); - } catch (e: any) { - expect( - e.message.includes( - 'Exception in HostFunction: Object is not an ArrayBuffer, cannot bind to SQLite', - ), - ).toEqual(true); - } - }); - - it('executeSync', () => { - const res = db.executeSync('SELECT 1'); - expect(res.rowsAffected).toEqual(0); - - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - const res2 = db.executeSync( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - expect(res2.rowsAffected).toEqual(1); - expect(res2.insertId).toEqual(1); - // expect(res2.rows).toBe([]); - expect(res2.rows?.length).toEqual(0); - }); - - it('Insert', async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - const res = await db.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - expect(res.rowsAffected).toEqual(1); - expect(res.insertId).toEqual(1); - // expect(res.metadata).toEqual([]); - expect(res.rows).toDeepEqual([]); - expect(res.rows?.length).toEqual(0); - }); - - it('Casts booleans to ints correctly', async () => { - await db.execute(`SELECT ?`, [1]); - await db.execute(`SELECT ?`, [true]); - }); - - it('Insert and query with host objects', async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - const res = await db.executeWithHostObjects( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - expect(res.rowsAffected).toEqual(1); - expect(res.insertId).toEqual(1); - // expect(res.metadata).toEqual([]); - expect(res.rows).toDeepEqual([]); - expect(res.rows?.length).toEqual(0); - - const queryRes = await db.executeWithHostObjects('SELECT * FROM User'); - - expect(queryRes.rowsAffected).toEqual(1); - expect(queryRes.insertId).toEqual(1); - expect(queryRes.rows).toDeepEqual([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - }); - - it('Query without params', async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - await db.execute( - 'INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - const res = await db.execute('SELECT * FROM User'); - - expect(res.rowsAffected).toEqual(1); - expect(res.insertId).toEqual(1); - expect(res.rows).toDeepEqual([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - }); - - it('Query with params', async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - await db.execute( - 'INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - const res = await db.execute('SELECT * FROM User WHERE id = ?', [id]); - - expect(res.rowsAffected).toEqual(1); - expect(res.insertId).toEqual(1); - expect(res.rows).toDeepEqual([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - }); - - it('Query with sqlite functions', async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - // COUNT(*) - await db.execute( - 'INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - const countRes = await db.execute('SELECT COUNT(*) as count FROM User'); - - expect(countRes.rows?.length).toEqual(1); - expect(countRes.rows?.[0]?.count).toEqual(1); - - // SUM(age) - const id2 = chance.integer(); - const name2 = chance.name(); - const age2 = chance.integer(); - const networth2 = chance.floating(); - - await db.execute( - 'INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id2, name2, age2, networth2], - ); - - const sumRes = await db.execute('SELECT SUM(age) as sum FROM User;'); - - expect(sumRes.rows[0]!.sum).toEqual(age + age2); - - const maxRes = await db.execute('SELECT MAX(networth) as `max` FROM User;'); - const minRes = await db.execute('SELECT MIN(networth) as `min` FROM User;'); - const maxNetworth = Math.max(networth, networth2); - const minNetworth = Math.min(networth, networth2); - - expect(maxRes.rows[0]!.max).toEqual(maxNetworth); - expect(minRes.rows[0]!.min).toEqual(minNetworth); - }); - - it('Executes all the statements in a single string', async () => { - if (isLibsql()) { - return; - } - await db.execute( - `CREATE TABLE T1 ( id INT PRIMARY KEY) STRICT; +describe("Queries tests", () => { + let db: DB; + + beforeEach(async () => { + db = open({ + name: "queries.sqlite", + encryptionKey: "test", + }); + + await db.execute("DROP TABLE IF EXISTS User;"); + await db.execute("DROP TABLE IF EXISTS T1;"); + await db.execute("DROP TABLE IF EXISTS T2;"); + await db.execute( + "CREATE TABLE User (id INT PRIMARY KEY, name TEXT NOT NULL, age INT, networth REAL, nickname TEXT) STRICT;", + ); + }); + + afterEach(() => { + if (db) { + db.delete(); + // @ts-expect-error + db = null; + } + }); + + if (isLibsql()) { + // itOnly('Remote open a turso database', async () => { + // const remoteDb = openRemote({ + // url: 'libsql://foo-ospfranco.turso.io', + // authToken: + // 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhIjoicnciLCJpYXQiOjE3MTY5NTc5OTUsImlkIjoiZmJkNzZmMjYtZTliYy00MGJiLTlmYmYtMDczZjFmMjdjOGY4In0.U3cAWBOvcdiqoPN3MB81sco7x8CGOjjtZ1ZEf30uo2iPcAmOuJzcnAznmDlZ6SpQd4qzuJxE4mAIoRlOkpzgBQ', + // }); + // console.log('Running select 1'); + // const res = await remoteDb.execute('SELECT 1'); + // console.log('after select 1;'); + // expect(res.rowsAffected).toEqual(0); + // }); + // it('Open a libsql database replicated to turso', async () => { + // const remoteDb = openSync({ + // url: 'libsql://foo-ospfranco.turso.io', + // authToken: + // 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhIjoicnciLCJpYXQiOjE3MTY5NTc5OTUsImlkIjoiZmJkNzZmMjYtZTliYy00MGJiLTlmYmYtMDczZjFmMjdjOGY4In0.U3cAWBOvcdiqoPN3MB81sco7x8CGOjjtZ1ZEf30uo2iPcAmOuJzcnAznmDlZ6SpQd4qzuJxE4mAIoRlOkpzgBQ', + // name: 'my replica', + // libsqlSyncInterval: 1000, + // encryptionKey: 'blah', + // }); + // const res = await remoteDb.execute('SELECT 1'); + // remoteDb.sync(); + // expect(res.rowsAffected).toEqual(0); + // }); + } + + it("Can create multiple connections to same db", async () => { + const db2 = open({ + name: "queries.sqlite", + encryptionKey: "test", + }); + + const db3 = open({ + name: "queries.sqlite", + encryptionKey: "test", + }); + + const promises = [ + db.execute("SELECT 1"), + db2.execute("SELECT 1"), + db3.execute("SELECT 1"), + ]; + + const res = await Promise.all(promises); + res.forEach((r) => { + expect(r.rowsAffected).toEqual(0); + expect(r.rows[0]?.["1"]).toEqual(1); + }); + }); + + it("Trying to pass object as param should throw", async () => { + try { + // @ts-expect-error + await db.execute("SELECT ?", [{ foo: "bar" }]); + } catch (e: any) { + expect( + e.message.includes( + "Exception in HostFunction: Object is not an ArrayBuffer, cannot bind to SQLite", + ), + ).toEqual(true); + } + }); + + it("executeSync", () => { + const res = db.executeSync("SELECT 1"); + expect(res.rowsAffected).toEqual(0); + + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + const res2 = db.executeSync( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id, name, age, networth], + ); + + expect(res2.rowsAffected).toEqual(1); + expect(res2.insertId).toEqual(1); + // expect(res2.rows).toBe([]); + expect(res2.rows?.length).toEqual(0); + }); + + it("Insert", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + const res = await db.execute( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id, name, age, networth], + ); + + expect(res.rowsAffected).toEqual(1); + expect(res.insertId).toEqual(1); + // expect(res.metadata).toEqual([]); + expect(res.rows).toDeepEqual([]); + expect(res.rows?.length).toEqual(0); + }); + + it("Casts booleans to ints correctly", async () => { + await db.execute(`SELECT ?`, [1]); + await db.execute(`SELECT ?`, [true]); + }); + + it("Insert and query with host objects", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + const res = await db.executeWithHostObjects( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id, name, age, networth], + ); + + expect(res.rowsAffected).toEqual(1); + expect(res.insertId).toEqual(1); + // expect(res.metadata).toEqual([]); + expect(res.rows).toDeepEqual([]); + expect(res.rows?.length).toEqual(0); + + const queryRes = await db.executeWithHostObjects("SELECT * FROM User"); + + expect(queryRes.rowsAffected).toEqual(1); + expect(queryRes.insertId).toEqual(1); + expect(queryRes.rows).toDeepEqual([ + { + id, + name, + age, + networth, + nickname: null, + }, + ]); + }); + + it("Query without params", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + await db.execute( + "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", + [id, name, age, networth], + ); + + const res = await db.execute("SELECT * FROM User"); + + expect(res.rowsAffected).toEqual(1); + expect(res.insertId).toEqual(1); + expect(res.rows).toDeepEqual([ + { + id, + name, + age, + networth, + nickname: null, + }, + ]); + }); + + it("Query with params", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + await db.execute( + "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", + [id, name, age, networth], + ); + + const res = await db.execute("SELECT * FROM User WHERE id = ?", [id]); + + expect(res.rowsAffected).toEqual(1); + expect(res.insertId).toEqual(1); + expect(res.rows).toDeepEqual([ + { + id, + name, + age, + networth, + nickname: null, + }, + ]); + }); + + it("Query with sqlite functions", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + // COUNT(*) + await db.execute( + "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", + [id, name, age, networth], + ); + + const countRes = await db.execute("SELECT COUNT(*) as count FROM User"); + + expect(countRes.rows?.length).toEqual(1); + expect(countRes.rows?.[0]?.count).toEqual(1); + + // SUM(age) + const id2 = chance.integer(); + const name2 = chance.name(); + const age2 = chance.integer(); + const networth2 = chance.floating(); + + await db.execute( + "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", + [id2, name2, age2, networth2], + ); + + const sumRes = await db.execute("SELECT SUM(age) as sum FROM User;"); + + expect(sumRes.rows[0]!.sum).toEqual(age + age2); + + const maxRes = await db.execute("SELECT MAX(networth) as `max` FROM User;"); + const minRes = await db.execute("SELECT MIN(networth) as `min` FROM User;"); + const maxNetworth = Math.max(networth, networth2); + const minNetworth = Math.min(networth, networth2); + + expect(maxRes.rows[0]!.max).toEqual(maxNetworth); + expect(minRes.rows[0]!.min).toEqual(minNetworth); + }); + + it("Executes all the statements in a single string", async () => { + if (isLibsql()) { + return; + } + await db.execute( + `CREATE TABLE T1 ( id INT PRIMARY KEY) STRICT; CREATE TABLE T2 ( id INT PRIMARY KEY) STRICT;`, - ); - - let t1name = await db.execute( - "SELECT name FROM sqlite_master WHERE type='table' AND name='T1';", - ); - - expect(t1name.rows[0]!.name).toEqual('T1'); - - let t2name = await db.execute( - "SELECT name FROM sqlite_master WHERE type='table' AND name='T2';", - ); - - expect(t2name.rows[0]!.name).toEqual('T2'); - }); - - it('Failed insert', async () => { - const id = chance.string(); - const name = chance.name(); - const age = chance.string(); - const networth = chance.string(); - try { - await db.execute( - 'INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - } catch (e: any) { - expect(typeof e).toEqual('object'); - - expect( - e.message.includes('cannot store TEXT value in INT column User.id'), - ).toEqual(true); - } - }); - - it('Transaction, auto commit', async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - await db.transaction(async tx => { - const res = await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - expect(res.rowsAffected).toEqual(1); - expect(res.insertId).toEqual(1); - // expect(res.metadata).toEqual([]); - expect(res.rows).toDeepEqual([]); - expect(res.rows?.length).toEqual(0); - }); - - const res = await db.execute('SELECT * FROM User'); - expect(res.rows).toDeepEqual([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - }); - - it('Transaction, manual commit', async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - await db.transaction(async tx => { - const res = await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - expect(res.rowsAffected).toEqual(1); - expect(res.insertId).toEqual(1); - expect(res.rows).toDeepEqual([]); - expect(res.rows?.length).toEqual(0); - - await tx.commit(); - }); - - const res = await db.execute('SELECT * FROM User'); - // console.log(res); - expect(res.rows).toDeepEqual([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - }); - - it('Transaction, executed in order', async () => { - const xs = 10; - const actual: unknown[] = []; - - // ARRANGE: Generate expected data - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - - // ACT: Start multiple transactions to upsert and select the same record - const promises = []; - for (let i = 1; i <= xs; i++) { - const promised = db.transaction(async tx => { - // ACT: Upsert statement to create record / increment the value - await tx.execute( - ` + ); + + const t1name = await db.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name='T1';", + ); + + expect(t1name.rows[0]!.name).toEqual("T1"); + + const t2name = await db.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name='T2';", + ); + + expect(t2name.rows[0]!.name).toEqual("T2"); + }); + + it("Failed insert", async () => { + const id = chance.string(); + const name = chance.name(); + const age = chance.string(); + const networth = chance.string(); + try { + await db.execute( + "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", + [id, name, age, networth], + ); + } catch (e: any) { + expect(typeof e).toEqual("object"); + + expect( + e.message.includes("cannot store TEXT value in INT column User.id"), + ).toEqual(true); + } + }); + + it("Transaction, auto commit", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + await db.transaction(async (tx) => { + const res = await tx.execute( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id, name, age, networth], + ); + + expect(res.rowsAffected).toEqual(1); + expect(res.insertId).toEqual(1); + // expect(res.metadata).toEqual([]); + expect(res.rows).toDeepEqual([]); + expect(res.rows?.length).toEqual(0); + }); + + const res = await db.execute("SELECT * FROM User"); + expect(res.rows).toDeepEqual([ + { + id, + name, + age, + networth, + nickname: null, + }, + ]); + }); + + it("Transaction, manual commit", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + await db.transaction(async (tx) => { + const res = await tx.execute( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id, name, age, networth], + ); + + expect(res.rowsAffected).toEqual(1); + expect(res.insertId).toEqual(1); + expect(res.rows).toDeepEqual([]); + expect(res.rows?.length).toEqual(0); + + await tx.commit(); + }); + + const res = await db.execute("SELECT * FROM User"); + // console.log(res); + expect(res.rows).toDeepEqual([ + { + id, + name, + age, + networth, + nickname: null, + }, + ]); + }); + + it("Transaction, executed in order", async () => { + const xs = 10; + const actual: unknown[] = []; + + // ARRANGE: Generate expected data + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + + // ACT: Start multiple transactions to upsert and select the same record + const promises = []; + for (let i = 1; i <= xs; i++) { + const promised = db.transaction(async (tx) => { + // ACT: Upsert statement to create record / increment the value + await tx.execute( + ` INSERT OR REPLACE INTO [User] ([id], [name], [age], [networth]) SELECT ?, ?, ?, IFNULL(( @@ -397,429 +398,427 @@ describe('Queries tests', () => { WHERE [id] = ? ), 0) `, - [id, name, age, id], - ); - - // ACT: Select statement to get incremented value and store it for checking later - const results = await tx.execute( - 'SELECT [networth] FROM [User] WHERE [id] = ?', - [id], - ); - - actual.push(results.rows[0]!.networth); - }); - - promises.push(promised); - } - - // ACT: Wait for all transactions to complete - await Promise.all(promises); - - // ASSERT: That the expected values where returned - const expected = Array(xs) - .fill(0) - .map((_, index) => index * 1000); - - expect(actual).toDeepEqual(expected); - }); - - it('Transaction, cannot execute after commit', async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - await db.transaction(async tx => { - const res = await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - expect(res.rowsAffected).toEqual(1); - expect(res.insertId).toEqual(1); - // expect(res.metadata).toEqual([]); - expect(res.rows).toDeepEqual([]); - expect(res.rows.length).toEqual(0); - - await tx.commit(); - - try { - await tx.execute('SELECT * FROM "User"'); - } catch (e) { - expect(!!e).toEqual(true); - } - }); - - const res = await db.execute('SELECT * FROM User'); - expect(res.rows).toDeepEqual([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - }); - - it('Incorrect transaction, manual rollback', async () => { - const id = chance.string(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - await db.transaction(async tx => { - try { - await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - } catch (e) { - await tx.rollback(); - } - }); - - const res = await db.execute('SELECT * FROM User'); - expect(res.rows).toDeepEqual([]); - }); - - it('Correctly throws', async () => { - const id = chance.string(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - try { - await db.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - } catch (e: any) { - expect(!!e).toEqual(true); - } - }); - - it('Rollback', async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - await db.transaction(async tx => { - await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - await tx.rollback(); - const res = await db.execute('SELECT * FROM User'); - expect(res.rows).toDeepEqual([]); - }); - }); - - it('Execute raw sync should return just an array of objects', async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - await db.execute( - 'INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - const res = db.executeRawSync('SELECT id, name, age, networth FROM User'); - expect(res).toDeepEqual([[id, name, age, networth]]); - }); - - it('Transaction, rejects on callback error', async () => { - const promised = db.transaction(() => { - throw new Error('Error from callback'); - }); - - // ASSERT: should return a promise that eventually rejects - expect(typeof promised == 'object'); - try { - await promised; - // expect.fail('Should not resolve'); - } catch (e) { - // expect(e).to.be.a.instanceof(Error); - expect((e as Error)?.message).toEqual('Error from callback'); - } - }); - - it('Transaction, rejects on invalid query', async () => { - const promised = db.transaction(async tx => { - await tx.execute('SELECT * FROM [tableThatDoesNotExist];'); - }); - - // ASSERT: should return a promise that eventually rejects - // expect(promised).to.have.property('then').that.is.a('function'); - try { - await promised; - // expect.fail('Should not resolve'); - } catch (e) { - // expect(e).to.be.a.instanceof(Error); - expect( - (e as Error)?.message.includes('no such table: tableThatDoesNotExist'), - ).toBe(true); - } - }); - - it('Transaction, handle async callback', async () => { - let ranCallback = false; - const promised = db.transaction(async tx => { - await new Promise(done => { - setTimeout(() => done(), 50); - }); - tx.execute('SELECT * FROM User;'); - ranCallback = true; - }); - - // ASSERT: should return a promise that eventually rejects - // expect(promised).to.have.property('then').that.is.a('function'); - await promised; - expect(ranCallback).toEqual(true); - }); - - it('executeBatch', async () => { - const id1 = chance.integer(); - const name1 = chance.name(); - const age1 = chance.integer(); - const networth1 = chance.floating(); - - const id2 = chance.integer(); - const name2 = chance.name(); - const age2 = chance.integer(); - const networth2 = chance.floating(); - - const commands: SQLBatchTuple[] = [ - ['SELECT * FROM "User"', []], - ['SELECT * FROM "User"'], - [ - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id1, name1, age1, networth1], - ], - [ - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [[id2, name2, age2, networth2]], - ], - ]; - - await db.executeBatch(commands); - - const res = await db.execute('SELECT * FROM User'); - - expect(res.rows).toDeepEqual([ - {id: id1, name: name1, age: age1, networth: networth1, nickname: null}, - { - id: id2, - name: name2, - age: age2, - networth: networth2, - nickname: null, - }, - ]); - }); - - it('Batch execute with BLOB', async () => { - let db = open({ - name: 'queries.sqlite', - encryptionKey: 'test', - }); - - await db.execute('DROP TABLE IF EXISTS User;'); - await db.execute( - 'CREATE TABLE IF NOT EXISTS User (id TEXT PRIMARY KEY NOT NULL, name TEXT NOT NULL, age INT, networth BLOB, nickname TEXT) STRICT;', - ); - const id1 = '1'; - const name1 = 'name1'; - const age1 = 12; - const networth1 = new Uint8Array([1, 2, 3]); - - const id2 = '2'; - const name2 = 'name2'; - const age2 = 17; - const networth2 = new Uint8Array([3, 2, 1]); - - const commands: SQLBatchTuple[] = [ - [ - 'INSERT OR REPLACE INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id1, name1, age1, networth1], - ], - [ - 'INSERT OR REPLACE INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [[id2, name2, age2, networth2]], - ], - ]; - - // bomb~ (NOBRIDGE) ERROR Error: Exception in HostFunction: - await db.executeBatch(commands); - }); - - it('DumbHostObject allows to write known props', async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - await db.execute( - 'INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - const res = await db.executeWithHostObjects('SELECT * FROM User'); - - expect(res.insertId).toEqual(1); - expect(res.rows).toDeepEqual([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - - res.rows[0]!.name = 'quack_changed'; - - expect(res.rows[0]!.name).toEqual('quack_changed'); - }); - - it('DumbHostObject allows to write new props', async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - await db.execute( - 'INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - const res = await db.executeWithHostObjects('SELECT * FROM User'); - - expect(res.rowsAffected).toEqual(1); - expect(res.insertId).toEqual(1); - expect(res.rows!).toDeepEqual([ - { - id, - name, - age, - networth, - nickname: null, - }, - ]); - - res.rows[0]!.myWeirdProp = 'quack_changed'; - - expect(res.rows[0]!.myWeirdProp).toEqual('quack_changed'); - }); - - it('Execute raw should return just an array of objects', async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - await db.execute( - 'INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - - const res = await db.executeRaw('SELECT id, name, age, networth FROM User'); - expect(res).toDeepEqual([[id, name, age, networth]]); - }); - - it('Create fts5 virtual table', async () => { - await db.execute( - 'CREATE VIRTUAL TABLE fts5_table USING fts5(name, content);', - ); - await db.execute('INSERT INTO fts5_table (name, content) VALUES(?, ?)', [ - 'test', - 'test content', - ]); - - const res = await db.execute('SELECT * FROM fts5_table'); - expect(res.rows).toDeepEqual([{name: 'test', content: 'test content'}]); - }); - - it('Various queries', async () => { - await db.execute('SELECT 1 '); - await db.execute('SELECT 1 '); - await db.execute('SELECT 1; ', []); - await db.execute('SELECT ?; ', [1]); - }); - - it('Handles concurrent transactions correctly', async () => { - const id = chance.integer(); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - const transaction1 = db.transaction(async tx => { - await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id, name, age, networth], - ); - }); - - const transaction2 = db.transaction(async tx => { - await tx.execute( - 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', - [id + 1, name, age, networth], - ); - }); - - await Promise.all([transaction1, transaction2]); - - const res = await db.execute('SELECT * FROM User'); - expect(res.rows.length).toEqual(2); - }); - - it('Pragma user_version', () => { - const res = db.executeSync('PRAGMA user_version'); - expect(res.rows).toDeepEqual([{user_version: 0}]); - }); - - -// const sqliteVecEnabled = pkg?.['op-sqlite']?.sqliteVec === true; -// if (sqliteVecEnabled) { -// it('sqlite-vec extension: vector similarity search', async () => { -// // Create a virtual table for storing vectors -// await db.execute(` -// CREATE VIRTUAL TABLE vec_items USING vec0( -// embedding FLOAT[8] -// ) -// `); - -// // Insert some sample vectors -// await db.execute(` -// INSERT INTO vec_items(rowid, embedding) -// VALUES -// (1, '[-0.200, 0.250, 0.341, -0.211, 0.645, 0.935, -0.316, -0.924]'), -// (2, '[0.443, -0.501, 0.355, -0.771, 0.707, -0.708, -0.185, 0.362]'), -// (3, '[0.716, -0.927, 0.134, 0.052, -0.669, 0.793, -0.634, -0.162]'), -// (4, '[-0.710, 0.330, 0.656, 0.041, -0.990, 0.726, 0.385, -0.958]') -// `); - -// // Perform KNN query to find the 2 nearest neighbors -// const queryVector = '[0.890, 0.544, 0.825, 0.961, 0.358, 0.0196, 0.521, 0.175]'; -// const result = await db.execute(` -// SELECT rowid, distance -// FROM vec_items -// WHERE embedding MATCH ? -// ORDER BY distance -// LIMIT 2 -// `, [queryVector]); - -// // Verify results -// expect(result.rows.length).toEqual(2); -// expect(result.rows[0]!.rowid).toEqual(2); -// expect(result.rows[1]!.rowid).toEqual(1); - -// // Verify distances are positive numbers -// const distance0 = result.rows[0]!.distance as number; -// const distance1 = result.rows[1]!.distance as number; -// expect(typeof distance0).toEqual('number'); -// expect(distance0 > 0).toBeTruthy(); -// expect(distance1 > 0).toBeTruthy(); -// }); -// } - + [id, name, age, id], + ); + + // ACT: Select statement to get incremented value and store it for checking later + const results = await tx.execute( + "SELECT [networth] FROM [User] WHERE [id] = ?", + [id], + ); + + actual.push(results.rows[0]!.networth); + }); + + promises.push(promised); + } + + // ACT: Wait for all transactions to complete + await Promise.all(promises); + + // ASSERT: That the expected values where returned + const expected = Array(xs) + .fill(0) + .map((_, index) => index * 1000); + + expect(actual).toDeepEqual(expected); + }); + + it("Transaction, cannot execute after commit", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + await db.transaction(async (tx) => { + const res = await tx.execute( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id, name, age, networth], + ); + + expect(res.rowsAffected).toEqual(1); + expect(res.insertId).toEqual(1); + // expect(res.metadata).toEqual([]); + expect(res.rows).toDeepEqual([]); + expect(res.rows.length).toEqual(0); + + await tx.commit(); + + try { + await tx.execute('SELECT * FROM "User"'); + } catch (e) { + expect(!!e).toEqual(true); + } + }); + + const res = await db.execute("SELECT * FROM User"); + expect(res.rows).toDeepEqual([ + { + id, + name, + age, + networth, + nickname: null, + }, + ]); + }); + + it("Incorrect transaction, manual rollback", async () => { + const id = chance.string(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + await db.transaction(async (tx) => { + try { + await tx.execute( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id, name, age, networth], + ); + } catch (_e) { + await tx.rollback(); + } + }); + + const res = await db.execute("SELECT * FROM User"); + expect(res.rows).toDeepEqual([]); + }); + + it("Correctly throws", async () => { + const id = chance.string(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + try { + await db.execute( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id, name, age, networth], + ); + } catch (e: any) { + expect(!!e).toEqual(true); + } + }); + + it("Rollback", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + await db.transaction(async (tx) => { + await tx.execute( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id, name, age, networth], + ); + await tx.rollback(); + const res = await db.execute("SELECT * FROM User"); + expect(res.rows).toDeepEqual([]); + }); + }); + + it("Execute raw sync should return just an array of objects", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + await db.execute( + "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", + [id, name, age, networth], + ); + + const res = db.executeRawSync("SELECT id, name, age, networth FROM User"); + expect(res).toDeepEqual([[id, name, age, networth]]); + }); + + it("Transaction, rejects on callback error", async () => { + const promised = db.transaction(() => { + throw new Error("Error from callback"); + }); + + // ASSERT: should return a promise that eventually rejects + expect(typeof promised === "object"); + try { + await promised; + // expect.fail('Should not resolve'); + } catch (e) { + // expect(e).to.be.a.instanceof(Error); + expect((e as Error)?.message).toEqual("Error from callback"); + } + }); + + it("Transaction, rejects on invalid query", async () => { + const promised = db.transaction(async (tx) => { + await tx.execute("SELECT * FROM [tableThatDoesNotExist];"); + }); + + // ASSERT: should return a promise that eventually rejects + // expect(promised).to.have.property('then').that.is.a('function'); + try { + await promised; + // expect.fail('Should not resolve'); + } catch (e) { + // expect(e).to.be.a.instanceof(Error); + expect( + (e as Error)?.message.includes("no such table: tableThatDoesNotExist"), + ).toBe(true); + } + }); + + it("Transaction, handle async callback", async () => { + let ranCallback = false; + const promised = db.transaction(async (tx) => { + await new Promise((done) => { + setTimeout(() => done(), 50); + }); + tx.execute("SELECT * FROM User;"); + ranCallback = true; + }); + + // ASSERT: should return a promise that eventually rejects + // expect(promised).to.have.property('then').that.is.a('function'); + await promised; + expect(ranCallback).toEqual(true); + }); + + it("executeBatch", async () => { + const id1 = chance.integer(); + const name1 = chance.name(); + const age1 = chance.integer(); + const networth1 = chance.floating(); + + const id2 = chance.integer(); + const name2 = chance.name(); + const age2 = chance.integer(); + const networth2 = chance.floating(); + + const commands: SQLBatchTuple[] = [ + ['SELECT * FROM "User"', []], + ['SELECT * FROM "User"'], + [ + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id1, name1, age1, networth1], + ], + [ + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [[id2, name2, age2, networth2]], + ], + ]; + + await db.executeBatch(commands); + + const res = await db.execute("SELECT * FROM User"); + + expect(res.rows).toDeepEqual([ + { id: id1, name: name1, age: age1, networth: networth1, nickname: null }, + { + id: id2, + name: name2, + age: age2, + networth: networth2, + nickname: null, + }, + ]); + }); + + it("Batch execute with BLOB", async () => { + const db = open({ + name: "queries.sqlite", + encryptionKey: "test", + }); + + await db.execute("DROP TABLE IF EXISTS User;"); + await db.execute( + "CREATE TABLE IF NOT EXISTS User (id TEXT PRIMARY KEY NOT NULL, name TEXT NOT NULL, age INT, networth BLOB, nickname TEXT) STRICT;", + ); + const id1 = "1"; + const name1 = "name1"; + const age1 = 12; + const networth1 = new Uint8Array([1, 2, 3]); + + const id2 = "2"; + const name2 = "name2"; + const age2 = 17; + const networth2 = new Uint8Array([3, 2, 1]); + + const commands: SQLBatchTuple[] = [ + [ + 'INSERT OR REPLACE INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id1, name1, age1, networth1], + ], + [ + 'INSERT OR REPLACE INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [[id2, name2, age2, networth2]], + ], + ]; + + // bomb~ (NOBRIDGE) ERROR Error: Exception in HostFunction: + await db.executeBatch(commands); + }); + + it("DumbHostObject allows to write known props", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + await db.execute( + "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", + [id, name, age, networth], + ); + + const res = await db.executeWithHostObjects("SELECT * FROM User"); + + expect(res.insertId).toEqual(1); + expect(res.rows).toDeepEqual([ + { + id, + name, + age, + networth, + nickname: null, + }, + ]); + + res.rows[0]!.name = "quack_changed"; + + expect(res.rows[0]!.name).toEqual("quack_changed"); + }); + + it("DumbHostObject allows to write new props", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + await db.execute( + "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", + [id, name, age, networth], + ); + + const res = await db.executeWithHostObjects("SELECT * FROM User"); + + expect(res.rowsAffected).toEqual(1); + expect(res.insertId).toEqual(1); + expect(res.rows!).toDeepEqual([ + { + id, + name, + age, + networth, + nickname: null, + }, + ]); + + res.rows[0]!.myWeirdProp = "quack_changed"; + + expect(res.rows[0]!.myWeirdProp).toEqual("quack_changed"); + }); + + it("Execute raw should return just an array of objects", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + await db.execute( + "INSERT INTO User (id, name, age, networth) VALUES(?, ?, ?, ?)", + [id, name, age, networth], + ); + + const res = await db.executeRaw("SELECT id, name, age, networth FROM User"); + expect(res).toDeepEqual([[id, name, age, networth]]); + }); + + it("Create fts5 virtual table", async () => { + await db.execute( + "CREATE VIRTUAL TABLE fts5_table USING fts5(name, content);", + ); + await db.execute("INSERT INTO fts5_table (name, content) VALUES(?, ?)", [ + "test", + "test content", + ]); + + const res = await db.execute("SELECT * FROM fts5_table"); + expect(res.rows).toDeepEqual([{ name: "test", content: "test content" }]); + }); + + it("Various queries", async () => { + await db.execute("SELECT 1 "); + await db.execute("SELECT 1 "); + await db.execute("SELECT 1; ", []); + await db.execute("SELECT ?; ", [1]); + }); + + it("Handles concurrent transactions correctly", async () => { + const id = chance.integer(); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + const transaction1 = db.transaction(async (tx) => { + await tx.execute( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id, name, age, networth], + ); + }); + + const transaction2 = db.transaction(async (tx) => { + await tx.execute( + 'INSERT INTO "User" (id, name, age, networth) VALUES(?, ?, ?, ?)', + [id + 1, name, age, networth], + ); + }); + + await Promise.all([transaction1, transaction2]); + + const res = await db.execute("SELECT * FROM User"); + expect(res.rows.length).toEqual(2); + }); + + it("Pragma user_version", () => { + const res = db.executeSync("PRAGMA user_version"); + expect(res.rows).toDeepEqual([{ user_version: 0 }]); + }); + + // const sqliteVecEnabled = pkg?.['op-sqlite']?.sqliteVec === true; + // if (sqliteVecEnabled) { + // it('sqlite-vec extension: vector similarity search', async () => { + // // Create a virtual table for storing vectors + // await db.execute(` + // CREATE VIRTUAL TABLE vec_items USING vec0( + // embedding FLOAT[8] + // ) + // `); + + // // Insert some sample vectors + // await db.execute(` + // INSERT INTO vec_items(rowid, embedding) + // VALUES + // (1, '[-0.200, 0.250, 0.341, -0.211, 0.645, 0.935, -0.316, -0.924]'), + // (2, '[0.443, -0.501, 0.355, -0.771, 0.707, -0.708, -0.185, 0.362]'), + // (3, '[0.716, -0.927, 0.134, 0.052, -0.669, 0.793, -0.634, -0.162]'), + // (4, '[-0.710, 0.330, 0.656, 0.041, -0.990, 0.726, 0.385, -0.958]') + // `); + + // // Perform KNN query to find the 2 nearest neighbors + // const queryVector = '[0.890, 0.544, 0.825, 0.961, 0.358, 0.0196, 0.521, 0.175]'; + // const result = await db.execute(` + // SELECT rowid, distance + // FROM vec_items + // WHERE embedding MATCH ? + // ORDER BY distance + // LIMIT 2 + // `, [queryVector]); + + // // Verify results + // expect(result.rows.length).toEqual(2); + // expect(result.rows[0]!.rowid).toEqual(2); + // expect(result.rows[1]!.rowid).toEqual(1); + + // // Verify distances are positive numbers + // const distance0 = result.rows[0]!.distance as number; + // const distance1 = result.rows[1]!.distance as number; + // expect(typeof distance0).toEqual('number'); + // expect(distance0 > 0).toBeTruthy(); + // expect(distance1 > 0).toBeTruthy(); + // }); + // } }); diff --git a/example/src/tests/reactive.ts b/example/src/tests/reactive.ts index 1804ca07..aeedbc49 100644 --- a/example/src/tests/reactive.ts +++ b/example/src/tests/reactive.ts @@ -1,329 +1,329 @@ -import {isLibsql, open, type DB} from '@op-engineering/op-sqlite'; +import { type DB, isLibsql, open } from "@op-engineering/op-sqlite"; import { - afterAll, - beforeEach, - describe, - it, - expect, -} from '@op-engineering/op-test'; -import {sleep} from './utils'; -import Chance from 'chance'; + afterAll, + beforeEach, + describe, + expect, + it, +} from "@op-engineering/op-test"; +import Chance from "chance"; +import { sleep } from "./utils"; const chance = new Chance(); -describe('Reactive queries', () => { - let db: DB; - beforeEach(async () => { - if (db) { - db.close(); - db.delete(); - } - - db = open({ - name: 'reactive.sqlite', - encryptionKey: 'test', - }); - - await db.execute('DROP TABLE IF EXISTS User;'); - await db.execute( - 'CREATE TABLE User ( id INT PRIMARY KEY, name TEXT NOT NULL, age INT, networth REAL, nickname TEXT) STRICT;', - ); - }); - - afterAll(() => { - if (db) { - db.close(); - db.delete(); - // @ts-ignore - db = null; - } - }); - // libsql does not support reactive queries - if (isLibsql()) { - return; - } - - it('Table reactive query', async () => { - let fullSelectRan = false; - let emittedUser = null; - const unsubscribe = db.reactiveExecute({ - query: 'SELECT * FROM User;', - arguments: [], - fireOn: [ - { - table: 'User', - }, - ], - callback: _data => { - fullSelectRan = true; - }, - }); - - const unsubscribe2 = db.reactiveExecute({ - query: 'SELECT name FROM User;', - arguments: [], - fireOn: [ - { - table: 'User', - }, - ], - callback: data => { - emittedUser = data.rows[0]; - }, - }); - - await db.transaction(async tx => { - await tx.execute( - 'INSERT INTO User (id, name, age, networth, nickname) VALUES (?, ?, ?, ?, ?);', - [1, 'John', 30, 1000, 'Johnny'], - ); - }); - - await sleep(20); - - await db.transaction(async tx => { - await tx.execute('UPDATE User SET name = ? WHERE id = ?;', ['Foo', 1]); - }); - - await sleep(20); - - expect(!!fullSelectRan).toBe(true); - expect(emittedUser).toDeepEqual({ - name: 'Foo', - }); - - unsubscribe(); - unsubscribe2(); - }); - - it('Can unsubscribe from reactive query', async () => { - let emittedCount = 0; - const unsubscribe = db.reactiveExecute({ - query: 'SELECT * FROM User;', - arguments: [], - fireOn: [ - { - table: 'User', - }, - ], - callback: () => { - emittedCount++; - }, - }); - - // expect(unsubscribe).to.be.a('function'); - - await db.transaction(async tx => { - await tx.execute( - 'INSERT INTO User (id, name, age, networth, nickname) VALUES (?, ?, ?, ?, ?);', - [1, 'John', 30, 1000, 'Johnny'], - ); - }); - - await sleep(20); - - unsubscribe(); - - await db.transaction(async tx => { - await tx.execute('UPDATE User SET name = ? WHERE id = ?;', ['Foo', 1]); - }); - await sleep(20); - expect(emittedCount).toEqual(1); - }); - - it('Row reactive query', async () => { - let firstReactiveRan = false; - let secondReactiveRan = false; - let emittedUser = null; - - const unsubscribe = db.reactiveExecute({ - query: 'SELECT * FROM User;', - arguments: [], - fireOn: [ - { - table: 'User', - ids: [2], - }, - ], - callback: () => { - firstReactiveRan = true; - }, - }); - - const unsubscribe2 = db.reactiveExecute({ - query: 'SELECT * FROM User;', - arguments: [], - fireOn: [ - { - table: 'Foo', - }, - ], - callback: () => { - secondReactiveRan = true; - }, - }); - - const unsubscribe3 = db.reactiveExecute({ - query: 'SELECT name FROM User;', - arguments: [], - fireOn: [ - { - table: 'User', - ids: [1], - }, - ], - callback: data => { - emittedUser = data.rows[0]; - }, - }); - - await db.transaction(async tx => { - await tx.execute( - 'INSERT INTO User (id, name, age, networth, nickname) VALUES (?, ?, ?, ?, ?);', - [1, 'John', 30, 1000, 'Johnny'], - ); - }); - - await sleep(0); - - await db.transaction(async tx => { - await tx.execute('UPDATE User SET name = ? WHERE id = ?;', ['Foo', 1]); - }); - - await sleep(0); - - expect(!!firstReactiveRan).toBe(false); - expect(!!secondReactiveRan).toBe(false); - expect(emittedUser).toDeepEqual({ - name: 'Foo', - }); - unsubscribe(); - unsubscribe2(); - unsubscribe3(); - }); - - it('Row reactive query with executeBatch', async () => { - let firstReactiveRan = false; - let secondReactiveRan = false; - let emittedUser = null; - - const unsubscribe = db.reactiveExecute({ - query: 'SELECT * FROM User;', - arguments: [], - fireOn: [ - { - table: 'User', - ids: [2], - }, - ], - callback: () => { - firstReactiveRan = true; - }, - }); - - const unsubscribe2 = db.reactiveExecute({ - query: 'SELECT * FROM User;', - arguments: [], - fireOn: [ - { - table: 'Foo', - }, - ], - callback: () => { - secondReactiveRan = true; - }, - }); - - const unsubscribe3 = db.reactiveExecute({ - query: 'SELECT name FROM User;', - arguments: [], - fireOn: [ - { - table: 'User', - ids: [1], - }, - ], - callback: data => { - emittedUser = data.rows[0]; - }, - }); - - await db.executeBatch([ - [ - 'INSERT INTO User (id, name, age, networth, nickname) VALUES (?, ?, ?, ?, ?);', - [1, 'John', 30, 1000, 'Johnny'], - ], - ]); - - await sleep(0); - - await db.transaction(async tx => { - await tx.execute('UPDATE User SET name = ? WHERE id = ?;', ['Foo', 1]); - }); - - await sleep(0); - - expect(!!firstReactiveRan).toBe(false); - expect(!!secondReactiveRan).toBe(false); - expect(emittedUser).toDeepEqual({ - name: 'Foo', - }); - unsubscribe(); - unsubscribe2(); - unsubscribe3(); - }); - - it('Update hook and reactive queries work at the same time', async () => { - let promiseResolve: any; - let promise = new Promise(resolve => { - promiseResolve = resolve; - }); - db.updateHook(({operation}) => { - promiseResolve?.(operation); - }); - - let emittedUser = null; - const unsubscribe = db.reactiveExecute({ - query: 'SELECT * FROM User;', - arguments: [], - fireOn: [ - { - table: 'User', - }, - ], - callback: data => { - emittedUser = data.rows[0]; - }, - }); - - const id = chance.integer({ - min: 1, - max: 100000, - }); - const name = chance.name(); - const age = chance.integer(); - const networth = chance.floating(); - - await db.transaction(async tx => { - await tx.execute( - 'INSERT INTO User (id, name, age, networth, nickname) VALUES (?, ?, ?, ?, ?);', - [id, name, age, networth, 'Johnny'], - ); - }); - - const operation = await promise; - - await sleep(0); - - expect(operation).toEqual('INSERT'); - expect(emittedUser).toDeepEqual({ - id, - name, - age, - networth, - nickname: 'Johnny', - }); - - unsubscribe(); - }); +describe("Reactive queries", () => { + let db: DB; + beforeEach(async () => { + if (db) { + db.close(); + db.delete(); + } + + db = open({ + name: "reactive.sqlite", + encryptionKey: "test", + }); + + await db.execute("DROP TABLE IF EXISTS User;"); + await db.execute( + "CREATE TABLE User ( id INT PRIMARY KEY, name TEXT NOT NULL, age INT, networth REAL, nickname TEXT) STRICT;", + ); + }); + + afterAll(() => { + if (db) { + db.close(); + db.delete(); + // @ts-expect-error + db = null; + } + }); + // libsql does not support reactive queries + if (isLibsql()) { + return; + } + + it("Table reactive query", async () => { + let fullSelectRan = false; + let emittedUser = null; + const unsubscribe = db.reactiveExecute({ + query: "SELECT * FROM User;", + arguments: [], + fireOn: [ + { + table: "User", + }, + ], + callback: (_data) => { + fullSelectRan = true; + }, + }); + + const unsubscribe2 = db.reactiveExecute({ + query: "SELECT name FROM User;", + arguments: [], + fireOn: [ + { + table: "User", + }, + ], + callback: (data) => { + emittedUser = data.rows[0]; + }, + }); + + await db.transaction(async (tx) => { + await tx.execute( + "INSERT INTO User (id, name, age, networth, nickname) VALUES (?, ?, ?, ?, ?);", + [1, "John", 30, 1000, "Johnny"], + ); + }); + + await sleep(20); + + await db.transaction(async (tx) => { + await tx.execute("UPDATE User SET name = ? WHERE id = ?;", ["Foo", 1]); + }); + + await sleep(20); + + expect(!!fullSelectRan).toBe(true); + expect(emittedUser).toDeepEqual({ + name: "Foo", + }); + + unsubscribe(); + unsubscribe2(); + }); + + it("Can unsubscribe from reactive query", async () => { + let emittedCount = 0; + const unsubscribe = db.reactiveExecute({ + query: "SELECT * FROM User;", + arguments: [], + fireOn: [ + { + table: "User", + }, + ], + callback: () => { + emittedCount++; + }, + }); + + // expect(unsubscribe).to.be.a('function'); + + await db.transaction(async (tx) => { + await tx.execute( + "INSERT INTO User (id, name, age, networth, nickname) VALUES (?, ?, ?, ?, ?);", + [1, "John", 30, 1000, "Johnny"], + ); + }); + + await sleep(20); + + unsubscribe(); + + await db.transaction(async (tx) => { + await tx.execute("UPDATE User SET name = ? WHERE id = ?;", ["Foo", 1]); + }); + await sleep(20); + expect(emittedCount).toEqual(1); + }); + + it("Row reactive query", async () => { + let firstReactiveRan = false; + let secondReactiveRan = false; + let emittedUser = null; + + const unsubscribe = db.reactiveExecute({ + query: "SELECT * FROM User;", + arguments: [], + fireOn: [ + { + table: "User", + ids: [2], + }, + ], + callback: () => { + firstReactiveRan = true; + }, + }); + + const unsubscribe2 = db.reactiveExecute({ + query: "SELECT * FROM User;", + arguments: [], + fireOn: [ + { + table: "Foo", + }, + ], + callback: () => { + secondReactiveRan = true; + }, + }); + + const unsubscribe3 = db.reactiveExecute({ + query: "SELECT name FROM User;", + arguments: [], + fireOn: [ + { + table: "User", + ids: [1], + }, + ], + callback: (data) => { + emittedUser = data.rows[0]; + }, + }); + + await db.transaction(async (tx) => { + await tx.execute( + "INSERT INTO User (id, name, age, networth, nickname) VALUES (?, ?, ?, ?, ?);", + [1, "John", 30, 1000, "Johnny"], + ); + }); + + await sleep(0); + + await db.transaction(async (tx) => { + await tx.execute("UPDATE User SET name = ? WHERE id = ?;", ["Foo", 1]); + }); + + await sleep(0); + + expect(!!firstReactiveRan).toBe(false); + expect(!!secondReactiveRan).toBe(false); + expect(emittedUser).toDeepEqual({ + name: "Foo", + }); + unsubscribe(); + unsubscribe2(); + unsubscribe3(); + }); + + it("Row reactive query with executeBatch", async () => { + let firstReactiveRan = false; + let secondReactiveRan = false; + let emittedUser = null; + + const unsubscribe = db.reactiveExecute({ + query: "SELECT * FROM User;", + arguments: [], + fireOn: [ + { + table: "User", + ids: [2], + }, + ], + callback: () => { + firstReactiveRan = true; + }, + }); + + const unsubscribe2 = db.reactiveExecute({ + query: "SELECT * FROM User;", + arguments: [], + fireOn: [ + { + table: "Foo", + }, + ], + callback: () => { + secondReactiveRan = true; + }, + }); + + const unsubscribe3 = db.reactiveExecute({ + query: "SELECT name FROM User;", + arguments: [], + fireOn: [ + { + table: "User", + ids: [1], + }, + ], + callback: (data) => { + emittedUser = data.rows[0]; + }, + }); + + await db.executeBatch([ + [ + "INSERT INTO User (id, name, age, networth, nickname) VALUES (?, ?, ?, ?, ?);", + [1, "John", 30, 1000, "Johnny"], + ], + ]); + + await sleep(0); + + await db.transaction(async (tx) => { + await tx.execute("UPDATE User SET name = ? WHERE id = ?;", ["Foo", 1]); + }); + + await sleep(0); + + expect(!!firstReactiveRan).toBe(false); + expect(!!secondReactiveRan).toBe(false); + expect(emittedUser).toDeepEqual({ + name: "Foo", + }); + unsubscribe(); + unsubscribe2(); + unsubscribe3(); + }); + + it("Update hook and reactive queries work at the same time", async () => { + let promiseResolve: any; + const promise = new Promise((resolve) => { + promiseResolve = resolve; + }); + db.updateHook(({ operation }) => { + promiseResolve?.(operation); + }); + + let emittedUser = null; + const unsubscribe = db.reactiveExecute({ + query: "SELECT * FROM User;", + arguments: [], + fireOn: [ + { + table: "User", + }, + ], + callback: (data) => { + emittedUser = data.rows[0]; + }, + }); + + const id = chance.integer({ + min: 1, + max: 100000, + }); + const name = chance.name(); + const age = chance.integer(); + const networth = chance.floating(); + + await db.transaction(async (tx) => { + await tx.execute( + "INSERT INTO User (id, name, age, networth, nickname) VALUES (?, ?, ?, ?, ?);", + [id, name, age, networth, "Johnny"], + ); + }); + + const operation = await promise; + + await sleep(0); + + expect(operation).toEqual("INSERT"); + expect(emittedUser).toDeepEqual({ + id, + name, + age, + networth, + nickname: "Johnny", + }); + + unsubscribe(); + }); }); diff --git a/example/src/tests/storage.ts b/example/src/tests/storage.ts index e6542b36..086218bd 100644 --- a/example/src/tests/storage.ts +++ b/example/src/tests/storage.ts @@ -1,68 +1,68 @@ -import {Storage} from '@op-engineering/op-sqlite'; +import { Storage } from "@op-engineering/op-sqlite"; import { - beforeEach, - describe, - it, - afterEach, - expect, -} from '@op-engineering/op-test'; + afterEach, + beforeEach, + describe, + expect, + it, +} from "@op-engineering/op-test"; -describe('Storage', () => { - let storage: Storage; - beforeEach(() => { - storage = new Storage({encryptionKey: 'test'}); - }); +describe("Storage", () => { + let storage: Storage; + beforeEach(() => { + storage = new Storage({ encryptionKey: "test" }); + }); - afterEach(() => { - storage.delete(); - }); + afterEach(() => { + storage.delete(); + }); - it('Can set and get sync', async () => { - storage.setItemSync('foo', 'bar'); - const res = storage.getItemSync('foo'); - expect(res).toEqual('bar'); - }); + it("Can set and get sync", async () => { + storage.setItemSync("foo", "bar"); + const res = storage.getItemSync("foo"); + expect(res).toEqual("bar"); + }); - it('Can set and get async', async () => { - await storage.setItem('quack', 'bark'); - const res = await storage.getItem('quack'); - expect(res).toEqual('bark'); - }); + it("Can set and get async", async () => { + await storage.setItem("quack", "bark"); + const res = await storage.getItem("quack"); + expect(res).toEqual("bark"); + }); - it('can remove item sync', async () => { - storage.setItemSync('foo', 'bar'); - storage.removeItemSync('foo'); - const res = storage.getItemSync('foo'); - expect(res).toEqual(undefined); - }); + it("can remove item sync", async () => { + storage.setItemSync("foo", "bar"); + storage.removeItemSync("foo"); + const res = storage.getItemSync("foo"); + expect(res).toEqual(undefined); + }); - it('can remove item async', async () => { - await storage.setItem('quack', 'bark'); - await storage.removeItem('quack'); - const res = await storage.getItem('quack'); - expect(res).toEqual(undefined); - }); + it("can remove item async", async () => { + await storage.setItem("quack", "bark"); + await storage.removeItem("quack"); + const res = await storage.getItem("quack"); + expect(res).toEqual(undefined); + }); - it('can clear', async () => { - await storage.setItem('quack', 'bark'); - await storage.setItem('quack2', 'bark'); - await storage.clear(); - const res = await storage.getItem('quack'); - expect(res).toEqual(undefined); - }); + it("can clear", async () => { + await storage.setItem("quack", "bark"); + await storage.setItem("quack2", "bark"); + await storage.clear(); + const res = await storage.getItem("quack"); + expect(res).toEqual(undefined); + }); - it('can clear sync', async () => { - storage.setItemSync('quack', 'bark'); - storage.setItemSync('quack2', 'bark'); - storage.clearSync(); - const res = storage.getItemSync('quack'); - expect(res).toEqual(undefined); - }); + it("can clear sync", async () => { + storage.setItemSync("quack", "bark"); + storage.setItemSync("quack2", "bark"); + storage.clearSync(); + const res = storage.getItemSync("quack"); + expect(res).toEqual(undefined); + }); - it('can get all keys', async () => { - await storage.setItem('quack', 'bark'); - await storage.setItem('quack2', 'bark'); - const keys = storage.getAllKeys(); - expect(keys).toDeepEqual(['quack', 'quack2']); - }); + it("can get all keys", async () => { + await storage.setItem("quack", "bark"); + await storage.setItem("quack2", "bark"); + const keys = storage.getAllKeys(); + expect(keys).toDeepEqual(["quack", "quack2"]); + }); }); diff --git a/example/src/tests/utils.ts b/example/src/tests/utils.ts index df4b169e..cf5dd279 100644 --- a/example/src/tests/utils.ts +++ b/example/src/tests/utils.ts @@ -1,8 +1,8 @@ -import Chance from 'chance'; +import Chance from "chance"; export async function sleep(ms: number): Promise { - return new Promise(resolve => { - setTimeout(resolve, ms); - }); + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); } export const chance = new Chance(); diff --git a/example/src/tests/web.ts b/example/src/tests/web.ts new file mode 100644 index 00000000..892271a8 --- /dev/null +++ b/example/src/tests/web.ts @@ -0,0 +1,86 @@ +import { type DB, open, openAsync } from "@op-engineering/op-sqlite"; +import { describe, expect, it } from "@op-engineering/op-test"; +import { Platform } from "react-native"; + +describe("Web backend", () => { + if (Platform.OS !== "web") { + return; + } + + it("opens with openAsync and executes queries", async () => { + const db = await openAsync({ + name: "web-tests.sqlite", + }); + + await db.execute( + "CREATE TABLE IF NOT EXISTS WebTests (id INTEGER PRIMARY KEY, value TEXT NOT NULL)", + ); + await db.execute("DELETE FROM WebTests"); + await db.execute("INSERT INTO WebTests (id, value) VALUES (?, ?)", [ + 1, + "ok", + ]); + + const result = await db.execute( + "SELECT value FROM WebTests WHERE id = ?", + [1], + ); + + expect(result.rows[0]?.value).toEqual("ok"); + + await db.closeAsync(); + }); + + it("open() throws on web", () => { + let didThrow = false; + + try { + open({ + name: "web-tests.sqlite", + }); + } catch (error) { + didThrow = true; + expect((error as Error).message.includes("async-only")).toEqual(true); + } + + expect(didThrow).toEqual(true); + }); + + it("executeSync() throws on web", async () => { + let db: DB | null = null; + + try { + db = await openAsync({ + name: "web-tests.sqlite", + }); + + let didThrow = false; + + try { + db.executeSync("SELECT 1"); + } catch (_error) { + didThrow = true; + } + + expect(didThrow).toEqual(true); + } finally { + await db?.closeAsync(); + } + }); + + it("rejects SQLCipher options on web", async () => { + let didThrow = false; + + try { + await openAsync({ + name: "web-tests-encrypted.sqlite", + encryptionKey: "not-supported-on-web", + }); + } catch (error) { + didThrow = true; + expect((error as Error).message.includes("SQLCipher")).toEqual(true); + } + + expect(didThrow).toEqual(true); + }); +}); diff --git a/example/vite.config.ts b/example/vite.config.ts new file mode 100644 index 00000000..95fde73c --- /dev/null +++ b/example/vite.config.ts @@ -0,0 +1,34 @@ +import path from 'path'; +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +const rootDir = __dirname; +const packageRoot = path.resolve(rootDir, '..'); + +export default defineConfig({ + root: path.resolve(rootDir, 'web'), + plugins: [react()], + resolve: { + alias: { + 'react-native': 'react-native-web', + '@op-engineering/op-sqlite': path.resolve(packageRoot, 'src/index.web.ts'), + }, + extensions: ['.web.tsx', '.web.ts', '.tsx', '.ts', '.jsx', '.js', '.mjs', '.json'], + }, + server: { + headers: { + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + }, + fs: { + allow: [packageRoot], + }, + }, + optimizeDeps: { + exclude: ['@op-engineering/op-sqlite', '@sqlite.org/sqlite-wasm'], + }, + build: { + outDir: path.resolve(rootDir, 'web-build'), + emptyOutDir: true, + }, +}); diff --git a/example/web/index.html b/example/web/index.html new file mode 100644 index 00000000..bc4195fc --- /dev/null +++ b/example/web/index.html @@ -0,0 +1,28 @@ + + + + + + + OP-SQLite Web Example + + + + +
+ + + + \ No newline at end of file diff --git a/example/web/index.web.tsx b/example/web/index.web.tsx new file mode 100644 index 00000000..68110419 --- /dev/null +++ b/example/web/index.web.tsx @@ -0,0 +1 @@ +import '../index.web'; diff --git a/package.json b/package.json index 2e241001..de8fc603 100644 --- a/package.json +++ b/package.json @@ -1,125 +1,133 @@ { - "name": "@op-engineering/op-sqlite", - "version": "0.0.0", - "description": "Fastest SQLite for React Native (with node.js support)", - "main": "./lib/module/index.js", - "types": "./lib/typescript/src/index.d.ts", - "react-native": "src/index", - "exports": { - ".": { - "source": "./src/index.ts", - "node": "./node/dist/index.js", - "types": "./lib/typescript/src/index.d.ts", - "default": "./lib/module/index.js" - }, - "./package.json": "./package.json" - }, - "files": [ - "src", - "lib", - "android", - "ios", - "cpp", - "node/dist", - "node/package.json", - "*.podspec", - "*.rb", - "react-native.config.js", - "ios/**.xcframework", - "!ios/build", - "!android/build", - "!android/gradle", - "!android/gradlew", - "!android/gradlew.bat", - "!android/local.properties", - "!**/__tests__", - "!**/__fixtures__", - "!**/__mocks__", - "!**/.*" - ], - "scripts": { - "test:node": "yarn workspace node test", - "example": "yarn workspace op_sqlite_example", - "typecheck": "tsc", - "prepare": "bob build && yarn build:node", - "build:node": "yarn workspace node build", - "pods": "cd example && yarn pods", - "clang-format-check": "clang-format -i cpp/*.cpp cpp/*.h" - }, - "keywords": [ - "react-native", - "ios", - "android", - "node", - "sqlite", - "database" - ], - "repository": "https://github.com/OP-Engineering/op-sqlite", - "author": "Oscar Franco (https://github.com/ospfranco)", - "license": "MIT", - "bugs": { - "url": "https://github.com/OP-Engineering/op-sqlite/issues" - }, - "homepage": "https://github.com/OP-Engineering/op-sqlite#readme", - "publishConfig": { - "registry": "https://registry.npmjs.org/" - }, - "devDependencies": { - "@types/better-sqlite3": "^7.6.13", - "@types/jest": "^30.0.0", - "better-sqlite3": "^12.5.0", - "clang-format": "^1.8.0", - "jest": "^29.5.0", - "react": "19.1.1", - "react-native": "0.82.1", - "react-native-builder-bob": "^0.40.15", - "typescript": "^5.9.2" - }, - "peerDependencies": { - "react": "*", - "react-native": "*" - }, - "workspaces": [ - "example", - "node" - ], - "prettier": { - "quoteProps": "consistent", - "singleQuote": true, - "tabWidth": 2, - "trailingComma": "es5", - "useTabs": false - }, - "react-native-builder-bob": { - "source": "src", - "output": "lib", - "targets": [ - [ - "module", - { - "esm": true - } - ], - [ - "typescript", - { - "project": "tsconfig.build.json" - } - ] - ] - }, - "codegenConfig": { - "name": "OPSQLiteSpec", - "type": "modules", - "jsSrcsDir": "src", - "android": { - "javaPackageName": "com.op.sqlite" - } - }, - "create-react-native-library": { - "languages": "kotlin-objc", - "type": "turbo-module", - "version": "0.52.1" - }, - "packageManager": "yarn@4.11.0" + "name": "@op-engineering/op-sqlite", + "version": "0.0.0", + "description": "Fastest SQLite for React Native (with node.js support)", + "main": "./lib/module/index.js", + "types": "./lib/typescript/src/index.d.ts", + "react-native": "src/index", + "exports": { + ".": { + "source": "./src/index.ts", + "node": "./node/dist/index.js", + "types": "./lib/typescript/src/index.d.ts", + "default": "./lib/module/index.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "src", + "lib", + "android", + "ios", + "cpp", + "node/dist", + "node/package.json", + "*.podspec", + "*.rb", + "react-native.config.js", + "ios/**.xcframework", + "!ios/build", + "!android/build", + "!android/gradle", + "!android/gradlew", + "!android/gradlew.bat", + "!android/local.properties", + "!**/__tests__", + "!**/__fixtures__", + "!**/__mocks__", + "!**/.*" + ], + "scripts": { + "test:node": "yarn workspace node test", + "example": "yarn workspace op_sqlite_example", + "typecheck": "tsc", + "prepare": "bob build && yarn build:node", + "build:node": "yarn workspace node build", + "pods": "cd example && yarn pods", + "clang-format-check": "clang-format -i cpp/*.cpp cpp/*.h" + }, + "keywords": [ + "react-native", + "ios", + "android", + "node", + "sqlite", + "database" + ], + "repository": "https://github.com/OP-Engineering/op-sqlite", + "author": "Oscar Franco (https://github.com/ospfranco)", + "license": "MIT", + "bugs": { + "url": "https://github.com/OP-Engineering/op-sqlite/issues" + }, + "homepage": "https://github.com/OP-Engineering/op-sqlite#readme", + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, + "devDependencies": { + "@biomejs/biome": "^2.4.10", + "@sqlite.org/sqlite-wasm": "^3.51.2-build8", + "@types/better-sqlite3": "^7.6.13", + "@types/jest": "^30.0.0", + "better-sqlite3": "^12.5.0", + "clang-format": "^1.8.0", + "jest": "^29.5.0", + "react": "19.1.1", + "react-native": "0.82.1", + "react-native-builder-bob": "^0.40.15", + "typescript": "^5.9.2" + }, + "peerDependencies": { + "@sqlite.org/sqlite-wasm": "*", + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "@sqlite.org/sqlite-wasm": { + "optional": true + } + }, + "workspaces": [ + "example", + "node" + ], + "prettier": { + "quoteProps": "consistent", + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false + }, + "react-native-builder-bob": { + "source": "src", + "output": "lib", + "targets": [ + [ + "module", + { + "esm": true + } + ], + [ + "typescript", + { + "project": "tsconfig.build.json" + } + ] + ] + }, + "codegenConfig": { + "name": "OPSQLiteSpec", + "type": "modules", + "jsSrcsDir": "src", + "android": { + "javaPackageName": "com.op.sqlite" + } + }, + "create-react-native-library": { + "languages": "kotlin-objc", + "type": "turbo-module", + "version": "0.52.1" + }, + "packageManager": "yarn@4.11.0" } diff --git a/src/Storage.web.ts b/src/Storage.web.ts new file mode 100644 index 00000000..f851c358 --- /dev/null +++ b/src/Storage.web.ts @@ -0,0 +1,82 @@ +import { openAsync } from './functions.web'; +import { type DB } from './types'; + +type StorageOptions = { + location?: string; + encryptionKey?: string; +}; + +export class Storage { + private dbPromise: Promise; + + constructor(options: StorageOptions) { + this.dbPromise = (async () => { + const db = await openAsync({ ...options, name: '__opsqlite_storage.sqlite' }); + await db.execute( + 'CREATE TABLE IF NOT EXISTS storage (key TEXT PRIMARY KEY, value TEXT)' + ); + return db; + })(); + } + + private async getDb(): Promise { + return this.dbPromise; + } + + async getItem(key: string): Promise { + const db = await this.getDb(); + const result = await db.execute('SELECT value FROM storage WHERE key = ?', [key]); + + const value = result.rows[0]?.value; + if (typeof value !== 'undefined' && typeof value !== 'string') { + throw new Error('Value must be a string or undefined'); + } + + return value; + } + + getItemSync(_key: string): string | undefined { + throw new Error('[op-sqlite] Storage sync APIs are not supported on web.'); + } + + async setItem(key: string, value: string): Promise { + const db = await this.getDb(); + await db.execute('INSERT OR REPLACE INTO storage (key, value) VALUES (?, ?)', [ + key, + value, + ]); + } + + setItemSync(_key: string, _value: string): void { + throw new Error('[op-sqlite] Storage sync APIs are not supported on web.'); + } + + async removeItem(key: string): Promise { + const db = await this.getDb(); + await db.execute('DELETE FROM storage WHERE key = ?', [key]); + } + + removeItemSync(_key: string): void { + throw new Error('[op-sqlite] Storage sync APIs are not supported on web.'); + } + + async clear(): Promise { + const db = await this.getDb(); + await db.execute('DELETE FROM storage'); + } + + clearSync(): void { + throw new Error('[op-sqlite] Storage sync APIs are not supported on web.'); + } + + async getAllKeys(): Promise { + const db = await this.getDb(); + const result = await db.execute('SELECT key FROM storage'); + + return result.rows.map((row: any) => String(row.key)); + } + + delete(): void { + throw new Error('[op-sqlite] Storage.delete() is not supported on web.'); + } +} diff --git a/src/functions.ts b/src/functions.ts index e0f7c386..f1f90134 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -1,361 +1,364 @@ -import { NativeModules, Platform } from 'react-native'; +import { NativeModules, Platform } from "react-native"; import type { - _InternalDB, - _PendingTransaction, - BatchQueryResult, - DB, - DBParams, - OPSQLiteProxy, - QueryResult, - Scalar, - SQLBatchTuple, - Transaction, -} from './types'; + _InternalDB, + _PendingTransaction, + BatchQueryResult, + DB, + DBParams, + OPSQLiteProxy, + QueryResult, + Scalar, + SQLBatchTuple, + Transaction, +} from "./types"; declare global { - var __OPSQLiteProxy: object | undefined; + var __OPSQLiteProxy: object | undefined; } if (global.__OPSQLiteProxy == null) { - if (NativeModules.OPSQLite == null) { - throw new Error( - 'Base module not found. Did you do a pod install/clear the gradle cache?' - ); - } - - // Call the synchronous blocking install() function - const installed = NativeModules.OPSQLite.install(); - if (!installed) { - throw new Error( - `Failed to install op-sqlite: The native OPSQLite Module could not be installed! Looks like something went wrong when installing JSI bindings, check the native logs for more info` - ); - } - - // Check again if the constructor now exists. If not, throw an error. - if (global.__OPSQLiteProxy == null) { - throw new Error( - 'OPSqlite native object is not available. Something is wrong. Check the native logs for more information.' - ); - } + if (NativeModules.OPSQLite == null) { + throw new Error( + "Base module not found. Did you do a pod install/clear the gradle cache?", + ); + } + + // Call the synchronous blocking install() function + const installed = NativeModules.OPSQLite.install(); + if (!installed) { + throw new Error( + `Failed to install op-sqlite: The native OPSQLite Module could not be installed! Looks like something went wrong when installing JSI bindings, check the native logs for more info`, + ); + } + + // Check again if the constructor now exists. If not, throw an error. + if (global.__OPSQLiteProxy == null) { + throw new Error( + "OPSqlite native object is not available. Something is wrong. Check the native logs for more information.", + ); + } } const proxy = global.__OPSQLiteProxy; export const OPSQLite = proxy as OPSQLiteProxy; function enhanceDB(db: _InternalDB, options: DBParams): DB { - const lock = { - queue: [] as _PendingTransaction[], - inProgress: false, - }; - - const startNextTransaction = () => { - if (lock.inProgress) { - // Transaction is already in process bail out - return; - } - - if (lock.queue.length) { - lock.inProgress = true; - const tx = lock.queue.shift(); - - if (!tx) { - throw new Error('Could not get a operation on database'); - } - - setImmediate(() => { - tx.start(); - }); - } - }; - - function sanitizeArrayBuffersInArray( - params?: any[] | any[][] - ): any[] | undefined { - if (!params) { - return params; - } - - return params.map((p) => { - if (Array.isArray(p)) { - return sanitizeArrayBuffersInArray(p); - } - - if (ArrayBuffer.isView(p)) { - return p.buffer; - } - - return p; - }); - } - - // spreading the object does not work with HostObjects (db) - // We need to manually assign the fields - let enhancedDb = { - delete: db.delete, - attach: db.attach, - detach: db.detach, - loadFile: db.loadFile, - updateHook: db.updateHook, - commitHook: db.commitHook, - rollbackHook: db.rollbackHook, - loadExtension: db.loadExtension, - getDbPath: db.getDbPath, - reactiveExecute: db.reactiveExecute, - sync: db.sync, - setReservedBytes: db.setReservedBytes, - getReservedBytes: db.getReservedBytes, - close: db.close, - flushPendingReactiveQueries: db.flushPendingReactiveQueries, - executeBatch: async ( - commands: SQLBatchTuple[] - ): Promise => { - // Do normal for loop and replace in place for performance - for (let i = 0; i < commands.length; i++) { - // [1] is the params arg - if (commands[i]![1]) { - commands[i]![1] = sanitizeArrayBuffersInArray(commands[i]![1]) as any; - } - } - - async function run() { - try { - enhancedDb.executeSync('BEGIN TRANSACTION;'); - - let res = await db.executeBatch(commands as any[]); - - enhancedDb.executeSync('COMMIT;'); - - await db.flushPendingReactiveQueries(); - - return res; - } catch (executionError) { - try { - enhancedDb.executeSync('ROLLBACK;'); - } catch (rollbackError) { - throw rollbackError; - } - - throw executionError; - } finally { - lock.inProgress = false; - startNextTransaction(); - } - } - - return await new Promise((resolve, reject) => { - const tx: _PendingTransaction = { - start: () => { - run().then(resolve).catch(reject); - }, - }; - - lock.queue.push(tx); - startNextTransaction(); - }); - }, - executeWithHostObjects: async ( - query: string, - params?: Scalar[] - ): Promise => { - const sanitizedParams = sanitizeArrayBuffersInArray(params); - - return sanitizedParams - ? await db.executeWithHostObjects(query, sanitizedParams as Scalar[]) - : await db.executeWithHostObjects(query); - }, - executeRaw: async (query: string, params?: Scalar[]) => { - const sanitizedParams = sanitizeArrayBuffersInArray(params); - - return db.executeRaw(query, sanitizedParams as Scalar[]); - }, - executeRawSync: (query: string, params?: Scalar[]) => { - const sanitizedParams = sanitizeArrayBuffersInArray(params); - return db.executeRawSync(query, sanitizedParams as Scalar[]); - }, - // Wrapper for executeRaw, drizzleORM uses this function - // at some point I changed the API but they did not pin their dependency to a specific version - // so re-inserting this so it starts working again - executeRawAsync: async (query: string, params?: Scalar[]) => { - const sanitizedParams = sanitizeArrayBuffersInArray(params); - - return db.executeRaw(query, sanitizedParams as Scalar[]); - }, - executeSync: (query: string, params?: Scalar[]): QueryResult => { - let res = params - ? db.executeSync(query, sanitizeArrayBuffersInArray(params) as Scalar[]) - : db.executeSync(query); - - if (!res.rows) { - let rows: Record[] = []; - for (let i = 0; i < (res.rawRows?.length ?? 0); i++) { - let row: Record = {}; - let rawRow = res.rawRows![i]!; - for (let j = 0; j < res.columnNames!.length; j++) { - let columnName = res.columnNames![j]!; - let value = rawRow[j]!; - - row[columnName] = value; - } - rows.push(row); - } - - delete res.rawRows; - - res = { - ...res, - rows, - }; - } - - return res; - }, - executeAsync: async ( - query: string, - params?: Scalar[] | undefined - ): Promise => { - return db.execute(query, params); - }, - execute: async ( - query: string, - params?: Scalar[] | undefined - ): Promise => { - let res = params - ? await db.execute( - query, - sanitizeArrayBuffersInArray(params) as Scalar[] - ) - : await db.execute(query); - - if (!res.rows) { - let rows: Record[] = []; - for (let i = 0; i < (res.rawRows?.length ?? 0); i++) { - let row: Record = {}; - let rawRow = res.rawRows![i]!; - for (let j = 0; j < res.columnNames!.length; j++) { - let columnName = res.columnNames![j]!; - let value = rawRow[j]!; - - row[columnName] = value; - } - rows.push(row); - } - - delete res.rawRows; - - res = { - ...res, - rows, - }; - } - - return res; - }, - prepareStatement: (query: string) => { - const stmt = db.prepareStatement(query); - - return { - bindSync: (params: Scalar[]) => { - const sanitizedParams = sanitizeArrayBuffersInArray(params); - - stmt.bindSync(sanitizedParams!); - }, - bind: async (params: Scalar[]) => { - const sanitizedParams = sanitizeArrayBuffersInArray(params); - - await stmt.bind(sanitizedParams!); - }, - execute: stmt.execute, - }; - }, - transaction: async ( - fn: (tx: Transaction) => Promise - ): Promise => { - let isFinalized = false; - - const execute = async (query: string, params?: Scalar[]) => { - if (isFinalized) { - throw Error( - `OP-Sqlite Error: Database: ${ - options.name || options.url - }. Cannot execute query on finalized transaction` - ); - } - return await enhancedDb.execute(query, params); - }; - - const commit = async (): Promise => { - if (isFinalized) { - throw Error( - `OP-Sqlite Error: Database: ${ - options.name || options.url - }. Cannot execute query on finalized transaction` - ); - } - const result = enhancedDb.executeSync('COMMIT;'); - - await db.flushPendingReactiveQueries(); - - isFinalized = true; - return result; - }; - - const rollback = (): QueryResult => { - if (isFinalized) { - throw Error( - `OP-Sqlite Error: Database: ${ - options.name || options.url - }. Cannot execute query on finalized transaction` - ); - } - const result = enhancedDb.executeSync('ROLLBACK;'); - isFinalized = true; - return result; - }; - - async function run() { - try { - enhancedDb.executeSync('BEGIN TRANSACTION;'); - - await fn({ - commit, - execute, - rollback, - }); - - if (!isFinalized) { - commit(); - } - } catch (executionError) { - if (!isFinalized) { - try { - rollback(); - } catch (rollbackError) { - throw rollbackError; - } - } - - throw executionError; - } finally { - lock.inProgress = false; - isFinalized = false; - startNextTransaction(); - } - } - - return await new Promise((resolve, reject) => { - const tx: _PendingTransaction = { - start: () => { - run().then(resolve).catch(reject); - }, - }; - - lock.queue.push(tx); - startNextTransaction(); - }); - }, - }; - - return enhancedDb; + const lock = { + queue: [] as _PendingTransaction[], + inProgress: false, + }; + + const startNextTransaction = () => { + if (lock.inProgress) { + // Transaction is already in process bail out + return; + } + + if (lock.queue.length) { + lock.inProgress = true; + const tx = lock.queue.shift(); + + if (!tx) { + throw new Error("Could not get a operation on database"); + } + + setImmediate(() => { + tx.start(); + }); + } + }; + + function sanitizeArrayBuffersInArray( + params?: any[] | any[][], + ): any[] | undefined { + if (!params) { + return params; + } + + return params.map((p) => { + if (Array.isArray(p)) { + return sanitizeArrayBuffersInArray(p); + } + + if (ArrayBuffer.isView(p)) { + return p.buffer; + } + + return p; + }); + } + + // spreading the object does not work with HostObjects (db) + // We need to manually assign the fields + const enhancedDb = { + delete: db.delete, + attach: db.attach, + detach: db.detach, + loadFile: db.loadFile, + updateHook: db.updateHook, + commitHook: db.commitHook, + rollbackHook: db.rollbackHook, + loadExtension: db.loadExtension, + getDbPath: db.getDbPath, + reactiveExecute: db.reactiveExecute, + sync: db.sync, + setReservedBytes: db.setReservedBytes, + getReservedBytes: db.getReservedBytes, + close: db.close, + closeAsync: async () => { + db.close(); + }, + flushPendingReactiveQueries: db.flushPendingReactiveQueries, + executeBatch: async ( + commands: SQLBatchTuple[], + ): Promise => { + // Do normal for loop and replace in place for performance + for (let i = 0; i < commands.length; i++) { + // [1] is the params arg + if (commands[i]![1]) { + commands[i]![1] = sanitizeArrayBuffersInArray(commands[i]![1]) as any; + } + } + + async function run() { + try { + enhancedDb.executeSync("BEGIN TRANSACTION;"); + + const res = await db.executeBatch(commands as any[]); + + enhancedDb.executeSync("COMMIT;"); + + await db.flushPendingReactiveQueries(); + + return res; + } catch (executionError) { + try { + enhancedDb.executeSync("ROLLBACK;"); + } catch (rollbackError) { + throw rollbackError; + } + + throw executionError; + } finally { + lock.inProgress = false; + startNextTransaction(); + } + } + + return await new Promise((resolve, reject) => { + const tx: _PendingTransaction = { + start: () => { + run().then(resolve).catch(reject); + }, + }; + + lock.queue.push(tx); + startNextTransaction(); + }); + }, + executeWithHostObjects: async ( + query: string, + params?: Scalar[], + ): Promise => { + const sanitizedParams = sanitizeArrayBuffersInArray(params); + + return sanitizedParams + ? await db.executeWithHostObjects(query, sanitizedParams as Scalar[]) + : await db.executeWithHostObjects(query); + }, + executeRaw: async (query: string, params?: Scalar[]) => { + const sanitizedParams = sanitizeArrayBuffersInArray(params); + + return db.executeRaw(query, sanitizedParams as Scalar[]); + }, + executeRawSync: (query: string, params?: Scalar[]) => { + const sanitizedParams = sanitizeArrayBuffersInArray(params); + return db.executeRawSync(query, sanitizedParams as Scalar[]); + }, + // Wrapper for executeRaw, drizzleORM uses this function + // at some point I changed the API but they did not pin their dependency to a specific version + // so re-inserting this so it starts working again + executeRawAsync: async (query: string, params?: Scalar[]) => { + const sanitizedParams = sanitizeArrayBuffersInArray(params); + + return db.executeRaw(query, sanitizedParams as Scalar[]); + }, + executeSync: (query: string, params?: Scalar[]): QueryResult => { + let res = params + ? db.executeSync(query, sanitizeArrayBuffersInArray(params) as Scalar[]) + : db.executeSync(query); + + if (!res.rows) { + const rows: Record[] = []; + for (let i = 0; i < (res.rawRows?.length ?? 0); i++) { + const row: Record = {}; + const rawRow = res.rawRows![i]!; + for (let j = 0; j < res.columnNames!.length; j++) { + const columnName = res.columnNames![j]!; + const value = rawRow[j]!; + + row[columnName] = value; + } + rows.push(row); + } + + delete res.rawRows; + + res = { + ...res, + rows, + }; + } + + return res; + }, + executeAsync: async ( + query: string, + params?: Scalar[] | undefined, + ): Promise => { + return db.execute(query, params); + }, + execute: async ( + query: string, + params?: Scalar[] | undefined, + ): Promise => { + let res = params + ? await db.execute( + query, + sanitizeArrayBuffersInArray(params) as Scalar[], + ) + : await db.execute(query); + + if (!res.rows) { + const rows: Record[] = []; + for (let i = 0; i < (res.rawRows?.length ?? 0); i++) { + const row: Record = {}; + const rawRow = res.rawRows![i]!; + for (let j = 0; j < res.columnNames!.length; j++) { + const columnName = res.columnNames![j]!; + const value = rawRow[j]!; + + row[columnName] = value; + } + rows.push(row); + } + + delete res.rawRows; + + res = { + ...res, + rows, + }; + } + + return res; + }, + prepareStatement: (query: string) => { + const stmt = db.prepareStatement(query); + + return { + bindSync: (params: Scalar[]) => { + const sanitizedParams = sanitizeArrayBuffersInArray(params); + + stmt.bindSync(sanitizedParams!); + }, + bind: async (params: Scalar[]) => { + const sanitizedParams = sanitizeArrayBuffersInArray(params); + + await stmt.bind(sanitizedParams!); + }, + execute: stmt.execute, + }; + }, + transaction: async ( + fn: (tx: Transaction) => Promise, + ): Promise => { + let isFinalized = false; + + const execute = async (query: string, params?: Scalar[]) => { + if (isFinalized) { + throw Error( + `OP-Sqlite Error: Database: ${ + options.name || options.url + }. Cannot execute query on finalized transaction`, + ); + } + return await enhancedDb.execute(query, params); + }; + + const commit = async (): Promise => { + if (isFinalized) { + throw Error( + `OP-Sqlite Error: Database: ${ + options.name || options.url + }. Cannot execute query on finalized transaction`, + ); + } + const result = enhancedDb.executeSync("COMMIT;"); + + await db.flushPendingReactiveQueries(); + + isFinalized = true; + return result; + }; + + const rollback = (): QueryResult => { + if (isFinalized) { + throw Error( + `OP-Sqlite Error: Database: ${ + options.name || options.url + }. Cannot execute query on finalized transaction`, + ); + } + const result = enhancedDb.executeSync("ROLLBACK;"); + isFinalized = true; + return result; + }; + + async function run() { + try { + enhancedDb.executeSync("BEGIN TRANSACTION;"); + + await fn({ + commit, + execute, + rollback, + }); + + if (!isFinalized) { + commit(); + } + } catch (executionError) { + if (!isFinalized) { + try { + rollback(); + } catch (rollbackError) { + throw rollbackError; + } + } + + throw executionError; + } finally { + lock.inProgress = false; + isFinalized = false; + startNextTransaction(); + } + } + + return await new Promise((resolve, reject) => { + const tx: _PendingTransaction = { + start: () => { + run().then(resolve).catch(reject); + }, + }; + + lock.queue.push(tx); + startNextTransaction(); + }); + }, + }; + + return enhancedDb; } /** @@ -363,23 +366,23 @@ function enhanceDB(db: _InternalDB, options: DBParams): DB { * libsql needs to be enabled on your package.json */ export const openSync = (params: { - url: string; - authToken: string; - name: string; - location?: string; - libsqlSyncInterval?: number; - libsqlOffline?: boolean; - encryptionKey?: string; - remoteEncryptionKey?: string; + url: string; + authToken: string; + name: string; + location?: string; + libsqlSyncInterval?: number; + libsqlOffline?: boolean; + encryptionKey?: string; + remoteEncryptionKey?: string; }): DB => { - if (!isLibsql()) { - throw new Error('This function is only available for libsql'); - } + if (!isLibsql()) { + throw new Error("This function is only available for libsql"); + } - const db = OPSQLite.openSync(params); - const enhancedDb = enhanceDB(db, params); + const db = OPSQLite.openSync(params); + const enhancedDb = enhanceDB(db, params); - return enhancedDb; + return enhancedDb; }; /** @@ -387,14 +390,14 @@ export const openSync = (params: { * libsql needs to be enabled on your package.json */ export const openRemote = (params: { url: string; authToken: string }): DB => { - if (!isLibsql()) { - throw new Error('This function is only available for libsql'); - } + if (!isLibsql()) { + throw new Error("This function is only available for libsql"); + } - const db = OPSQLite.openRemote(params); - const enhancedDb = enhanceDB(db, params); + const db = OPSQLite.openRemote(params); + const enhancedDb = enhanceDB(db, params); - return enhancedDb; + return enhancedDb; }; /** @@ -402,21 +405,33 @@ export const openRemote = (params: { url: string; authToken: string }): DB => { * If you want libsql remote or sync connections, use openSync or openRemote */ export const open = (params: { - name: string; - location?: string; - encryptionKey?: string; + name: string; + location?: string; + encryptionKey?: string; }): DB => { - if (params.location?.startsWith('file://')) { - console.warn( - "[op-sqlite] You are passing a path with 'file://' prefix, it's automatically removed" - ); - params.location = params.location.substring(7); - } + if (params.location?.startsWith("file://")) { + console.warn( + "[op-sqlite] You are passing a path with 'file://' prefix, it's automatically removed", + ); + params.location = params.location.substring(7); + } - const db = OPSQLite.open(params); - const enhancedDb = enhanceDB(db, params); + const db = OPSQLite.open(params); + const enhancedDb = enhanceDB(db, params); - return enhancedDb; + return enhancedDb; +}; + +/** + * Async wrapper around open(). + * Useful for cross-platform code that also targets web where openAsync() is required. + */ +export const openAsync = async (params: { + name: string; + location?: string; + encryptionKey?: string; +}): Promise => { + return open(params); }; /** @@ -427,11 +442,11 @@ export const open = (params: { * @returns promise, rejects if failed to move the database, resolves if the operation was successful */ export const moveAssetsDatabase = async (args: { - filename: string; - path?: string; - overwrite?: boolean; + filename: string; + path?: string; + overwrite?: boolean; }): Promise => { - return NativeModules.OPSQLite.moveAssetsDatabase(args); + return NativeModules.OPSQLite.moveAssetsDatabase(args); }; /** @@ -443,23 +458,23 @@ export const moveAssetsDatabase = async (args: { * @returns */ export const getDylibPath = (bundle: string, name: string): string => { - return NativeModules.OPSQLite.getDylibPath(bundle, name); + return NativeModules.OPSQLite.getDylibPath(bundle, name); }; export const isSQLCipher = (): boolean => { - return OPSQLite.isSQLCipher(); + return OPSQLite.isSQLCipher(); }; export const isLibsql = (): boolean => { - return OPSQLite.isLibsql(); + return OPSQLite.isLibsql(); }; export const isIOSEmbedded = (): boolean => { - if (Platform.OS !== 'ios') { - return false; - } + if (Platform.OS !== "ios") { + return false; + } - return OPSQLite.isIOSEmbedded(); + return OPSQLite.isIOSEmbedded(); }; /** diff --git a/src/functions.web.ts b/src/functions.web.ts new file mode 100644 index 00000000..fe2c1b32 --- /dev/null +++ b/src/functions.web.ts @@ -0,0 +1,509 @@ +import type { + _InternalDB, + _PendingTransaction, + BatchQueryResult, + DB, + DBParams, + FileLoadResult, + OPSQLiteProxy, + PreparedStatement, + QueryResult, + Scalar, + SQLBatchTuple, + Transaction, +} from "./types"; + +type WorkerPromiser = ( + type: string, + args?: Record, +) => Promise; + +const WEB_ONLY_SYNC_ERROR = + "[op-sqlite] Web backend is async-only. Use openAsync() and async methods like execute()."; + +function throwSyncApiError(method: string): never { + throw new Error(`${WEB_ONLY_SYNC_ERROR} Called sync method: ${method}().`); +} + +function toNumber(value: unknown): number | undefined { + if (value == null) { + return undefined; + } + + if (typeof value === "bigint") { + const asNumber = Number(value); + return Number.isFinite(asNumber) ? asNumber : undefined; + } + + if (typeof value === "number") { + return Number.isFinite(value) ? value : undefined; + } + + return undefined; +} + +function ensureSingleStatement(sql: string): void { + // Web worker executes the full SQL string while native executes only the first prepared statement. + // We warn here so callers can keep behavior consistent across platforms when needed. + if (sql.includes(";")) { + const trimmed = sql.trim(); + if (!trimmed.endsWith(";") || trimmed.slice(0, -1).includes(";")) { + console.warn( + "[op-sqlite] Web execute() runs full SQL strings. Avoid multi-statement SQL for parity with native first-statement behavior.", + ); + } + } +} + +let promiserPromise: Promise | null = null; + +async function getPromiser(): Promise { + if (!promiserPromise) { + promiserPromise = (async () => { + let mod: any; + + try { + mod = await import("@sqlite.org/sqlite-wasm"); + } catch (error) { + throw new Error( + `[op-sqlite] Web support requires optional dependency @sqlite.org/sqlite-wasm. Install it in your app: npm i @sqlite.org/sqlite-wasm (or yarn add @sqlite.org/sqlite-wasm). Original error: ${(error as Error).message}`, + ); + } + + const makePromiser = mod.sqlite3Worker1Promiser as any; + + const maybePromiser = makePromiser(); + + if (typeof maybePromiser === "function") { + return maybePromiser as WorkerPromiser; + } + + return (await maybePromiser) as WorkerPromiser; + })(); + } + + return promiserPromise; +} + +async function ensureOpfs(promiser: WorkerPromiser): Promise { + const config = await promiser("config-get", {}); + const vfsList = config?.result?.vfsList; + + if (!Array.isArray(vfsList) || !vfsList.includes("opfs")) { + throw new Error( + "[op-sqlite] OPFS is required on web for persistence. Ensure COOP/COEP headers are set and OPFS is available in this browser.", + ); + } +} + +async function executeWorker( + promiser: WorkerPromiser, + dbId: string, + query: string, + params?: Scalar[], +): Promise { + ensureSingleStatement(query); + + const response = await promiser("exec", { + dbId, + sql: query, + bind: params, + rowMode: "object", + resultRows: [], + columnNames: [], + returnValue: "resultRows", + }); + + const result = response?.result; + const rows = Array.isArray(result?.resultRows) + ? (result.resultRows as Array>) + : Array.isArray(result) + ? (result as Array>) + : []; + const columnNames = Array.isArray(result?.columnNames) + ? (result.columnNames as string[]) + : rows.length > 0 + ? Object.keys(rows[0] ?? {}) + : []; + + const rowsAffected = toNumber(result?.changeCount) ?? 0; + const insertId = toNumber(result?.lastInsertRowId); + + return { + rowsAffected, + insertId, + rows, + columnNames, + }; +} + +function enhanceWebDb( + db: _InternalDB, + options: { name?: string; location?: string }, +): DB { + const lock = { + queue: [] as _PendingTransaction[], + inProgress: false, + }; + + const startNextTransaction = () => { + if (lock.inProgress || lock.queue.length === 0) { + return; + } + + lock.inProgress = true; + const tx = lock.queue.shift(); + if (!tx) { + throw new Error("Could not get an operation on database"); + } + + setTimeout(() => { + tx.start(); + }, 0); + }; + + const withTransactionLock = async (work: () => Promise): Promise => { + return new Promise((resolve, reject) => { + const tx: _PendingTransaction = { + start: () => { + work() + .then(resolve) + .catch(reject) + .finally(() => { + lock.inProgress = false; + startNextTransaction(); + }); + }, + }; + + lock.queue.push(tx); + startNextTransaction(); + }); + }; + + const unsupported = (method: string) => () => throwSyncApiError(method); + + const enhancedDb: DB = { + close: unsupported("close"), + closeAsync: async () => { + await db.closeAsync?.(); + }, + delete: unsupported("delete"), + attach: unsupported("attach"), + detach: unsupported("detach"), + transaction: async ( + fn: (tx: Transaction) => Promise, + ): Promise => { + return withTransactionLock(async () => { + let finalized = false; + + const commit = async (): Promise => { + if (finalized) { + throw new Error( + `OP-Sqlite Error: Database: ${options.name}. Cannot execute query on finalized transaction`, + ); + } + + const res = await enhancedDb.execute("COMMIT;"); + finalized = true; + return res; + }; + + const rollback = (): QueryResult => { + throwSyncApiError("rollback"); + }; + + const execute = async (query: string, params?: Scalar[]) => { + if (finalized) { + throw new Error( + `OP-Sqlite Error: Database: ${options.name}. Cannot execute query on finalized transaction`, + ); + } + + return enhancedDb.execute(query, params); + }; + + await enhancedDb.execute("BEGIN TRANSACTION;"); + + try { + await fn({ + execute, + commit, + rollback, + }); + + if (!finalized) { + await commit(); + } + } catch (error) { + if (!finalized) { + await enhancedDb.execute("ROLLBACK;"); + } + + throw error; + } + }); + }, + executeSync: unsupported("executeSync"), + execute: db.execute, + executeWithHostObjects: db.execute, + executeBatch: async ( + commands: SQLBatchTuple[], + ): Promise => { + await withTransactionLock(async () => { + await db.execute("BEGIN TRANSACTION;"); + + try { + for (const command of commands) { + const [sql, bind] = command; + + if (!bind) { + await db.execute(sql); + continue; + } + + if (Array.isArray(bind[0])) { + for (const rowBind of bind as Scalar[][]) { + await db.execute(sql, rowBind); + } + } else { + await db.execute(sql, bind as Scalar[]); + } + } + + await db.execute("COMMIT;"); + } catch (error) { + await db.execute("ROLLBACK;"); + throw error; + } + }); + + return { + rowsAffected: 0, + }; + }, + loadFile: async (_location: string): Promise => { + throw new Error("[op-sqlite] loadFile() is not supported on web."); + }, + updateHook: () => { + throw new Error("[op-sqlite] updateHook() is not supported on web."); + }, + commitHook: () => { + throw new Error("[op-sqlite] commitHook() is not supported on web."); + }, + rollbackHook: () => { + throw new Error("[op-sqlite] rollbackHook() is not supported on web."); + }, + prepareStatement: (query: string): PreparedStatement => { + let currentParams: Scalar[] = []; + + return { + bind: async (params: Scalar[]) => { + currentParams = params; + }, + bindSync: unsupported("bindSync"), + execute: async () => { + return db.execute(query, currentParams); + }, + }; + }, + loadExtension: unsupported("loadExtension"), + executeRaw: db.executeRaw, + executeRawSync: unsupported("executeRawSync"), + getDbPath: unsupported("getDbPath"), + reactiveExecute: unsupported("reactiveExecute"), + sync: unsupported("sync"), + setReservedBytes: unsupported("setReservedBytes"), + getReservedBytes: unsupported("getReservedBytes"), + flushPendingReactiveQueries: async () => {}, + }; + + return enhancedDb; +} + +async function createWebDb(params: { + name: string; + location?: string; + encryptionKey?: string; +}): Promise<_InternalDB> { + if (params.encryptionKey) { + throw new Error("[op-sqlite] SQLCipher is not supported on web."); + } + + const promiser = await getPromiser(); + await ensureOpfs(promiser); + + const filename = `file:${params.name}?vfs=opfs`; + const opened = await promiser("open", { + filename, + }); + + const dbId = opened?.dbId || opened?.result?.dbId; + if (!dbId || typeof dbId !== "string") { + throw new Error("[op-sqlite] Failed to open web sqlite database."); + } + + return { + close: () => { + throwSyncApiError("close"); + }, + closeAsync: async () => { + await promiser("close", { + dbId, + }); + }, + delete: () => { + throwSyncApiError("delete"); + }, + attach: () => { + throw new Error("[op-sqlite] attach() is not supported on web."); + }, + detach: () => { + throw new Error("[op-sqlite] detach() is not supported on web."); + }, + transaction: async () => { + throw new Error( + "[op-sqlite] transaction() must be called on an opened DB object.", + ); + }, + executeSync: () => { + throwSyncApiError("executeSync"); + }, + execute: async (query: string, bind?: Scalar[]) => { + return executeWorker(promiser, dbId, query, bind); + }, + executeWithHostObjects: async (query: string, bind?: Scalar[]) => { + return executeWorker(promiser, dbId, query, bind); + }, + executeBatch: async (_commands: SQLBatchTuple[]) => { + throw new Error( + "[op-sqlite] executeBatch() must be called on an opened DB object.", + ); + }, + loadFile: async (_location: string) => { + throw new Error("[op-sqlite] loadFile() is not supported on web."); + }, + updateHook: () => { + throw new Error("[op-sqlite] updateHook() is not supported on web."); + }, + commitHook: () => { + throw new Error("[op-sqlite] commitHook() is not supported on web."); + }, + rollbackHook: () => { + throw new Error("[op-sqlite] rollbackHook() is not supported on web."); + }, + prepareStatement: (_query: string) => { + throw new Error( + "[op-sqlite] prepareStatement() must be called on an opened DB object.", + ); + }, + loadExtension: () => { + throw new Error("[op-sqlite] loadExtension() is not supported on web."); + }, + executeRaw: async (query: string, bind?: Scalar[]) => { + ensureSingleStatement(query); + + const response = await promiser("exec", { + dbId, + sql: query, + bind, + rowMode: "array", + resultRows: [], + returnValue: "resultRows", + }); + + const result = response?.result; + const rows = result?.resultRows ?? result; + return Array.isArray(rows) ? rows : []; + }, + executeRawSync: () => { + throwSyncApiError("executeRawSync"); + }, + getDbPath: () => { + throwSyncApiError("getDbPath"); + }, + reactiveExecute: () => { + throw new Error("[op-sqlite] reactiveExecute() is not supported on web."); + }, + sync: () => { + throwSyncApiError("sync"); + }, + setReservedBytes: () => { + throwSyncApiError("setReservedBytes"); + }, + getReservedBytes: () => { + throwSyncApiError("getReservedBytes"); + }, + flushPendingReactiveQueries: async () => {}, + }; +} + +/** + * Open a connection to a local sqlite database on web. + * Web is async-only: use openAsync() and async methods like execute(). + */ +export const openAsync = async (params: { + name: string; + location?: string; + encryptionKey?: string; +}): Promise => { + const db = await createWebDb(params); + return enhanceWebDb(db, params); +}; + +export const open = (_params: { + name: string; + location?: string; + encryptionKey?: string; +}): DB => { + throwSyncApiError("open"); +}; + +export const openSync = (_params: { + url: string; + authToken: string; + name: string; + location?: string; + libsqlSyncInterval?: number; + libsqlOffline?: boolean; + encryptionKey?: string; + remoteEncryptionKey?: string; +}): DB => { + throwSyncApiError("openSync"); +}; + +export const openRemote = (_params: { url: string; authToken: string }): DB => { + throw new Error("[op-sqlite] openRemote() is not supported on web."); +}; + +export const moveAssetsDatabase = async (_args: { + filename: string; + path?: string; + overwrite?: boolean; +}): Promise => { + throw new Error("[op-sqlite] moveAssetsDatabase() is not supported on web."); +}; + +export const getDylibPath = (_bundle: string, _name: string): string => { + throw new Error("[op-sqlite] getDylibPath() is not supported on web."); +}; + +export const isSQLCipher = (): boolean => { + return false; +}; + +export const isLibsql = (): boolean => { + return false; +}; + +export const isIOSEmbedded = (): boolean => { + return false; +}; + +/** + * @deprecated Use `isIOSEmbedded` instead. This alias will be removed in a future release. + */ +export const isIOSEmbeeded = isIOSEmbedded; + +// Web does not expose the native JSI proxy object. +export const OPSQLite = {} as OPSQLiteProxy; diff --git a/src/index.web.ts b/src/index.web.ts new file mode 100644 index 00000000..5b5b2a55 --- /dev/null +++ b/src/index.web.ts @@ -0,0 +1,24 @@ +export * from './functions.web'; +export { Storage } from './Storage.web'; +export type { + Scalar, + QueryResult, + ColumnMetadata, + SQLBatchTuple, + UpdateHookOperation, + BatchQueryResult, + FileLoadResult, + Transaction, + _PendingTransaction, + PreparedStatement, + _InternalDB, + DB, + DBParams, + OPSQLiteProxy, +} from './types'; + +export const IOS_DOCUMENT_PATH = ''; +export const IOS_LIBRARY_PATH = ''; +export const ANDROID_DATABASE_PATH = ''; +export const ANDROID_FILES_PATH = ''; +export const ANDROID_EXTERNAL_FILES_PATH = ''; diff --git a/src/sqlite-wasm-optional.d.ts b/src/sqlite-wasm-optional.d.ts new file mode 100644 index 00000000..d455569b --- /dev/null +++ b/src/sqlite-wasm-optional.d.ts @@ -0,0 +1,3 @@ +declare module "@sqlite.org/sqlite-wasm" { + export const sqlite3Worker1Promiser: any; +} diff --git a/src/types.ts b/src/types.ts index 63bc446f..ce03b4f2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,10 +1,10 @@ export type Scalar = - | string - | number - | boolean - | null - | ArrayBuffer - | ArrayBufferView; + | string + | number + | boolean + | null + | ArrayBuffer + | ArrayBufferView; /** * Object returned by SQL Query executions { @@ -17,17 +17,17 @@ export type Scalar = * @interface QueryResult */ export type QueryResult = { - insertId?: number; - rowsAffected: number; - res?: any[]; - rows: Array>; - // An array of intermediate results, just values without column names - rawRows?: Scalar[][]; - columnNames?: string[]; - /** - * Query metadata, available only for select query results - */ - metadata?: ColumnMetadata[]; + insertId?: number; + rowsAffected: number; + res?: any[]; + rows: Array>; + // An array of intermediate results, just values without column names + rawRows?: Scalar[][]; + columnNames?: string[]; + /** + * Query metadata, available only for select query results + */ + metadata?: ColumnMetadata[]; }; /** @@ -35,13 +35,13 @@ export type QueryResult = { * Describes some information about columns fetched by the query */ export type ColumnMetadata = { - /** The name used for this column for this result set */ - name: string; - /** The declared column type for this column, when fetched directly from a table or a View resulting from a table column. "UNKNOWN" for dynamic values, like function returned ones. */ - type: string; - /** - * The index for this column for this result set*/ - index: number; + /** The name used for this column for this result set */ + name: string; + /** The declared column type for this column, when fetched directly from a table or a View resulting from a table column. "UNKNOWN" for dynamic values, like function returned ones. */ + type: string; + /** + * The index for this column for this result set*/ + index: number; }; /** @@ -51,11 +51,11 @@ export type ColumnMetadata = { * to declare it a single time, and use an array of array parameters. */ export type SQLBatchTuple = - | [string] - | [string, Scalar[]] - | [string, Scalar[][]]; + | [string] + | [string, Scalar[]] + | [string, Scalar[][]]; -export type UpdateHookOperation = 'INSERT' | 'DELETE' | 'UPDATE'; +export type UpdateHookOperation = "INSERT" | "DELETE" | "UPDATE"; /** * status: 0 or undefined for correct execution, 1 for error @@ -63,7 +63,7 @@ export type UpdateHookOperation = 'INSERT' | 'DELETE' | 'UPDATE'; * rowsAffected: Number of affected rows if status == 0 */ export type BatchQueryResult = { - rowsAffected?: number; + rowsAffected?: number; }; /** @@ -71,255 +71,257 @@ export type BatchQueryResult = { * Similar to BatchQueryResult */ export type FileLoadResult = BatchQueryResult & { - commands?: number; + commands?: number; }; export type Transaction = { - commit: () => Promise; - execute: (query: string, params?: Scalar[]) => Promise; - rollback: () => QueryResult; + commit: () => Promise; + execute: (query: string, params?: Scalar[]) => Promise; + rollback: () => QueryResult; }; export type _PendingTransaction = { - /* - * The start function should not throw or return a promise because the - * queue just calls it and does not monitor for failures or completions. - * - * It should catch any errors and call the resolve or reject of the wrapping - * promise when complete. - * - * It should also automatically commit or rollback the transaction if needed - */ - start: () => void; + /* + * The start function should not throw or return a promise because the + * queue just calls it and does not monitor for failures or completions. + * + * It should catch any errors and call the resolve or reject of the wrapping + * promise when complete. + * + * It should also automatically commit or rollback the transaction if needed + */ + start: () => void; }; export type PreparedStatement = { - bind: (params: any[]) => Promise; - bindSync: (params: any[]) => void; - execute: () => Promise; + bind: (params: any[]) => Promise; + bindSync: (params: any[]) => void; + execute: () => Promise; }; export type _InternalDB = { - close: () => void; - delete: (location?: string) => void; - attach: (params: { - secondaryDbFileName: string; - alias: string; - location?: string; - }) => void; - detach: (alias: string) => void; - transaction: (fn: (tx: Transaction) => Promise) => Promise; - executeSync: (query: string, params?: Scalar[]) => QueryResult; - execute: (query: string, params?: Scalar[]) => Promise; - executeWithHostObjects: ( - query: string, - params?: Scalar[] - ) => Promise; - executeBatch: (commands: SQLBatchTuple[]) => Promise; - loadFile: (location: string) => Promise; - updateHook: ( - callback?: - | ((params: { - table: string; - operation: UpdateHookOperation; - row?: any; - rowId: number; - }) => void) - | null - ) => void; - commitHook: (callback?: (() => void) | null) => void; - rollbackHook: (callback?: (() => void) | null) => void; - prepareStatement: (query: string) => PreparedStatement; - loadExtension: (path: string, entryPoint?: string) => void; - executeRaw: (query: string, params?: Scalar[]) => Promise; - executeRawSync: (query: string, params?: Scalar[]) => any[]; - getDbPath: (location?: string) => string; - reactiveExecute: (params: { - query: string; - arguments: any[]; - fireOn: { - table: string; - ids?: number[]; - }[]; - callback: (response: any) => void; - }) => () => void; - sync: () => void; - setReservedBytes: (reservedBytes: number) => void; - getReservedBytes: () => number; - flushPendingReactiveQueries: () => Promise; + close: () => void; + closeAsync?: () => Promise; + delete: (location?: string) => void; + attach: (params: { + secondaryDbFileName: string; + alias: string; + location?: string; + }) => void; + detach: (alias: string) => void; + transaction: (fn: (tx: Transaction) => Promise) => Promise; + executeSync: (query: string, params?: Scalar[]) => QueryResult; + execute: (query: string, params?: Scalar[]) => Promise; + executeWithHostObjects: ( + query: string, + params?: Scalar[], + ) => Promise; + executeBatch: (commands: SQLBatchTuple[]) => Promise; + loadFile: (location: string) => Promise; + updateHook: ( + callback?: + | ((params: { + table: string; + operation: UpdateHookOperation; + row?: any; + rowId: number; + }) => void) + | null, + ) => void; + commitHook: (callback?: (() => void) | null) => void; + rollbackHook: (callback?: (() => void) | null) => void; + prepareStatement: (query: string) => PreparedStatement; + loadExtension: (path: string, entryPoint?: string) => void; + executeRaw: (query: string, params?: Scalar[]) => Promise; + executeRawSync: (query: string, params?: Scalar[]) => any[]; + getDbPath: (location?: string) => string; + reactiveExecute: (params: { + query: string; + arguments: any[]; + fireOn: { + table: string; + ids?: number[]; + }[]; + callback: (response: any) => void; + }) => () => void; + sync: () => void; + setReservedBytes: (reservedBytes: number) => void; + getReservedBytes: () => number; + flushPendingReactiveQueries: () => Promise; }; export type DB = { - close: () => void; - delete: (location?: string) => void; - attach: (params: { - secondaryDbFileName: string; - alias: string; - location?: string; - }) => void; - detach: (alias: string) => void; - /** - * Wraps all the executions into a transaction. If an error is thrown it will rollback all of the changes - * - * You need to use this if you are using reactive queries for the queries to fire after the transaction is done - */ - transaction: (fn: (tx: Transaction) => Promise) => Promise; - /** - * Sync version of the execute function - * It will block the JS thread and therefore your UI and should be used with caution - * - * When writing your queries, you can use the ? character as a placeholder for parameters - * The parameters will be automatically escaped and sanitized - * - * Example: - * db.executeSync('SELECT * FROM table WHERE id = ?', [1]); - * - * If you are writing a query that doesn't require parameters, you can omit the second argument - * - * If you are writing to the database YOU SHOULD BE USING TRANSACTIONS! - * Transactions protect you from partial writes and ensure that your data is always in a consistent state - * - * @param query - * @param params - * @returns QueryResult - */ - executeSync: (query: string, params?: Scalar[]) => QueryResult; - /** - * Basic query execution function, it is async don't forget to await it - * - * When writing your queries, you can use the ? character as a placeholder for parameters - * The parameters will be automatically escaped and sanitized - * - * Example: - * await db.execute('SELECT * FROM table WHERE id = ?', [1]); - * - * If you are writing a query that doesn't require parameters, you can omit the second argument - * - * If you are writing to the database YOU SHOULD BE USING TRANSACTIONS! - * Transactions protect you from partial writes and ensure that your data is always in a consistent state - * - * If you need a large amount of queries ran as fast as possible you should be using `executeBatch`, `executeRaw`, `loadFile` or `executeWithHostObjects` - * - * @param query string of your SQL query - * @param params a list of parameters to bind to the query, if any - * @returns Promise with the result of the query - */ - execute: (query: string, params?: Scalar[]) => Promise; - /** - * Similar to the execute function but returns the response in HostObjects - * Read more about HostObjects in the documentation and their pitfalls - * - * Will be a lot faster than the normal execute functions when returning data but you will pay when accessing the fields - * as the conversion is done the moment you access any field - * @param query - * @param params - * @returns - */ - executeWithHostObjects: ( - query: string, - params?: Scalar[] - ) => Promise; - /** - * Executes all the queries in the params inside a single transaction - * - * It's faster than executing single queries as data is sent to the native side only once - * @param commands - * @returns Promise - */ - executeBatch: (commands: SQLBatchTuple[]) => Promise; - /** - * Loads a SQLite Dump from disk. It will be the fastest way to execute a large set of queries as no JS is involved - */ - loadFile: (location: string) => Promise; - updateHook: ( - callback?: - | ((params: { - table: string; - operation: UpdateHookOperation; - row?: any; - rowId: number; - }) => void) - | null - ) => void; - commitHook: (callback?: (() => void) | null) => void; - rollbackHook: (callback?: (() => void) | null) => void; - /** - * Constructs a prepared statement from the query string - * The statement can be re-bound with parameters and executed - * The performance gain is significant when the same query is executed multiple times, NOT when the query is executed (once) - * The cost lies in the preparation of the statement as it is compiled and optimized by the sqlite engine, the params can then rebound - * but the query itself is already optimized - * - * @param query string of your SQL query - * @returns Prepared statement object - */ - prepareStatement: (query: string) => PreparedStatement; - /** - * Loads a runtime loadable sqlite extension. Libsql and iOS embedded version do not support loading extensions - */ - loadExtension: (path: string, entryPoint?: string) => void; - /** - * Same as `execute` except the results are not returned in objects but rather in arrays with just the values and not the keys - * It will be faster since a lot of repeated work is skipped and only the values you care about are returned - */ - executeRaw: (query: string, params?: Scalar[]) => Promise; - /** - * Same as `executeRaw` but it will block the JS thread and therefore your UI and should be used with caution - * It will return an array of arrays with just the values and not the keys - */ - executeRawSync: (query: string, params?: Scalar[]) => any[]; - /** - * Gets the absolute path to the db file. Useful for debugging on local builds and for attaching the DB from users devices - */ - getDbPath: (location?: string) => string; - /** - * Reactive execution of queries when data is written to the database. Check the docs for how to use them. - */ - reactiveExecute: (params: { - query: string; - arguments: any[]; - fireOn: { - table: string; - ids?: number[]; - }[]; - callback: (response: any) => void; - }) => () => void; - /** This function is only available for libsql. - * Allows to trigger a sync the database with it's remote replica - * In order for this function to work you need to use openSync or openRemote functions - * with libsql: true in the package.json - * - * The database is hosted in turso - **/ - sync: () => void; - setReservedBytes: (reservedBytes: number) => void; - getReservedBytes: () => number; - /** - * If you have changed any of the tables outside of a transaction then the reactive queries will not fire on their own - * This method allows to flush the pending queue of changes. Useful when using Drizzle or other ORM that do not - * use the db.transaction method internally - * @returns void - */ - flushPendingReactiveQueries: () => Promise; + close: () => void; + closeAsync: () => Promise; + delete: (location?: string) => void; + attach: (params: { + secondaryDbFileName: string; + alias: string; + location?: string; + }) => void; + detach: (alias: string) => void; + /** + * Wraps all the executions into a transaction. If an error is thrown it will rollback all of the changes + * + * You need to use this if you are using reactive queries for the queries to fire after the transaction is done + */ + transaction: (fn: (tx: Transaction) => Promise) => Promise; + /** + * Sync version of the execute function + * It will block the JS thread and therefore your UI and should be used with caution + * + * When writing your queries, you can use the ? character as a placeholder for parameters + * The parameters will be automatically escaped and sanitized + * + * Example: + * db.executeSync('SELECT * FROM table WHERE id = ?', [1]); + * + * If you are writing a query that doesn't require parameters, you can omit the second argument + * + * If you are writing to the database YOU SHOULD BE USING TRANSACTIONS! + * Transactions protect you from partial writes and ensure that your data is always in a consistent state + * + * @param query + * @param params + * @returns QueryResult + */ + executeSync: (query: string, params?: Scalar[]) => QueryResult; + /** + * Basic query execution function, it is async don't forget to await it + * + * When writing your queries, you can use the ? character as a placeholder for parameters + * The parameters will be automatically escaped and sanitized + * + * Example: + * await db.execute('SELECT * FROM table WHERE id = ?', [1]); + * + * If you are writing a query that doesn't require parameters, you can omit the second argument + * + * If you are writing to the database YOU SHOULD BE USING TRANSACTIONS! + * Transactions protect you from partial writes and ensure that your data is always in a consistent state + * + * If you need a large amount of queries ran as fast as possible you should be using `executeBatch`, `executeRaw`, `loadFile` or `executeWithHostObjects` + * + * @param query string of your SQL query + * @param params a list of parameters to bind to the query, if any + * @returns Promise with the result of the query + */ + execute: (query: string, params?: Scalar[]) => Promise; + /** + * Similar to the execute function but returns the response in HostObjects + * Read more about HostObjects in the documentation and their pitfalls + * + * Will be a lot faster than the normal execute functions when returning data but you will pay when accessing the fields + * as the conversion is done the moment you access any field + * @param query + * @param params + * @returns + */ + executeWithHostObjects: ( + query: string, + params?: Scalar[], + ) => Promise; + /** + * Executes all the queries in the params inside a single transaction + * + * It's faster than executing single queries as data is sent to the native side only once + * @param commands + * @returns Promise + */ + executeBatch: (commands: SQLBatchTuple[]) => Promise; + /** + * Loads a SQLite Dump from disk. It will be the fastest way to execute a large set of queries as no JS is involved + */ + loadFile: (location: string) => Promise; + updateHook: ( + callback?: + | ((params: { + table: string; + operation: UpdateHookOperation; + row?: any; + rowId: number; + }) => void) + | null, + ) => void; + commitHook: (callback?: (() => void) | null) => void; + rollbackHook: (callback?: (() => void) | null) => void; + /** + * Constructs a prepared statement from the query string + * The statement can be re-bound with parameters and executed + * The performance gain is significant when the same query is executed multiple times, NOT when the query is executed (once) + * The cost lies in the preparation of the statement as it is compiled and optimized by the sqlite engine, the params can then rebound + * but the query itself is already optimized + * + * @param query string of your SQL query + * @returns Prepared statement object + */ + prepareStatement: (query: string) => PreparedStatement; + /** + * Loads a runtime loadable sqlite extension. Libsql and iOS embedded version do not support loading extensions + */ + loadExtension: (path: string, entryPoint?: string) => void; + /** + * Same as `execute` except the results are not returned in objects but rather in arrays with just the values and not the keys + * It will be faster since a lot of repeated work is skipped and only the values you care about are returned + */ + executeRaw: (query: string, params?: Scalar[]) => Promise; + /** + * Same as `executeRaw` but it will block the JS thread and therefore your UI and should be used with caution + * It will return an array of arrays with just the values and not the keys + */ + executeRawSync: (query: string, params?: Scalar[]) => any[]; + /** + * Gets the absolute path to the db file. Useful for debugging on local builds and for attaching the DB from users devices + */ + getDbPath: (location?: string) => string; + /** + * Reactive execution of queries when data is written to the database. Check the docs for how to use them. + */ + reactiveExecute: (params: { + query: string; + arguments: any[]; + fireOn: { + table: string; + ids?: number[]; + }[]; + callback: (response: any) => void; + }) => () => void; + /** This function is only available for libsql. + * Allows to trigger a sync the database with it's remote replica + * In order for this function to work you need to use openSync or openRemote functions + * with libsql: true in the package.json + * + * The database is hosted in turso + **/ + sync: () => void; + setReservedBytes: (reservedBytes: number) => void; + getReservedBytes: () => number; + /** + * If you have changed any of the tables outside of a transaction then the reactive queries will not fire on their own + * This method allows to flush the pending queue of changes. Useful when using Drizzle or other ORM that do not + * use the db.transaction method internally + * @returns void + */ + flushPendingReactiveQueries: () => Promise; }; export type DBParams = { - url?: string; - authToken?: string; - name?: string; - location?: string; - syncInterval?: number; + url?: string; + authToken?: string; + name?: string; + location?: string; + syncInterval?: number; }; export type OPSQLiteProxy = { - open: (options: { - name: string; - location?: string; - encryptionKey?: string; - }) => _InternalDB; - openRemote: (options: { url: string; authToken: string }) => _InternalDB; - openSync: (options: DBParams) => _InternalDB; - isSQLCipher: () => boolean; - isLibsql: () => boolean; - isIOSEmbedded: () => boolean; + open: (options: { + name: string; + location?: string; + encryptionKey?: string; + }) => _InternalDB; + openRemote: (options: { url: string; authToken: string }) => _InternalDB; + openSync: (options: DBParams) => _InternalDB; + isSQLCipher: () => boolean; + isLibsql: () => boolean; + isIOSEmbedded: () => boolean; }; diff --git a/yarn.lock b/yarn.lock index eabe566c..56a957e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -46,7 +46,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.23.9, @babel/core@npm:^7.25.2": +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.23.9, @babel/core@npm:^7.25.2, @babel/core@npm:^7.29.0": version: 7.29.0 resolution: "@babel/core@npm:7.29.0" dependencies: @@ -1114,7 +1114,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-react-jsx-self@npm:^7.24.7": +"@babel/plugin-transform-react-jsx-self@npm:^7.24.7, @babel/plugin-transform-react-jsx-self@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-react-jsx-self@npm:7.27.1" dependencies: @@ -1125,7 +1125,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-react-jsx-source@npm:^7.24.7": +"@babel/plugin-transform-react-jsx-source@npm:^7.24.7, @babel/plugin-transform-react-jsx-source@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-react-jsx-source@npm:7.27.1" dependencies: @@ -1466,6 +1466,13 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.18.6": + version: 7.29.2 + resolution: "@babel/runtime@npm:7.29.2" + checksum: 10c0/30b80a0140d16467792e1bbeb06f655b0dab70407da38dfac7fedae9c859f9ae9d846ef14ad77bd3814c064295fe9b1bc551f1541ea14646ae9f22b71a8bc17a + languageName: node + linkType: hard + "@babel/runtime@npm:^7.25.0": version: 7.28.6 resolution: "@babel/runtime@npm:7.28.6" @@ -1516,6 +1523,279 @@ __metadata: languageName: node linkType: hard +"@biomejs/biome@npm:^2.4.10": + version: 2.4.10 + resolution: "@biomejs/biome@npm:2.4.10" + dependencies: + "@biomejs/cli-darwin-arm64": "npm:2.4.10" + "@biomejs/cli-darwin-x64": "npm:2.4.10" + "@biomejs/cli-linux-arm64": "npm:2.4.10" + "@biomejs/cli-linux-arm64-musl": "npm:2.4.10" + "@biomejs/cli-linux-x64": "npm:2.4.10" + "@biomejs/cli-linux-x64-musl": "npm:2.4.10" + "@biomejs/cli-win32-arm64": "npm:2.4.10" + "@biomejs/cli-win32-x64": "npm:2.4.10" + dependenciesMeta: + "@biomejs/cli-darwin-arm64": + optional: true + "@biomejs/cli-darwin-x64": + optional: true + "@biomejs/cli-linux-arm64": + optional: true + "@biomejs/cli-linux-arm64-musl": + optional: true + "@biomejs/cli-linux-x64": + optional: true + "@biomejs/cli-linux-x64-musl": + optional: true + "@biomejs/cli-win32-arm64": + optional: true + "@biomejs/cli-win32-x64": + optional: true + bin: + biome: bin/biome + checksum: 10c0/80d10d5e6fa41a24efb9020ee73b79b0aca46942b55ea96e880c3bb45ea14c71e49fb1be9f134bee23b2d940bb8cad51a70351ca051e09a43613018dba693bd6 + languageName: node + linkType: hard + +"@biomejs/cli-darwin-arm64@npm:2.4.10": + version: 2.4.10 + resolution: "@biomejs/cli-darwin-arm64@npm:2.4.10" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@biomejs/cli-darwin-x64@npm:2.4.10": + version: 2.4.10 + resolution: "@biomejs/cli-darwin-x64@npm:2.4.10" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@biomejs/cli-linux-arm64-musl@npm:2.4.10": + version: 2.4.10 + resolution: "@biomejs/cli-linux-arm64-musl@npm:2.4.10" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@biomejs/cli-linux-arm64@npm:2.4.10": + version: 2.4.10 + resolution: "@biomejs/cli-linux-arm64@npm:2.4.10" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@biomejs/cli-linux-x64-musl@npm:2.4.10": + version: 2.4.10 + resolution: "@biomejs/cli-linux-x64-musl@npm:2.4.10" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@biomejs/cli-linux-x64@npm:2.4.10": + version: 2.4.10 + resolution: "@biomejs/cli-linux-x64@npm:2.4.10" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@biomejs/cli-win32-arm64@npm:2.4.10": + version: 2.4.10 + resolution: "@biomejs/cli-win32-arm64@npm:2.4.10" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@biomejs/cli-win32-x64@npm:2.4.10": + version: 2.4.10 + resolution: "@biomejs/cli-win32-x64@npm:2.4.10" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/aix-ppc64@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/aix-ppc64@npm:0.27.7" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/android-arm64@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/android-arm64@npm:0.27.7" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/android-arm@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/android-arm@npm:0.27.7" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@esbuild/android-x64@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/android-x64@npm:0.27.7" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/darwin-arm64@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/darwin-arm64@npm:0.27.7" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/darwin-x64@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/darwin-x64@npm:0.27.7" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/freebsd-arm64@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/freebsd-arm64@npm:0.27.7" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/freebsd-x64@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/freebsd-x64@npm:0.27.7" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/linux-arm64@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/linux-arm64@npm:0.27.7" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/linux-arm@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/linux-arm@npm:0.27.7" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@esbuild/linux-ia32@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/linux-ia32@npm:0.27.7" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/linux-loong64@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/linux-loong64@npm:0.27.7" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + +"@esbuild/linux-mips64el@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/linux-mips64el@npm:0.27.7" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + +"@esbuild/linux-ppc64@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/linux-ppc64@npm:0.27.7" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/linux-riscv64@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/linux-riscv64@npm:0.27.7" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"@esbuild/linux-s390x@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/linux-s390x@npm:0.27.7" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + +"@esbuild/linux-x64@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/linux-x64@npm:0.27.7" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/netbsd-arm64@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/netbsd-arm64@npm:0.27.7" + conditions: os=netbsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/netbsd-x64@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/netbsd-x64@npm:0.27.7" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openbsd-arm64@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/openbsd-arm64@npm:0.27.7" + conditions: os=openbsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/openbsd-x64@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/openbsd-x64@npm:0.27.7" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openharmony-arm64@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/openharmony-arm64@npm:0.27.7" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/sunos-x64@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/sunos-x64@npm:0.27.7" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/win32-arm64@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/win32-arm64@npm:0.27.7" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/win32-ia32@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/win32-ia32@npm:0.27.7" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/win32-x64@npm:0.27.7": + version: 0.27.7 + resolution: "@esbuild/win32-x64@npm:0.27.7" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@gar/promise-retry@npm:^1.0.0": version: 1.0.2 resolution: "@gar/promise-retry@npm:1.0.2" @@ -2007,6 +2287,8 @@ __metadata: version: 0.0.0-use.local resolution: "@op-engineering/op-sqlite@workspace:." dependencies: + "@biomejs/biome": "npm:^2.4.10" + "@sqlite.org/sqlite-wasm": "npm:^3.51.2-build8" "@types/better-sqlite3": "npm:^7.6.13" "@types/jest": "npm:^30.0.0" better-sqlite3: "npm:^12.5.0" @@ -2017,8 +2299,12 @@ __metadata: react-native-builder-bob: "npm:^0.40.15" typescript: "npm:^5.9.2" peerDependencies: + "@sqlite.org/sqlite-wasm": "*" react: "*" react-native: "*" + peerDependenciesMeta: + "@sqlite.org/sqlite-wasm": + optional: true languageName: unknown linkType: soft @@ -2490,6 +2776,13 @@ __metadata: languageName: node linkType: hard +"@react-native/normalize-colors@npm:^0.74.1": + version: 0.74.89 + resolution: "@react-native/normalize-colors@npm:0.74.89" + checksum: 10c0/6d0e5c91793ca5a66b4a0e5995361f474caacac56bde4772ac02b8ab470bd323076c567bd8856b0b097816d2b890e73a4040a3df01fd284adee683f5ba89d5ba + languageName: node + linkType: hard + "@react-native/typescript-config@npm:0.81.5": version: 0.81.5 resolution: "@react-native/typescript-config@npm:0.81.5" @@ -2514,6 +2807,188 @@ __metadata: languageName: node linkType: hard +"@rolldown/pluginutils@npm:1.0.0-rc.3": + version: 1.0.0-rc.3 + resolution: "@rolldown/pluginutils@npm:1.0.0-rc.3" + checksum: 10c0/3928b6282a30f307d1b075d2f217180ae173ea9e00638ce46ab65f089bd5f7a0b2c488ae1ce530f509387793c656a2910337c4cd68fa9d37d7e439365989e699 + languageName: node + linkType: hard + +"@rollup/rollup-android-arm-eabi@npm:4.60.1": + version: 4.60.1 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.60.1" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@rollup/rollup-android-arm64@npm:4.60.1": + version: 4.60.1 + resolution: "@rollup/rollup-android-arm64@npm:4.60.1" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-darwin-arm64@npm:4.60.1": + version: 4.60.1 + resolution: "@rollup/rollup-darwin-arm64@npm:4.60.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-darwin-x64@npm:4.60.1": + version: 4.60.1 + resolution: "@rollup/rollup-darwin-x64@npm:4.60.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-freebsd-arm64@npm:4.60.1": + version: 4.60.1 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.60.1" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-freebsd-x64@npm:4.60.1": + version: 4.60.1 + resolution: "@rollup/rollup-freebsd-x64@npm:4.60.1" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm-gnueabihf@npm:4.60.1": + version: 4.60.1 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.60.1" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm-musleabihf@npm:4.60.1": + version: 4.60.1 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.60.1" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm64-gnu@npm:4.60.1": + version: 4.60.1 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.60.1" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm64-musl@npm:4.60.1": + version: 4.60.1 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.60.1" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-loong64-gnu@npm:4.60.1": + version: 4.60.1 + resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.60.1" + conditions: os=linux & cpu=loong64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-loong64-musl@npm:4.60.1": + version: 4.60.1 + resolution: "@rollup/rollup-linux-loong64-musl@npm:4.60.1" + conditions: os=linux & cpu=loong64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-ppc64-gnu@npm:4.60.1": + version: 4.60.1 + resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.60.1" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-ppc64-musl@npm:4.60.1": + version: 4.60.1 + resolution: "@rollup/rollup-linux-ppc64-musl@npm:4.60.1" + conditions: os=linux & cpu=ppc64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-riscv64-gnu@npm:4.60.1": + version: 4.60.1 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.60.1" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-riscv64-musl@npm:4.60.1": + version: 4.60.1 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.60.1" + conditions: os=linux & cpu=riscv64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-s390x-gnu@npm:4.60.1": + version: 4.60.1 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.60.1" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-x64-gnu@npm:4.60.1": + version: 4.60.1 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.60.1" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-x64-musl@npm:4.60.1": + version: 4.60.1 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.60.1" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-openbsd-x64@npm:4.60.1": + version: 4.60.1 + resolution: "@rollup/rollup-openbsd-x64@npm:4.60.1" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-openharmony-arm64@npm:4.60.1": + version: 4.60.1 + resolution: "@rollup/rollup-openharmony-arm64@npm:4.60.1" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-arm64-msvc@npm:4.60.1": + version: 4.60.1 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.60.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-ia32-msvc@npm:4.60.1": + version: 4.60.1 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.60.1" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@rollup/rollup-win32-x64-gnu@npm:4.60.1": + version: 4.60.1 + resolution: "@rollup/rollup-win32-x64-gnu@npm:4.60.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-x64-msvc@npm:4.60.1": + version: 4.60.1 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.60.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@sideway/address@npm:^4.1.5": version: 4.1.5 resolution: "@sideway/address@npm:4.1.5" @@ -2569,7 +3044,14 @@ __metadata: languageName: node linkType: hard -"@types/babel__core@npm:^7.1.14": +"@sqlite.org/sqlite-wasm@npm:^3.51.2-build8": + version: 3.51.2-build8 + resolution: "@sqlite.org/sqlite-wasm@npm:3.51.2-build8" + checksum: 10c0/f193298c13a2e156897ca4dcae524f4c84c6004b7175baefc36dacec35b1ab3f7868f69d0ce975adfe64cb20c2cd15efba6285d3a48439ae4b556eb7f923e749 + languageName: node + linkType: hard + +"@types/babel__core@npm:^7.1.14, @types/babel__core@npm:^7.20.5": version: 7.20.5 resolution: "@types/babel__core@npm:7.20.5" dependencies: @@ -2626,6 +3108,13 @@ __metadata: languageName: node linkType: hard +"@types/estree@npm:1.0.8": + version: 1.0.8 + resolution: "@types/estree@npm:1.0.8" + checksum: 10c0/39d34d1afaa338ab9763f37ad6066e3f349444f9052b9676a7cc0252ef9485a41c6d81c9c4e0d26e9077993354edf25efc853f3224dd4b447175ef62bdcc86a5 + languageName: node + linkType: hard + "@types/graceful-fs@npm:^4.1.3": version: 4.1.9 resolution: "@types/graceful-fs@npm:4.1.9" @@ -2730,6 +3219,22 @@ __metadata: languageName: node linkType: hard +"@vitejs/plugin-react@npm:^5.1.0": + version: 5.2.0 + resolution: "@vitejs/plugin-react@npm:5.2.0" + dependencies: + "@babel/core": "npm:^7.29.0" + "@babel/plugin-transform-react-jsx-self": "npm:^7.27.1" + "@babel/plugin-transform-react-jsx-source": "npm:^7.27.1" + "@rolldown/pluginutils": "npm:1.0.0-rc.3" + "@types/babel__core": "npm:^7.20.5" + react-refresh: "npm:^0.18.0" + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 10c0/bac0a409e71eee954a05bc41580411c369bd5f9ef0586a1f9743fba76ad6603c437d93d407d230780015361f93d1592c55e53314813cded6369c36d3c1e8edbf + languageName: node + linkType: hard + "@vscode/sudo-prompt@npm:^9.0.0": version: 9.3.2 resolution: "@vscode/sudo-prompt@npm:9.3.2" @@ -2960,7 +3465,7 @@ __metadata: languageName: node linkType: hard -"asap@npm:~2.0.6": +"asap@npm:~2.0.3, asap@npm:~2.0.6": version: 2.0.6 resolution: "asap@npm:2.0.6" checksum: 10c0/c6d5e39fe1f15e4b87677460bd66b66050cd14c772269cee6688824c1410a08ab20254bb6784f9afb75af9144a9f9a7692d49547f4d19d715aeb7c0318f3136d @@ -3799,6 +4304,15 @@ __metadata: languageName: node linkType: hard +"cross-fetch@npm:^3.1.5": + version: 3.2.0 + resolution: "cross-fetch@npm:3.2.0" + dependencies: + node-fetch: "npm:^2.7.0" + checksum: 10c0/d8596adf0269130098a676f6739a0922f3cc7b71cc89729925411ebe851a87026171c82ea89154c4811c9867c01c44793205a52e618ce2684650218c7fbeeb9f + languageName: node + linkType: hard + "cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" @@ -3810,6 +4324,15 @@ __metadata: languageName: node linkType: hard +"css-in-js-utils@npm:^3.1.0": + version: 3.1.0 + resolution: "css-in-js-utils@npm:3.1.0" + dependencies: + hyphenate-style-name: "npm:^1.0.3" + checksum: 10c0/8bb042e8f7701a7edadc3cce5ce2d5cf41189631d7e2aed194d5a7059b25776dded2a0466cb9da1d1f3fc6c99dcecb51e45671148d073b8a2a71e34755152e52 + languageName: node + linkType: hard + "cssesc@npm:^3.0.0": version: 3.0.0 resolution: "cssesc@npm:3.0.0" @@ -4140,6 +4663,95 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:^0.27.0": + version: 0.27.7 + resolution: "esbuild@npm:0.27.7" + dependencies: + "@esbuild/aix-ppc64": "npm:0.27.7" + "@esbuild/android-arm": "npm:0.27.7" + "@esbuild/android-arm64": "npm:0.27.7" + "@esbuild/android-x64": "npm:0.27.7" + "@esbuild/darwin-arm64": "npm:0.27.7" + "@esbuild/darwin-x64": "npm:0.27.7" + "@esbuild/freebsd-arm64": "npm:0.27.7" + "@esbuild/freebsd-x64": "npm:0.27.7" + "@esbuild/linux-arm": "npm:0.27.7" + "@esbuild/linux-arm64": "npm:0.27.7" + "@esbuild/linux-ia32": "npm:0.27.7" + "@esbuild/linux-loong64": "npm:0.27.7" + "@esbuild/linux-mips64el": "npm:0.27.7" + "@esbuild/linux-ppc64": "npm:0.27.7" + "@esbuild/linux-riscv64": "npm:0.27.7" + "@esbuild/linux-s390x": "npm:0.27.7" + "@esbuild/linux-x64": "npm:0.27.7" + "@esbuild/netbsd-arm64": "npm:0.27.7" + "@esbuild/netbsd-x64": "npm:0.27.7" + "@esbuild/openbsd-arm64": "npm:0.27.7" + "@esbuild/openbsd-x64": "npm:0.27.7" + "@esbuild/openharmony-arm64": "npm:0.27.7" + "@esbuild/sunos-x64": "npm:0.27.7" + "@esbuild/win32-arm64": "npm:0.27.7" + "@esbuild/win32-ia32": "npm:0.27.7" + "@esbuild/win32-x64": "npm:0.27.7" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-arm64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-arm64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/openharmony-arm64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10c0/ccd51f0555708bc9ff4ec9dc3ac92d3daacd45ecaac949ca8645984c5c323bf8cefe98c2df307418685e0b4ce37f9a3bdbfe8e3651fe632a0059a436195a17d4 + languageName: node + linkType: hard + "escalade@npm:^3.1.1, escalade@npm:^3.2.0": version: 3.2.0 resolution: "escalade@npm:3.2.0" @@ -4353,6 +4965,28 @@ __metadata: languageName: node linkType: hard +"fbjs-css-vars@npm:^1.0.0": + version: 1.0.2 + resolution: "fbjs-css-vars@npm:1.0.2" + checksum: 10c0/dfb64116b125a64abecca9e31477b5edb9a2332c5ffe74326fe36e0a72eef7fc8a49b86adf36c2c293078d79f4524f35e80f5e62546395f53fb7c9e69821f54f + languageName: node + linkType: hard + +"fbjs@npm:^3.0.4": + version: 3.0.5 + resolution: "fbjs@npm:3.0.5" + dependencies: + cross-fetch: "npm:^3.1.5" + fbjs-css-vars: "npm:^1.0.0" + loose-envify: "npm:^1.0.0" + object-assign: "npm:^4.1.0" + promise: "npm:^7.1.1" + setimmediate: "npm:^1.0.5" + ua-parser-js: "npm:^1.0.35" + checksum: 10c0/66d0a2fc9a774f9066e35ac2ac4bf1245931d27f3ac287c7d47e6aa1fc152b243c2109743eb8f65341e025621fb51a12038fadb9fd8fda2e3ddae04ebab06f91 + languageName: node + linkType: hard + "fdir@npm:^6.5.0": version: 6.5.0 resolution: "fdir@npm:6.5.0" @@ -4494,7 +5128,7 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:^2.3.2, fsevents@npm:~2.3.2": +"fsevents@npm:^2.3.2, fsevents@npm:~2.3.2, fsevents@npm:~2.3.3": version: 2.3.3 resolution: "fsevents@npm:2.3.3" dependencies: @@ -4504,7 +5138,7 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": +"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" dependencies: @@ -4855,6 +5489,13 @@ __metadata: languageName: node linkType: hard +"hyphenate-style-name@npm:^1.0.3": + version: 1.1.0 + resolution: "hyphenate-style-name@npm:1.1.0" + checksum: 10c0/bfe88deac2414a41a0d08811e277c8c098f23993d6a1eb17f14a0f11b54c4d42865a63d3cfe1914668eefb9a188e2de58f38b55a179a238fd1fef606893e194f + languageName: node + linkType: hard + "iconv-lite@npm:^0.7.2": version: 0.7.2 resolution: "iconv-lite@npm:0.7.2" @@ -4958,6 +5599,15 @@ __metadata: languageName: node linkType: hard +"inline-style-prefixer@npm:^7.0.1": + version: 7.0.1 + resolution: "inline-style-prefixer@npm:7.0.1" + dependencies: + css-in-js-utils: "npm:^3.1.0" + checksum: 10c0/15da5a396b7f286b5b6742efe315218cd577bc96b43de08aeb76af7697d9f1ab3bfc66cf19fad2173957dd5d617a790240b9d51898bdcf4c2efb40d3f8bcb370 + languageName: node + linkType: hard + "invariant@npm:^2.2.4": version: 2.2.4 resolution: "invariant@npm:2.2.4" @@ -6137,6 +6787,13 @@ __metadata: languageName: node linkType: hard +"memoize-one@npm:^6.0.0": + version: 6.0.0 + resolution: "memoize-one@npm:6.0.0" + checksum: 10c0/45c88e064fd715166619af72e8cf8a7a17224d6edf61f7a8633d740ed8c8c0558a4373876c9b8ffc5518c2b65a960266adf403cc215cb1e90f7e262b58991f54 + languageName: node + linkType: hard + "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -6667,6 +7324,20 @@ __metadata: languageName: node linkType: hard +"node-fetch@npm:^2.7.0": + version: 2.7.0 + resolution: "node-fetch@npm:2.7.0" + dependencies: + whatwg-url: "npm:^5.0.0" + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: 10c0/b55786b6028208e6fbe594ccccc213cab67a72899c9234eb59dba51062a299ea853210fcf526998eaa2867b0963ad72338824450905679ff0fa304b8c5093ae8 + languageName: node + linkType: hard + "node-gyp@npm:latest": version: 12.2.0 resolution: "node-gyp@npm:12.2.0" @@ -6761,7 +7432,7 @@ __metadata: languageName: node linkType: hard -"object-assign@npm:^4.0.1": +"object-assign@npm:^4.0.1, object-assign@npm:^4.1.0": version: 4.1.1 resolution: "object-assign@npm:4.1.1" checksum: 10c0/1f4df9945120325d041ccf7b86f31e8bcc14e73d29171e37a7903050e96b81323784ec59f93f102ec635bcf6fa8034ba3ea0a8c7e69fa202b87ae3b6cec5a414 @@ -6846,19 +7517,24 @@ __metadata: "@react-native/babel-preset": "npm:0.82.1" "@react-native/metro-config": "npm:0.82.1" "@react-native/typescript-config": "npm:0.81.5" + "@sqlite.org/sqlite-wasm": "npm:^3.51.2-build8" "@types/chance": "npm:^1.1.7" "@types/react": "npm:^19.1.1" + "@vitejs/plugin-react": "npm:^5.1.0" chance: "npm:^1.1.9" clsx: "npm:^2.0.0" events: "npm:^3.3.0" patch-package: "npm:^8.0.1" react: "npm:19.1.1" + react-dom: "npm:19.1.1" react-native: "npm:0.82.1" react-native-builder-bob: "npm:^0.40.13" react-native-monorepo-config: "npm:^0.1.9" react-native-restart: "npm:^0.0.27" react-native-safe-area-context: "npm:^5.6.2" + react-native-web: "npm:^0.21.2" tailwindcss: "npm:3.3.2" + vite: "npm:^7.1.9" languageName: unknown linkType: soft @@ -7185,7 +7861,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.4.23": +"postcss@npm:^8.4.23, postcss@npm:^8.5.6": version: 8.5.8 resolution: "postcss@npm:8.5.8" dependencies: @@ -7259,6 +7935,15 @@ __metadata: languageName: node linkType: hard +"promise@npm:^7.1.1": + version: 7.3.1 + resolution: "promise@npm:7.3.1" + dependencies: + asap: "npm:~2.0.3" + checksum: 10c0/742e5c0cc646af1f0746963b8776299701ad561ce2c70b49365d62c8db8ea3681b0a1bf0d4e2fe07910bf72f02d39e51e8e73dc8d7503c3501206ac908be107f + languageName: node + linkType: hard + "promise@npm:^8.3.0": version: 8.3.0 resolution: "promise@npm:8.3.0" @@ -7363,6 +8048,17 @@ __metadata: languageName: node linkType: hard +"react-dom@npm:19.1.1": + version: 19.1.1 + resolution: "react-dom@npm:19.1.1" + dependencies: + scheduler: "npm:^0.26.0" + peerDependencies: + react: ^19.1.1 + checksum: 10c0/8c91198510521299c56e4e8d5e3a4508b2734fb5e52f29eeac33811de64e76fe586ad32c32182e2e84e070d98df67125da346c3360013357228172dbcd20bcdd + languageName: node + linkType: hard + "react-is@npm:^17.0.1": version: 17.0.2 resolution: "react-is@npm:17.0.2" @@ -7449,6 +8145,25 @@ __metadata: languageName: node linkType: hard +"react-native-web@npm:^0.21.2": + version: 0.21.2 + resolution: "react-native-web@npm:0.21.2" + dependencies: + "@babel/runtime": "npm:^7.18.6" + "@react-native/normalize-colors": "npm:^0.74.1" + fbjs: "npm:^3.0.4" + inline-style-prefixer: "npm:^7.0.1" + memoize-one: "npm:^6.0.0" + nullthrows: "npm:^1.1.1" + postcss-value-parser: "npm:^4.2.0" + styleq: "npm:^0.1.3" + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + checksum: 10c0/8c184fef0045c25deff765c8e80963454a5dffd8e389a9e11cf2fec9e769ff0f82c3d56d082b1897a7ded8374d9ae8a49dac7f09377a104f1995a5ddea645095 + languageName: node + linkType: hard + "react-native@npm:0.82.1": version: 0.82.1 resolution: "react-native@npm:0.82.1" @@ -7507,6 +8222,13 @@ __metadata: languageName: node linkType: hard +"react-refresh@npm:^0.18.0": + version: 0.18.0 + resolution: "react-refresh@npm:0.18.0" + checksum: 10c0/34a262f7fd803433a534f50deb27a148112a81adcae440c7d1cbae7ef14d21ea8f2b3d783e858cb7698968183b77755a38b4d4b5b1d79b4f4689c2f6d358fff2 + languageName: node + linkType: hard + "react@npm:19.1.1": version: 19.1.1 resolution: "react@npm:19.1.1" @@ -7703,6 +8425,96 @@ __metadata: languageName: node linkType: hard +"rollup@npm:^4.43.0": + version: 4.60.1 + resolution: "rollup@npm:4.60.1" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.60.1" + "@rollup/rollup-android-arm64": "npm:4.60.1" + "@rollup/rollup-darwin-arm64": "npm:4.60.1" + "@rollup/rollup-darwin-x64": "npm:4.60.1" + "@rollup/rollup-freebsd-arm64": "npm:4.60.1" + "@rollup/rollup-freebsd-x64": "npm:4.60.1" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.60.1" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.60.1" + "@rollup/rollup-linux-arm64-gnu": "npm:4.60.1" + "@rollup/rollup-linux-arm64-musl": "npm:4.60.1" + "@rollup/rollup-linux-loong64-gnu": "npm:4.60.1" + "@rollup/rollup-linux-loong64-musl": "npm:4.60.1" + "@rollup/rollup-linux-ppc64-gnu": "npm:4.60.1" + "@rollup/rollup-linux-ppc64-musl": "npm:4.60.1" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.60.1" + "@rollup/rollup-linux-riscv64-musl": "npm:4.60.1" + "@rollup/rollup-linux-s390x-gnu": "npm:4.60.1" + "@rollup/rollup-linux-x64-gnu": "npm:4.60.1" + "@rollup/rollup-linux-x64-musl": "npm:4.60.1" + "@rollup/rollup-openbsd-x64": "npm:4.60.1" + "@rollup/rollup-openharmony-arm64": "npm:4.60.1" + "@rollup/rollup-win32-arm64-msvc": "npm:4.60.1" + "@rollup/rollup-win32-ia32-msvc": "npm:4.60.1" + "@rollup/rollup-win32-x64-gnu": "npm:4.60.1" + "@rollup/rollup-win32-x64-msvc": "npm:4.60.1" + "@types/estree": "npm:1.0.8" + fsevents: "npm:~2.3.2" + dependenciesMeta: + "@rollup/rollup-android-arm-eabi": + optional: true + "@rollup/rollup-android-arm64": + optional: true + "@rollup/rollup-darwin-arm64": + optional: true + "@rollup/rollup-darwin-x64": + optional: true + "@rollup/rollup-freebsd-arm64": + optional: true + "@rollup/rollup-freebsd-x64": + optional: true + "@rollup/rollup-linux-arm-gnueabihf": + optional: true + "@rollup/rollup-linux-arm-musleabihf": + optional: true + "@rollup/rollup-linux-arm64-gnu": + optional: true + "@rollup/rollup-linux-arm64-musl": + optional: true + "@rollup/rollup-linux-loong64-gnu": + optional: true + "@rollup/rollup-linux-loong64-musl": + optional: true + "@rollup/rollup-linux-ppc64-gnu": + optional: true + "@rollup/rollup-linux-ppc64-musl": + optional: true + "@rollup/rollup-linux-riscv64-gnu": + optional: true + "@rollup/rollup-linux-riscv64-musl": + optional: true + "@rollup/rollup-linux-s390x-gnu": + optional: true + "@rollup/rollup-linux-x64-gnu": + optional: true + "@rollup/rollup-linux-x64-musl": + optional: true + "@rollup/rollup-openbsd-x64": + optional: true + "@rollup/rollup-openharmony-arm64": + optional: true + "@rollup/rollup-win32-arm64-msvc": + optional: true + "@rollup/rollup-win32-ia32-msvc": + optional: true + "@rollup/rollup-win32-x64-gnu": + optional: true + "@rollup/rollup-win32-x64-msvc": + optional: true + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: 10c0/48d3f2216b5533639b007e6756e2275c7f594e45adee21ce03674aa2e004406c661f8b86c7a0b471c9e889c6a9efbb29240ca0b7673c50e391406c490c309833 + languageName: node + linkType: hard + "run-parallel@npm:^1.1.9": version: 1.2.0 resolution: "run-parallel@npm:1.2.0" @@ -7726,7 +8538,7 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:0.26.0": +"scheduler@npm:0.26.0, scheduler@npm:^0.26.0": version: 0.26.0 resolution: "scheduler@npm:0.26.0" checksum: 10c0/5b8d5bfddaae3513410eda54f2268e98a376a429931921a81b5c3a2873aab7ca4d775a8caac5498f8cbc7d0daeab947cf923dbd8e215d61671f9f4e392d34356 @@ -7812,6 +8624,13 @@ __metadata: languageName: node linkType: hard +"setimmediate@npm:^1.0.5": + version: 1.0.5 + resolution: "setimmediate@npm:1.0.5" + checksum: 10c0/5bae81bfdbfbd0ce992893286d49c9693c82b1bcc00dcaaf3a09c8f428fdeacf4190c013598b81875dfac2b08a572422db7df779a99332d0fce186d15a3e4d49 + languageName: node + linkType: hard + "setprototypeof@npm:~1.2.0": version: 1.2.0 resolution: "setprototypeof@npm:1.2.0" @@ -8181,6 +9000,13 @@ __metadata: languageName: node linkType: hard +"styleq@npm:^0.1.3": + version: 0.1.3 + resolution: "styleq@npm:0.1.3" + checksum: 10c0/975d951792e65052f1f6e41aaad46492642ce4922b3dc36d4b49b37c8509f9a776794d8f275360f00116a5e6ab1e31514bdcd5840656c4e3213da6803fa12941 + languageName: node + linkType: hard + "sucrase@npm:^3.32.0": version: 3.35.1 resolution: "sucrase@npm:3.35.1" @@ -8346,7 +9172,7 @@ __metadata: languageName: node linkType: hard -"tinyglobby@npm:^0.2.11, tinyglobby@npm:^0.2.12": +"tinyglobby@npm:^0.2.11, tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.15": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" dependencies: @@ -8386,6 +9212,13 @@ __metadata: languageName: node linkType: hard +"tr46@npm:~0.0.3": + version: 0.0.3 + resolution: "tr46@npm:0.0.3" + checksum: 10c0/047cb209a6b60c742f05c9d3ace8fa510bff609995c129a37ace03476a9b12db4dbf975e74600830ef0796e18882b2381fb5fb1f6b4f96b832c374de3ab91a11 + languageName: node + linkType: hard + "ts-interface-checker@npm:^0.1.9": version: 0.1.13 resolution: "ts-interface-checker@npm:0.1.13" @@ -8500,6 +9333,15 @@ __metadata: languageName: node linkType: hard +"ua-parser-js@npm:^1.0.35": + version: 1.0.41 + resolution: "ua-parser-js@npm:1.0.41" + bin: + ua-parser-js: script/cli.js + checksum: 10c0/45dc1f7f3ce8248e0e64640d2e29c65c0ea1fc9cb105594de84af80e2a57bba4f718b9376098ca7a5b0ffe240f8995b0fa3714afa9d36861c41370a378f1a274 + languageName: node + linkType: hard + "uglify-js@npm:^3.1.4": version: 3.19.3 resolution: "uglify-js@npm:3.19.3" @@ -8639,6 +9481,61 @@ __metadata: languageName: node linkType: hard +"vite@npm:^7.1.9": + version: 7.3.1 + resolution: "vite@npm:7.3.1" + dependencies: + esbuild: "npm:^0.27.0" + fdir: "npm:^6.5.0" + fsevents: "npm:~2.3.3" + picomatch: "npm:^4.0.3" + postcss: "npm:^8.5.6" + rollup: "npm:^4.43.0" + tinyglobby: "npm:^0.2.15" + peerDependencies: + "@types/node": ^20.19.0 || >=22.12.0 + jiti: ">=1.21.0" + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: ">=0.54.8" + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + bin: + vite: bin/vite.js + checksum: 10c0/5c7548f5f43a23533e53324304db4ad85f1896b1bfd3ee32ae9b866bac2933782c77b350eb2b52a02c625c8ad1ddd4c000df077419410650c982cd97fde8d014 + languageName: node + linkType: hard + "vlq@npm:^1.0.0": version: 1.0.1 resolution: "vlq@npm:1.0.1" @@ -8664,6 +9561,13 @@ __metadata: languageName: node linkType: hard +"webidl-conversions@npm:^3.0.0": + version: 3.0.1 + resolution: "webidl-conversions@npm:3.0.1" + checksum: 10c0/5612d5f3e54760a797052eb4927f0ddc01383550f542ccd33d5238cfd65aeed392a45ad38364970d0a0f4fea32e1f4d231b3d8dac4a3bdd385e5cf802ae097db + languageName: node + linkType: hard + "whatwg-fetch@npm:^3.0.0": version: 3.6.20 resolution: "whatwg-fetch@npm:3.6.20" @@ -8671,6 +9575,16 @@ __metadata: languageName: node linkType: hard +"whatwg-url@npm:^5.0.0": + version: 5.0.0 + resolution: "whatwg-url@npm:5.0.0" + dependencies: + tr46: "npm:~0.0.3" + webidl-conversions: "npm:^3.0.0" + checksum: 10c0/1588bed84d10b72d5eec1d0faa0722ba1962f1821e7539c535558fb5398d223b0c50d8acab950b8c488b4ba69043fd833cc2697056b167d8ad46fac3995a55d5 + languageName: node + linkType: hard + "which-module@npm:^2.0.0": version: 2.0.1 resolution: "which-module@npm:2.0.1"