diff --git a/bindings/javascript/__test__/dual-test.mjs b/bindings/javascript/__test__/dual-test.mjs index 75e5c265f..5b03dba14 100644 --- a/bindings/javascript/__test__/dual-test.mjs +++ b/bindings/javascript/__test__/dual-test.mjs @@ -1,5 +1,5 @@ import avaTest from "ava"; -import turso from "../wrapper.js"; +import turso from "../sync.js"; import sqlite from "better-sqlite3"; class DualTest { diff --git a/bindings/javascript/package.json b/bindings/javascript/package.json index 502d940af..15918b636 100644 --- a/bindings/javascript/package.json +++ b/bindings/javascript/package.json @@ -6,12 +6,17 @@ "url": "https://github.com/tursodatabase/turso" }, "description": "The Turso database library", - "main": "wrapper.js", + "main": "promise.js", + "exports": { + ".": "./promise.js", + "./sync": "./sync.js" + }, "files": [ "browser.js", "index.js", + "promise.js", "sqlite-error.js", - "wrapper.js" + "sync.js" ], "types": "index.d.ts", "napi": { @@ -46,4 +51,4 @@ "version": "napi version" }, "packageManager": "yarn@4.9.2" -} \ No newline at end of file +} diff --git a/bindings/javascript/wrapper.js b/bindings/javascript/promise.js similarity index 100% rename from bindings/javascript/wrapper.js rename to bindings/javascript/promise.js diff --git a/bindings/javascript/sync.js b/bindings/javascript/sync.js new file mode 100644 index 000000000..64d4d10c6 --- /dev/null +++ b/bindings/javascript/sync.js @@ -0,0 +1,315 @@ +"use strict"; + +const { Database: NativeDB } = require("./index.js"); + +const SqliteError = require("./sqlite-error.js"); + +const convertibleErrorTypes = { TypeError }; +const CONVERTIBLE_ERROR_PREFIX = "[TURSO_CONVERT_TYPE]"; + +function convertError(err) { + if ((err.code ?? "").startsWith(CONVERTIBLE_ERROR_PREFIX)) { + return createErrorByName( + err.code.substring(CONVERTIBLE_ERROR_PREFIX.length), + err.message, + ); + } + + return new SqliteError(err.message, err.code, err.rawCode); +} + +function createErrorByName(name, message) { + const ErrorConstructor = convertibleErrorTypes[name]; + if (!ErrorConstructor) { + throw new Error(`unknown error type ${name} from Turso`); + } + + return new ErrorConstructor(message); +} + +/** + * Database represents a connection that can prepare and execute SQL statements. + */ +class Database { + /** + * Creates a new database connection. If the database file pointed to by `path` does not exists, it will be created. + * + * @constructor + * @param {string} path - Path to the database file. + * @param {Object} opts - Options for database behavior. + * @param {boolean} [opts.readonly=false] - Open the database in read-only mode. + * @param {boolean} [opts.fileMustExist=false] - If true, throws if database file does not exist. + * @param {number} [opts.timeout=0] - Timeout duration in milliseconds for database operations. Defaults to 0 (no timeout). + */ + constructor(path, opts = {}) { + opts.readonly = opts.readonly === undefined ? false : opts.readonly; + opts.fileMustExist = + opts.fileMustExist === undefined ? false : opts.fileMustExist; + opts.timeout = opts.timeout === undefined ? 0 : opts.timeout; + + this.db = new NativeDB(path, opts); + this.memory = this.db.memory; + const db = this.db; + + Object.defineProperties(this, { + inTransaction: { + get() { + return db.inTransaction(); + }, + }, + name: { + get() { + return path; + }, + }, + readonly: { + get() { + return opts.readonly; + }, + }, + open: { + get() { + return this.db.open; + }, + }, + }); + } + + /** + * Prepares a SQL statement for execution. + * + * @param {string} sql - The SQL statement string to prepare. + */ + prepare(sql) { + if (!sql) { + throw new RangeError("The supplied SQL string contains no statements"); + } + + try { + return new Statement(this.db.prepare(sql), this); + } catch (err) { + throw convertError(err); + } + } + + /** + * Returns a function that executes the given function in a transaction. + * + * @param {function} fn - The function to wrap in a transaction. + */ + transaction(fn) { + if (typeof fn !== "function") + throw new TypeError("Expected first argument to be a function"); + + const db = this; + const wrapTxn = (mode) => { + return (...bindParameters) => { + db.exec("BEGIN " + mode); + try { + const result = fn(...bindParameters); + db.exec("COMMIT"); + return result; + } catch (err) { + db.exec("ROLLBACK"); + throw err; + } + }; + }; + const properties = { + default: { value: wrapTxn("") }, + deferred: { value: wrapTxn("DEFERRED") }, + immediate: { value: wrapTxn("IMMEDIATE") }, + exclusive: { value: wrapTxn("EXCLUSIVE") }, + database: { value: this, enumerable: true }, + }; + Object.defineProperties(properties.default.value, properties); + Object.defineProperties(properties.deferred.value, properties); + Object.defineProperties(properties.immediate.value, properties); + Object.defineProperties(properties.exclusive.value, properties); + return properties.default.value; + } + + pragma(source, options) { + if (options == null) options = {}; + + if (typeof source !== "string") + throw new TypeError("Expected first argument to be a string"); + + if (typeof options !== "object") + throw new TypeError("Expected second argument to be an options object"); + + const simple = options["simple"]; + const pragma = `PRAGMA ${source}`; + + return simple + ? this.db.pragma(source, { simple: true }) + : this.db.pragma(source); + } + + backup(filename, options) { + throw new Error("not implemented"); + } + + serialize(options) { + throw new Error("not implemented"); + } + + function(name, options, fn) { + throw new Error("not implemented"); + } + + aggregate(name, options) { + throw new Error("not implemented"); + } + + table(name, factory) { + throw new Error("not implemented"); + } + + loadExtension(path) { + this.db.loadExtension(path); + } + + maxWriteReplicationIndex() { + throw new Error("not implemented"); + } + + /** + * Executes a SQL statement. + * + * @param {string} sql - The SQL statement string to execute. + */ + exec(sql) { + try { + this.db.exec(sql); + } catch (err) { + throw convertError(err); + } + } + + /** + * Interrupts the database connection. + */ + interrupt() { + this.db.interrupt(); + } + + /** + * Closes the database connection. + */ + close() { + this.db.close(); + } +} + +/** + * Statement represents a prepared SQL statement that can be executed. + */ +class Statement { + constructor(stmt, database) { + this.stmt = stmt; + this.db = database; + } + + /** + * Toggle raw mode. + * + * @param raw Enable or disable raw mode. If you don't pass the parameter, raw mode is enabled. + */ + raw(raw) { + this.stmt.raw(raw); + return this; + } + + /** + * Toggle pluck mode. + * + * @param pluckMode Enable or disable pluck mode. If you don't pass the parameter, pluck mode is enabled. + */ + pluck(pluckMode) { + this.stmt.pluck(pluckMode); + return this; + } + + get source() { + return this.stmt.source; + } + + get reader() { + throw new Error("not implemented"); + } + + get source() { + return this.stmt.source; + } + + get database() { + return this.db; + } + + /** + * Executes the SQL statement and returns an info object. + */ + run(...bindParameters) { + return this.stmt.run(bindParameters.flat()); + } + + /** + * Executes the SQL statement and returns the first row. + * + * @param bindParameters - The bind parameters for executing the statement. + */ + get(...bindParameters) { + return this.stmt.get(bindParameters.flat()); + } + + /** + * Executes the SQL statement and returns an iterator to the resulting rows. + * + * @param bindParameters - The bind parameters for executing the statement. + */ + *iterate(...bindParameters) { + throw new Error("not implemented"); + } + + /** + * Executes the SQL statement and returns an array of the resulting rows. + * + * @param bindParameters - The bind parameters for executing the statement. + */ + all(...bindParameters) { + return this.stmt.all(bindParameters.flat()); + } + + /** + * Interrupts the statement. + */ + interrupt() { + this.stmt.interrupt(); + return this; + } + + /** + * Returns the columns in the result set returned by this prepared statement. + */ + columns() { + return this.stmt.columns(); + } + + /** + * Binds the given parameters to the statement _permanently_ + * + * @param bindParameters - The bind parameters for binding the statement. + * @returns this - Statement with binded parameters + */ + bind(...bindParameters) { + try { + return new Statement(this.stmt.bind(bindParameters.flat()), this.db); + } catch (err) { + throw convertError(err); + } + } +} + +module.exports = Database; +module.exports.SqliteError = SqliteError;