Skip to content

Commit 65a652c

Browse files
committed
feat: add transaction support and examples to the database module
1 parent 9db51ee commit 65a652c

4 files changed

Lines changed: 346 additions & 11 deletions

File tree

src/features/db/TRANSACTIONS.md.ts

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/**
2+
* Transactions Support in Tryber API
3+
*
4+
* This file provides examples of how to use Knex transactions in the codebase.
5+
* Transactions ensure that multiple database operations are executed atomically:
6+
* either all succeed or all are rolled back.
7+
*/
8+
9+
import * as db from "@src/features/db";
10+
import { tryber } from "@src/features/database";
11+
12+
/**
13+
* Example 1: Using db.transaction() helper with raw queries
14+
*
15+
* The transaction() helper automatically handles commit/rollback:
16+
* - If the callback completes successfully, the transaction is committed
17+
* - If an error is thrown, the transaction is rolled back
18+
*/
19+
export async function exampleRawQueries() {
20+
await db.transaction(async (trx) => {
21+
// Execute multiple queries in the same transaction
22+
await db.query("INSERT INTO users (name) VALUES ('John')", trx);
23+
await db.query(
24+
"INSERT INTO profiles (user_id, bio) VALUES (1, 'Bio')",
25+
trx
26+
);
27+
28+
// If any query fails, all changes are rolled back
29+
});
30+
}
31+
32+
/**
33+
* Example 2: Using transactions with Database class
34+
*
35+
* All methods in the Database class now accept an optional `trx` parameter
36+
*/
37+
export async function exampleDatabaseClass() {
38+
const Experience = new (
39+
await import("@src/features/db/class/Experience")
40+
).default();
41+
42+
await db.transaction(async (trx) => {
43+
// Insert operations within a transaction
44+
const result1 = await Experience.insert(
45+
{
46+
tester_id: 1,
47+
amount: 100,
48+
creation_date: new Date().toISOString(),
49+
activity_id: 1,
50+
reason: "Campaign participation",
51+
campaign_id: 1,
52+
pm_id: 1,
53+
},
54+
trx
55+
);
56+
57+
const result2 = await Experience.insert(
58+
{
59+
tester_id: 1,
60+
amount: 50,
61+
creation_date: new Date().toISOString(),
62+
activity_id: 2,
63+
reason: "Bug report",
64+
campaign_id: 1,
65+
pm_id: 1,
66+
},
67+
trx
68+
);
69+
70+
// Query within the same transaction
71+
const records = await Experience.query({
72+
where: [{ tester_id: 1 }],
73+
trx,
74+
});
75+
76+
// Update within the same transaction
77+
await Experience.update({
78+
data: { amount: 150 },
79+
where: [{ id: result1.insertId }],
80+
trx,
81+
});
82+
83+
// Delete within the same transaction
84+
await Experience.delete([{ id: result2.insertId }], trx);
85+
});
86+
}
87+
88+
/**
89+
* Example 3: Using transactions with tryber tables (Knex query builder)
90+
*
91+
* You can also use the tryber instance directly with transactions
92+
*/
93+
export async function exampleKnexQueryBuilder() {
94+
await db.transaction(async (trx) => {
95+
// Using Knex query builder with transaction
96+
await tryber.tables.WpUsers.do().transacting(trx).insert({
97+
ID: 123,
98+
user_login: "test_user",
99+
user_email: "test@example.com",
100+
});
101+
102+
// Multiple operations in the same transaction
103+
const user = await tryber.tables.WpUsers.do()
104+
.transacting(trx)
105+
.where({ user_email: "test@example.com" })
106+
.first();
107+
108+
if (user) {
109+
await tryber.tables.WpAppqEvdProfile.do().transacting(trx).insert({
110+
wp_user_id: user.ID,
111+
id: 1,
112+
email: user.user_email,
113+
education_id: 1,
114+
employment_id: 1,
115+
});
116+
}
117+
});
118+
}
119+
120+
/**
121+
* Example 4: Error handling and rollback
122+
*
123+
* When an error occurs, the transaction is automatically rolled back
124+
*/
125+
export async function exampleErrorHandling() {
126+
try {
127+
await db.transaction(async (trx) => {
128+
await db.query("INSERT INTO users (name) VALUES ('John')", trx);
129+
130+
// This will cause an error and rollback all changes
131+
throw new Error("Something went wrong");
132+
133+
// This line will never be executed
134+
await db.query(
135+
"INSERT INTO profiles (user_id, bio) VALUES (1, 'Bio')",
136+
trx
137+
);
138+
});
139+
} catch (error) {
140+
console.error("Transaction failed:", error);
141+
// All database changes have been rolled back
142+
}
143+
}
144+
145+
/**
146+
* Example 5: Mixed usage - transaction with both Database class and raw queries
147+
*/
148+
export async function exampleMixedUsage() {
149+
const Experience = new (
150+
await import("@src/features/db/class/Experience")
151+
).default();
152+
153+
await db.transaction(async (trx) => {
154+
// Use Database class
155+
const expResult = await Experience.insert(
156+
{
157+
tester_id: 1,
158+
amount: 100,
159+
creation_date: new Date().toISOString(),
160+
activity_id: 1,
161+
reason: "Test",
162+
campaign_id: 1,
163+
pm_id: 1,
164+
},
165+
trx
166+
);
167+
168+
// Use raw query
169+
await db.query(
170+
`UPDATE wp_appq_user SET total_exp = total_exp + 100 WHERE id = 1`,
171+
trx
172+
);
173+
174+
// Use Knex query builder
175+
await tryber.tables.WpAppqEventTransactionalMail.do()
176+
.transacting(trx)
177+
.insert({
178+
event_name: "experience_added",
179+
template_id: 1,
180+
last_editor_tester_id: 1,
181+
});
182+
});
183+
}
184+
185+
/**
186+
* IMPORTANT NOTES:
187+
*
188+
* 1. Always pass the `trx` parameter to ALL database operations within the transaction
189+
* 2. Don't mix transactional and non-transactional operations in the same logical flow
190+
* 3. Keep transactions short to avoid locking issues
191+
* 4. The transaction is automatically committed if the callback completes without errors
192+
* 5. The transaction is automatically rolled back if an error is thrown
193+
* 6. All methods are backward compatible - the `trx` parameter is optional
194+
*/

