@proteinjs/db 1.7.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/generated/index.js +3 -1
- package/dist/generated/index.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/src/Db.d.ts +30 -4
- package/dist/src/Db.d.ts.map +1 -1
- package/dist/src/Db.js +66 -75
- package/dist/src/Db.js.map +1 -1
- package/dist/src/Table.d.ts +4 -0
- package/dist/src/Table.d.ts.map +1 -1
- package/dist/src/Table.js +106 -1
- package/dist/src/Table.js.map +1 -1
- package/dist/src/services/TransactionRunnerService.d.ts +7 -0
- package/dist/src/services/TransactionRunnerService.d.ts.map +1 -0
- package/dist/src/services/TransactionRunnerService.js +6 -0
- package/dist/src/services/TransactionRunnerService.js.map +1 -0
- package/dist/src/transaction/Transaction.d.ts +69 -0
- package/dist/src/transaction/Transaction.d.ts.map +1 -0
- package/dist/src/transaction/Transaction.js +255 -0
- package/dist/src/transaction/Transaction.js.map +1 -0
- package/dist/src/transaction/TransactionRunner.d.ts +7 -0
- package/dist/src/transaction/TransactionRunner.d.ts.map +1 -0
- package/dist/src/transaction/TransactionRunner.js +88 -0
- package/dist/src/transaction/TransactionRunner.js.map +1 -0
- package/dist/test/TransactionDb.test.d.ts +2 -0
- package/dist/test/TransactionDb.test.d.ts.map +1 -0
- package/dist/test/TransactionDb.test.js +404 -0
- package/dist/test/TransactionDb.test.js.map +1 -0
- package/dist/test/reusable/TransactionTests.d.ts +52 -0
- package/dist/test/reusable/TransactionTests.d.ts.map +1 -0
- package/dist/test/reusable/TransactionTests.js +396 -0
- package/dist/test/reusable/TransactionTests.js.map +1 -0
- package/generated/index.ts +3 -1
- package/index.ts +5 -2
- package/package.json +2 -2
- package/src/Db.ts +63 -28
- package/src/Table.ts +25 -0
- package/src/services/TransactionRunnerService.ts +10 -0
- package/src/transaction/Transaction.ts +186 -0
- package/src/transaction/TransactionRunner.ts +18 -0
- package/test/TransactionDb.test.ts +335 -0
- package/test/reusable/TransactionTests.ts +234 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { QueryBuilder } from '@proteinjs/db-query';
|
|
2
|
+
import { DbDriver, Db } from '../../src/Db';
|
|
3
|
+
import { withRecordColumns, Record } from '../../src/Record';
|
|
4
|
+
import { BooleanColumn, DateColumn, StringColumn } from '../../src/Columns';
|
|
5
|
+
import { Table } from '../../src/Table';
|
|
6
|
+
|
|
7
|
+
interface Employee extends Record {
|
|
8
|
+
name: string;
|
|
9
|
+
department?: string;
|
|
10
|
+
jobTitle?: string | null;
|
|
11
|
+
isRemote?: boolean;
|
|
12
|
+
startDate?: Date;
|
|
13
|
+
object?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class EmployeeTestTable extends Table<Employee> {
|
|
17
|
+
name = 'db_transaction_test_employee';
|
|
18
|
+
columns = withRecordColumns<Employee>({
|
|
19
|
+
name: new StringColumn('name'),
|
|
20
|
+
department: new StringColumn('department'),
|
|
21
|
+
isRemote: new BooleanColumn('is_remote'),
|
|
22
|
+
jobTitle: new StringColumn('job_title'),
|
|
23
|
+
startDate: new DateColumn('start_date'),
|
|
24
|
+
object: new StringColumn('object'),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ReservedWordTest extends Record {
|
|
29
|
+
name: string;
|
|
30
|
+
order?: string;
|
|
31
|
+
select?: string;
|
|
32
|
+
join?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class ReservedWordTestTable extends Table<ReservedWordTest> {
|
|
36
|
+
name = 'db_transaction_test_reserved_word';
|
|
37
|
+
columns = withRecordColumns<ReservedWordTest>({
|
|
38
|
+
name: new StringColumn('name'),
|
|
39
|
+
order: new StringColumn('order'),
|
|
40
|
+
select: new StringColumn('select'),
|
|
41
|
+
join: new StringColumn('join'),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Used for testing purposes only.
|
|
47
|
+
* */
|
|
48
|
+
export const getTransactionTestTable = (tableName: string) => {
|
|
49
|
+
const employeeTable = new EmployeeTestTable();
|
|
50
|
+
if (employeeTable.name == tableName) {
|
|
51
|
+
return new EmployeeTestTable();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const reservedWordTestTable = new ReservedWordTestTable();
|
|
55
|
+
if (reservedWordTestTable.name == tableName) {
|
|
56
|
+
return new ReservedWordTestTable();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
throw new Error(`Cannot find test table: ${tableName}`);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const transactionTests = (driver: DbDriver, dropTable: (table: Table<any>) => Promise<void>) => {
|
|
63
|
+
return () => {
|
|
64
|
+
const db = new Db(driver, getTransactionTestTable);
|
|
65
|
+
|
|
66
|
+
beforeAll(async () => {
|
|
67
|
+
if (driver.start) {
|
|
68
|
+
await driver.start();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await driver.getTableManager().loadTable(new EmployeeTestTable());
|
|
72
|
+
await driver.getTableManager().loadTable(new ReservedWordTestTable());
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterAll(async () => {
|
|
76
|
+
await dropTable(new EmployeeTestTable());
|
|
77
|
+
await dropTable(new ReservedWordTestTable());
|
|
78
|
+
|
|
79
|
+
if (driver.stop) {
|
|
80
|
+
await driver.stop();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('Transaction with successful operations', async () => {
|
|
85
|
+
const testEmployee1: Omit<Employee, keyof Record> = {
|
|
86
|
+
name: 'Veronica',
|
|
87
|
+
department: 'Engineering',
|
|
88
|
+
};
|
|
89
|
+
const testEmployee2: Omit<Employee, keyof Record> = {
|
|
90
|
+
name: 'Brent',
|
|
91
|
+
department: 'Engineering',
|
|
92
|
+
};
|
|
93
|
+
const emplyeeTable: Table<Employee> = new EmployeeTestTable();
|
|
94
|
+
|
|
95
|
+
// Execute multiple operations in a transaction
|
|
96
|
+
const results = await db.runTransaction(async () => {
|
|
97
|
+
const emp1 = await db.insert(emplyeeTable, testEmployee1);
|
|
98
|
+
const emp2 = await db.insert(emplyeeTable, testEmployee2);
|
|
99
|
+
await db.update(emplyeeTable, { department: 'R&D' }, { id: emp1.id });
|
|
100
|
+
return { emp1, emp2 };
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Verify the transaction results
|
|
104
|
+
expect(results.emp1.name).toBe('Veronica');
|
|
105
|
+
expect(results.emp2.name).toBe('Brent');
|
|
106
|
+
|
|
107
|
+
// Verify the records in the database
|
|
108
|
+
const updatedEmp1 = await db.get(emplyeeTable, { id: results.emp1.id });
|
|
109
|
+
const updatedEmp2 = await db.get(emplyeeTable, { id: results.emp2.id });
|
|
110
|
+
|
|
111
|
+
expect(updatedEmp1.department).toBe('R&D');
|
|
112
|
+
expect(updatedEmp2.department).toBe('Engineering');
|
|
113
|
+
|
|
114
|
+
// Clean up
|
|
115
|
+
const qb = new QueryBuilder<Employee>(emplyeeTable.name).condition({
|
|
116
|
+
field: 'id',
|
|
117
|
+
operator: 'IN',
|
|
118
|
+
value: [results.emp1.id, results.emp2.id],
|
|
119
|
+
});
|
|
120
|
+
await db.delete(emplyeeTable, qb);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('Transaction rollback on error', async () => {
|
|
124
|
+
const testEmployee1: Omit<Employee, keyof Record> = {
|
|
125
|
+
name: 'Veronica',
|
|
126
|
+
department: 'Engineering',
|
|
127
|
+
};
|
|
128
|
+
const testEmployee2: Omit<Employee, keyof Record> = {
|
|
129
|
+
name: 'Brent',
|
|
130
|
+
department: undefined, // This will cause an error
|
|
131
|
+
};
|
|
132
|
+
const emplyeeTable: Table<Employee> = new EmployeeTestTable();
|
|
133
|
+
|
|
134
|
+
let insertedId: string | undefined;
|
|
135
|
+
|
|
136
|
+
// Execute operations that should fail and rollback
|
|
137
|
+
try {
|
|
138
|
+
await db.runTransaction(async () => {
|
|
139
|
+
const emp1 = await db.insert(emplyeeTable, testEmployee1);
|
|
140
|
+
insertedId = emp1.id;
|
|
141
|
+
// This should fail and trigger a rollback
|
|
142
|
+
await db.insert(emplyeeTable, testEmployee2);
|
|
143
|
+
return emp1;
|
|
144
|
+
});
|
|
145
|
+
fail('Transaction should have thrown an error');
|
|
146
|
+
} catch (error) {
|
|
147
|
+
// Verify the first insert was rolled back
|
|
148
|
+
const result = await db.get(emplyeeTable, { id: insertedId });
|
|
149
|
+
expect(result).toBeFalsy();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('Nested transactions are not allowed', async () => {
|
|
154
|
+
const testEmployee: Omit<Employee, keyof Record> = {
|
|
155
|
+
name: 'Veronica',
|
|
156
|
+
department: 'Engineering',
|
|
157
|
+
};
|
|
158
|
+
const emplyeeTable: Table<Employee> = new EmployeeTestTable();
|
|
159
|
+
|
|
160
|
+
await expect(
|
|
161
|
+
db.runTransaction(async () => {
|
|
162
|
+
const emp = await db.insert(emplyeeTable, testEmployee);
|
|
163
|
+
// Try to start a nested transaction
|
|
164
|
+
await db.runTransaction(async () => {
|
|
165
|
+
await db.update(emplyeeTable, { department: 'R&D' }, { id: emp.id });
|
|
166
|
+
return emp;
|
|
167
|
+
});
|
|
168
|
+
return emp;
|
|
169
|
+
})
|
|
170
|
+
).rejects.toThrow();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('Transaction isolation', async () => {
|
|
174
|
+
const testEmployee: Omit<Employee, keyof Record> = {
|
|
175
|
+
name: 'Veronica',
|
|
176
|
+
department: 'Engineering',
|
|
177
|
+
};
|
|
178
|
+
const emplyeeTable: Table<Employee> = new EmployeeTestTable();
|
|
179
|
+
|
|
180
|
+
// Start a transaction but don't commit immediately
|
|
181
|
+
const transactionPromise = db.runTransaction(async () => {
|
|
182
|
+
const emp = await db.insert(emplyeeTable, testEmployee);
|
|
183
|
+
// Delay to simulate long-running transaction
|
|
184
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
185
|
+
return emp;
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Try to query the record before transaction commits
|
|
189
|
+
const results = await db.query(emplyeeTable, { name: 'Veronica' });
|
|
190
|
+
expect(results.length).toBe(0);
|
|
191
|
+
|
|
192
|
+
// Wait for transaction to complete
|
|
193
|
+
const insertedEmployee = await transactionPromise;
|
|
194
|
+
|
|
195
|
+
// Clean up
|
|
196
|
+
await db.delete(emplyeeTable, { id: insertedEmployee.id });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('Transaction with multiple table operations', async () => {
|
|
200
|
+
const testEmployee: Omit<Employee, keyof Record> = {
|
|
201
|
+
name: 'Veronica',
|
|
202
|
+
department: 'Engineering',
|
|
203
|
+
};
|
|
204
|
+
const testReservedWord: Omit<ReservedWordTest, keyof Record> = {
|
|
205
|
+
name: 'Test',
|
|
206
|
+
order: '1',
|
|
207
|
+
select: 'Option 1',
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const emplyeeTable: Table<Employee> = new EmployeeTestTable();
|
|
211
|
+
const reservedWordTable: Table<ReservedWordTest> = new ReservedWordTestTable();
|
|
212
|
+
|
|
213
|
+
// Execute operations on multiple tables in a transaction
|
|
214
|
+
const results = await db.runTransaction(async () => {
|
|
215
|
+
const emp = await db.insert(emplyeeTable, testEmployee);
|
|
216
|
+
const reserved = await db.insert(reservedWordTable, testReservedWord);
|
|
217
|
+
await db.update(emplyeeTable, { department: 'R&D' }, { id: emp.id });
|
|
218
|
+
await db.update(reservedWordTable, { order: '2' }, { id: reserved.id });
|
|
219
|
+
return { emp, reserved };
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Verify the records in both tables
|
|
223
|
+
const updatedEmp = await db.get(emplyeeTable, { id: results.emp.id });
|
|
224
|
+
const updatedReserved = await db.get(reservedWordTable, { id: results.reserved.id });
|
|
225
|
+
|
|
226
|
+
expect(updatedEmp.department).toBe('R&D');
|
|
227
|
+
expect(updatedReserved.order).toBe('2');
|
|
228
|
+
|
|
229
|
+
// Clean up
|
|
230
|
+
await db.delete(emplyeeTable, { id: results.emp.id });
|
|
231
|
+
await db.delete(reservedWordTable, { id: results.reserved.id });
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
};
|