@proteinjs/db 1.7.0 → 1.8.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/generated/index.d.ts.map +1 -1
  3. package/dist/generated/index.js +3 -1
  4. package/dist/generated/index.js.map +1 -1
  5. package/dist/index.d.ts +5 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +5 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/src/Db.d.ts +30 -4
  10. package/dist/src/Db.d.ts.map +1 -1
  11. package/dist/src/Db.js +66 -75
  12. package/dist/src/Db.js.map +1 -1
  13. package/dist/src/Table.d.ts +4 -0
  14. package/dist/src/Table.d.ts.map +1 -1
  15. package/dist/src/Table.js +104 -1
  16. package/dist/src/Table.js.map +1 -1
  17. package/dist/src/services/TransactionRunnerService.d.ts +7 -0
  18. package/dist/src/services/TransactionRunnerService.d.ts.map +1 -0
  19. package/dist/src/services/TransactionRunnerService.js +6 -0
  20. package/dist/src/services/TransactionRunnerService.js.map +1 -0
  21. package/dist/src/transaction/Transaction.d.ts +69 -0
  22. package/dist/src/transaction/Transaction.d.ts.map +1 -0
  23. package/dist/src/transaction/Transaction.js +255 -0
  24. package/dist/src/transaction/Transaction.js.map +1 -0
  25. package/dist/src/transaction/TransactionRunner.d.ts +7 -0
  26. package/dist/src/transaction/TransactionRunner.d.ts.map +1 -0
  27. package/dist/src/transaction/TransactionRunner.js +88 -0
  28. package/dist/src/transaction/TransactionRunner.js.map +1 -0
  29. package/dist/test/TransactionDb.test.d.ts +2 -0
  30. package/dist/test/TransactionDb.test.d.ts.map +1 -0
  31. package/dist/test/TransactionDb.test.js +404 -0
  32. package/dist/test/TransactionDb.test.js.map +1 -0
  33. package/dist/test/reusable/TransactionTests.d.ts +52 -0
  34. package/dist/test/reusable/TransactionTests.d.ts.map +1 -0
  35. package/dist/test/reusable/TransactionTests.js +396 -0
  36. package/dist/test/reusable/TransactionTests.js.map +1 -0
  37. package/generated/index.ts +3 -1
  38. package/index.ts +5 -2
  39. package/package.json +2 -2
  40. package/src/Db.ts +63 -28
  41. package/src/Table.ts +23 -0
  42. package/src/services/TransactionRunnerService.ts +10 -0
  43. package/src/transaction/Transaction.ts +186 -0
  44. package/src/transaction/TransactionRunner.ts +18 -0
  45. package/test/TransactionDb.test.ts +335 -0
  46. 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
+ };