src/features/db/class/Database.spec.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import jest from "jest";
22
import Database from "./Database";
3+
import * as db from "../index";
4+
import { tryber } from "@src/features/database";
35

46
class TestableDatabase extends Database<{
57
fields: { id: number; name: string };
@@ -89,3 +91,101 @@ describe("Database connector class", () => {
8991
expect(sql).toBe("ORDER BY id DESC, name ASC");
9092
});
9193
});
94+
95+
describe("Database transactions", () => {
96+
// Create a real Database instance for testing transactions
97+
class UserDatabase extends Database<{
98+
fields: { ID: number; user_login: string; user_email: string };
99+
}> {
100+
constructor() {
101+
super({
102+
table: "wp_users",
103+
primaryKey: "ID",
104+
fields: ["ID", "user_login", "user_email"],
105+
});
106+
}
107+
}
108+
109+
const userDb = new UserDatabase();
110+
111+
afterAll(async () => {
112+
// Clean up test data after all tests
113+
await tryber.tables.WpUsers.do().where("ID", ">", 999999).delete();
114+
});
115+
116+
it("Should commit insert operation when transaction succeeds", async () => {
117+
await db.transaction(async (trx) => {
118+
await userDb.insert(
119+
{
120+
ID: 1000000,
121+
user_login: "test_user_1",
122+
user_email: "test1@example.com",
123+
},
124+
trx
125+
);
126+
});
127+
128+
// Verify data was committed
129+
const user = await userDb.get(1000000);
130+
expect(user).toBeDefined();
131+
expect(user?.user_login).toBe("test_user_1");
132+
});
133+
134+
it("Should rollback insert operation when transaction fails", async () => {
135+
try {
136+
await db.transaction(async (trx) => {
137+
await userDb.insert(
138+
{
139+
ID: 1000001,
140+
user_login: "test_user_2",
141+
user_email: "test2@example.com",
142+
},
143+
trx
144+
);
145+
// Force transaction to fail
146+
throw new Error("Simulated transaction failure");
147+
});
148+
} catch (e) {
149+
// Expected error
150+
}
151+
152+
// Verify data was rolled back
153+
const exists = await userDb.exists(1000001);
154+
expect(exists).toBe(false);
155+
});
156+
157+
it("Should rollback all operations when one fails in a transaction", async () => {
158+
try {
159+
await db.transaction(async (trx) => {
160+
// Insert first user
161+
await userDb.insert(
162+
{
163+
ID: 1000008,
164+
user_login: "test_user_9",
165+
user_email: "test9@example.com",
166+
},
167+
trx
168+
);
169+
// Insert second user
170+
await userDb.insert(
171+
{
172+
ID: 1000009,
173+
user_login: "test_user_10",
174+
user_email: "test10@example.com",
175+
},
176+
trx
177+
);
178+
// Force transaction to fail
179+
throw new Error("Simulated transaction failure");
180+
});
181+
} catch (e) {
182+
// Expected error
183+
}
184+
185+
// Verify both inserts were rolled back
186+
const exists1 = await userDb.exists(1000008);
187+
const exists2 = await userDb.exists(1000009);
188+
expect(exists1).toBe(false);
189+
expect(exists2).toBe(false);
190+
});
191+
});

0 commit comments

Comments
 (0)