diff --git a/bindings/javascript/compat.ts b/bindings/javascript/compat.ts index 006ccfa22..3b99f0772 100644 --- a/bindings/javascript/compat.ts +++ b/bindings/javascript/compat.ts @@ -38,6 +38,7 @@ class Database { db: NativeDB; memory: boolean; open: boolean; + private _inTransaction: boolean = false; /** * Creates a new database connection. If the database file pointed to by `path` does not exists, it will be created. @@ -61,9 +62,7 @@ class Database { Object.defineProperties(this, { inTransaction: { - get() { - throw new Error("not implemented"); - }, + get: () => this._inTransaction, }, name: { get() { @@ -117,12 +116,15 @@ class Database { const wrapTxn = (mode) => { return (...bindParameters) => { db.exec("BEGIN " + mode); + db._inTransaction = true; try { const result = fn(...bindParameters); db.exec("COMMIT"); + db._inTransaction = false; return result; } catch (err) { db.exec("ROLLBACK"); + db._inTransaction = false; throw err; } }; diff --git a/bindings/javascript/promise.ts b/bindings/javascript/promise.ts index f858704c0..04df99d9f 100644 --- a/bindings/javascript/promise.ts +++ b/bindings/javascript/promise.ts @@ -38,6 +38,7 @@ class Database { db: NativeDB; memory: boolean; open: boolean; + private _inTransaction: boolean = false; /** * Creates a new database connection. If the database file pointed to by `path` does not exists, it will be created. * @@ -65,9 +66,7 @@ class Database { this.memory = db.memory; Object.defineProperties(this, { inTransaction: { - get() { - throw new Error("not implemented"); - }, + get: () => this._inTransaction, }, name: { get() { @@ -121,12 +120,15 @@ class Database { const wrapTxn = (mode) => { return (...bindParameters) => { db.exec("BEGIN " + mode); + db._inTransaction = true; try { const result = fn(...bindParameters); db.exec("COMMIT"); + db._inTransaction = false; return result; } catch (err) { db.exec("ROLLBACK"); + db._inTransaction = false; throw err; } }; diff --git a/packages/turso-serverless/src/connection.ts b/packages/turso-serverless/src/connection.ts index 372319b1f..0c0082eb3 100644 --- a/packages/turso-serverless/src/connection.ts +++ b/packages/turso-serverless/src/connection.ts @@ -17,6 +17,7 @@ export class Connection { private session: Session; private isOpen: boolean = true; private defaultSafeIntegerMode: boolean = false; + private _inTransaction: boolean = false; constructor(config: Config) { if (!config.url) { @@ -24,6 +25,19 @@ export class Connection { } this.config = config; this.session = new Session(config); + + // Define inTransaction property + Object.defineProperty(this, 'inTransaction', { + get: () => this._inTransaction, + enumerable: true + }); + } + + /** + * Whether the database is currently in a transaction. + */ + get inTransaction(): boolean { + return this._inTransaction; } /** @@ -143,6 +157,63 @@ export class Connection { this.defaultSafeIntegerMode = toggle === false ? false : true; } + /** + * Returns a function that executes the given function in a transaction. + * + * @param fn - The function to wrap in a transaction + * @returns A function that will execute fn within a transaction + * + * @example + * ```typescript + * const insert = await client.prepare("INSERT INTO users (name) VALUES (?)"); + * const insertMany = client.transaction((users) => { + * for (const user of users) { + * insert.run([user]); + * } + * }); + * + * await insertMany(['Alice', 'Bob', 'Charlie']); + * ``` + */ + transaction(fn: (...args: any[]) => any): any { + if (typeof fn !== "function") { + throw new TypeError("Expected first argument to be a function"); + } + + const db = this; + const wrapTxn = (mode: string) => { + return async (...bindParameters: any[]) => { + await db.exec("BEGIN " + mode); + db._inTransaction = true; + try { + const result = await fn(...bindParameters); + await db.exec("COMMIT"); + db._inTransaction = false; + return result; + } catch (err) { + await db.exec("ROLLBACK"); + db._inTransaction = false; + 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; + } + /** * Close the connection. * diff --git a/testing/javascript/__test__/async.test.js b/testing/javascript/__test__/async.test.js index 5a323ee2a..c27c1c3c0 100644 --- a/testing/javascript/__test__/async.test.js +++ b/testing/javascript/__test__/async.test.js @@ -145,16 +145,16 @@ test.serial("Database.pragma() after close()", async (t) => { // Database.transaction() // ========================================================================== -test.skip("Database.transaction()", async (t) => { +test.serial("Database.transaction()", async (t) => { const db = t.context.db; const insert = await db.prepare( "INSERT INTO users(name, email) VALUES (:name, :email)" ); - const insertMany = db.transaction((users) => { + const insertMany = db.transaction(async (users) => { t.is(db.inTransaction, true); - for (const user of users) insert.run(user); + for (const user of users) await insert.run(user); }); t.is(db.inTransaction, false); @@ -166,12 +166,12 @@ test.skip("Database.transaction()", async (t) => { t.is(db.inTransaction, false); const stmt = await db.prepare("SELECT * FROM users WHERE id = ?"); - t.is(stmt.get(3).name, "Joey"); - t.is(stmt.get(4).name, "Sally"); - t.is(stmt.get(5).name, "Junior"); + t.is((await stmt.get(3)).name, "Joey"); + t.is((await stmt.get(4)).name, "Sally"); + t.is((await stmt.get(5)).name, "Junior"); }); -test.skip("Database.transaction().immediate()", async (t) => { +test.serial("Database.transaction().immediate()", async (t) => { const db = t.context.db; const insert = await db.prepare( "INSERT INTO users(name, email) VALUES (:name, :email)" diff --git a/testing/javascript/__test__/sync.test.js b/testing/javascript/__test__/sync.test.js index 3cfa70f68..5a839663b 100644 --- a/testing/javascript/__test__/sync.test.js +++ b/testing/javascript/__test__/sync.test.js @@ -138,7 +138,7 @@ test.serial("Database.pragma() after close()", async (t) => { // Database.transaction() // ========================================================================== -test.skip("Database.transaction()", async (t) => { +test.serial("Database.transaction()", async (t) => { const db = t.context.db; const insert = db.prepare( @@ -164,7 +164,7 @@ test.skip("Database.transaction()", async (t) => { t.is(stmt.get(5).name, "Junior"); }); -test.skip("Database.transaction().immediate()", async (t) => { +test.serial("Database.transaction().immediate()", async (t) => { const db = t.context.db; const insert = db.prepare( "INSERT INTO users(name, email) VALUES (:name, :email)"