@dbos-inc/typeorm-datasource 5.3.3-preview → 5.3.5-preview

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.
@@ -1,746 +0,0 @@
1
- import { DBOS, DBOSWorkflowConflictError } from '@dbos-inc/dbos-sdk';
2
- import { Client, Pool } from 'pg';
3
- import { TypeOrmDataSource } from '..';
4
- import { dropDB, ensureDB } from './test-helpers';
5
- import { randomUUID } from 'crypto';
6
- import { SuperJSON } from 'superjson';
7
- import { Column, DataSource, Entity, PrimaryColumn } from 'typeorm';
8
-
9
- @Entity({ name: 'greetings' })
10
- class Greeting {
11
- @PrimaryColumn()
12
- name: string = '';
13
-
14
- @Column()
15
- greet_count: number = 0;
16
- }
17
-
18
- const config = { user: 'postgres', database: 'typeorm_ds_test_datasource' };
19
- const dataSource = TypeOrmDataSource.createFromConfig('app-db', config, [Greeting]);
20
-
21
- interface transaction_completion {
22
- workflow_id: string;
23
- function_num: number;
24
- output: string | null;
25
- error: string | null;
26
- }
27
-
28
- describe('TypeOrmDataSource', () => {
29
- const userDB = new Pool(config);
30
-
31
- beforeAll(async () => {
32
- await createDatabases(true);
33
- });
34
-
35
- afterAll(async () => {
36
- await userDB.end();
37
- });
38
-
39
- beforeEach(async () => {
40
- DBOS.setConfig({ name: 'typeorm-ds-test' });
41
- await DBOS.launch();
42
- });
43
-
44
- afterEach(async () => {
45
- await DBOS.shutdown();
46
- });
47
-
48
- test('insert dataSource.register function', async () => {
49
- const user = 'helloTest1';
50
-
51
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
52
- const workflowID = randomUUID();
53
-
54
- await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowReg(user))).resolves.toMatchObject({
55
- user,
56
- greet_count: 1,
57
- });
58
-
59
- const { rows } = await userDB.query<transaction_completion>(
60
- 'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
61
- [workflowID],
62
- );
63
- // Completion cleared the checkpoint the transaction wrote.
64
- expect(rows).toHaveLength(0);
65
- });
66
-
67
- test('rerun insert dataSource.register function', async () => {
68
- const user = 'rerunTest1';
69
-
70
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
71
- const workflowID = randomUUID();
72
-
73
- const result = await DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowReg(user));
74
- expect(result).toMatchObject({ user, greet_count: 1 });
75
-
76
- await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowReg(user))).resolves.toMatchObject(result);
77
- });
78
-
79
- test('insert dataSource.runAsTx function', async () => {
80
- const user = 'helloTest2';
81
-
82
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
83
- const workflowID = randomUUID();
84
-
85
- await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowRunTx(user))).resolves.toMatchObject({
86
- user,
87
- greet_count: 1,
88
- });
89
-
90
- const { rows } = await userDB.query<transaction_completion>(
91
- 'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
92
- [workflowID],
93
- );
94
- // Completion cleared the checkpoint the transaction wrote.
95
- expect(rows).toHaveLength(0);
96
- });
97
-
98
- test('rerun insert dataSource.runAsTx function', async () => {
99
- const user = 'rerunTest2';
100
-
101
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
102
- const workflowID = randomUUID();
103
-
104
- const result = await DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowRunTx(user));
105
- expect(result).toMatchObject({ user, greet_count: 1 });
106
-
107
- await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowRunTx(user))).resolves.toMatchObject(
108
- result,
109
- );
110
- });
111
-
112
- async function throws<R>(func: () => Promise<R>): Promise<unknown> {
113
- try {
114
- await func();
115
- fail('Expected function to throw an error');
116
- } catch (error) {
117
- return error;
118
- }
119
- }
120
-
121
- test('error dataSource.register function', async () => {
122
- const user = 'errorTest1';
123
-
124
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
125
- await userDB.query('INSERT INTO greetings("name","greet_count") VALUES($1,10);', [user]);
126
- const workflowID = randomUUID();
127
-
128
- const error = await throws(() => DBOS.withNextWorkflowID(workflowID, () => regErrorWorkflowReg(user)));
129
- expect(error).toBeInstanceOf(Error);
130
- expect((error as Error).message).toMatch(/^test error \d+$/);
131
-
132
- const { rows } = await userDB.query<greetings>('SELECT * FROM greetings WHERE name = $1', [user]);
133
- expect(rows.length).toBe(1);
134
- expect(rows[0].greet_count).toBe(10);
135
-
136
- const { rows: txOutput } = await userDB.query<transaction_completion>(
137
- 'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
138
- [workflowID],
139
- );
140
- // Completion cleared the checkpoint the transaction wrote.
141
- expect(txOutput).toHaveLength(0);
142
- });
143
-
144
- test('rerun error dataSource.register function', async () => {
145
- const user = 'rerunErrorTest1';
146
-
147
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
148
- await userDB.query('INSERT INTO greetings("name","greet_count") VALUES($1,10);', [user]);
149
- const workflowID = randomUUID();
150
-
151
- const error = await throws(() => DBOS.withNextWorkflowID(workflowID, () => regErrorWorkflowReg(user)));
152
- expect(error).toBeInstanceOf(Error);
153
- expect((error as Error).message).toMatch(/^test error \d+$/);
154
-
155
- const error2 = await throws(() => DBOS.withNextWorkflowID(workflowID, () => regErrorWorkflowReg(user)));
156
- expect(error2).toBeInstanceOf(Error);
157
- expect((error2 as Error).message).toMatch((error as Error).message);
158
- });
159
-
160
- test('error dataSource.runAsTx function', async () => {
161
- const user = 'errorTest2';
162
-
163
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
164
- await userDB.query('INSERT INTO greetings("name","greet_count") VALUES($1,10);', [user]);
165
- const workflowID = randomUUID();
166
-
167
- const error = await throws(() => DBOS.withNextWorkflowID(workflowID, () => regErrorWorkflowRunTx(user)));
168
- expect(error).toBeInstanceOf(Error);
169
- expect((error as Error).message).toMatch(/^test error \d+$/);
170
-
171
- const { rows } = await userDB.query<greetings>('SELECT * FROM greetings WHERE name = $1', [user]);
172
- expect(rows.length).toBe(1);
173
- expect(rows[0].greet_count).toBe(10);
174
-
175
- const { rows: txOutput } = await userDB.query<transaction_completion>(
176
- 'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
177
- [workflowID],
178
- );
179
- // Completion cleared the checkpoint the transaction wrote.
180
- expect(txOutput).toHaveLength(0);
181
- });
182
-
183
- test('rerun error dataSource.runAsTx function', async () => {
184
- const user = 'rerunErrorTest2';
185
-
186
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
187
- await userDB.query('INSERT INTO greetings("name","greet_count") VALUES($1,10);', [user]);
188
- const workflowID = randomUUID();
189
-
190
- const error = await throws(() => DBOS.withNextWorkflowID(workflowID, () => regErrorWorkflowRunTx(user)));
191
- expect(error).toBeInstanceOf(Error);
192
- expect((error as Error).message).toMatch(/^test error \d+$/);
193
-
194
- const error2 = await throws(() => DBOS.withNextWorkflowID(workflowID, () => regErrorWorkflowRunTx(user)));
195
- expect(error2).toBeInstanceOf(Error);
196
- expect((error2 as Error).message).toMatch((error as Error).message);
197
- });
198
-
199
- test('readonly dataSource.register function', async () => {
200
- const user = 'readTest1';
201
-
202
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
203
- await userDB.query('INSERT INTO greetings("name","greet_count") VALUES($1,10);', [user]);
204
-
205
- const workflowID = randomUUID();
206
- await expect(DBOS.withNextWorkflowID(workflowID, () => regReadWorkflowReg(user))).resolves.toMatchObject({
207
- user,
208
- greet_count: 10,
209
- });
210
-
211
- const { rows } = await userDB.query('SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1', [
212
- workflowID,
213
- ]);
214
- expect(rows.length).toBe(0);
215
- });
216
-
217
- test('readonly dataSource.runAsTx function', async () => {
218
- const user = 'readTest2';
219
-
220
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
221
- await userDB.query('INSERT INTO greetings("name","greet_count") VALUES($1,10);', [user]);
222
-
223
- const workflowID = randomUUID();
224
- await expect(DBOS.withNextWorkflowID(workflowID, () => regReadWorkflowRunTx(user))).resolves.toMatchObject({
225
- user,
226
- greet_count: 10,
227
- });
228
-
229
- const { rows } = await userDB.query('SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1', [
230
- workflowID,
231
- ]);
232
- expect(rows.length).toBe(0);
233
- });
234
-
235
- test('static dataSource.register methods', async () => {
236
- const user = 'staticTest1';
237
-
238
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
239
-
240
- const workflowID = randomUUID();
241
- await expect(DBOS.withNextWorkflowID(workflowID, () => regStaticWorkflow(user))).resolves.toMatchObject([
242
- { user, greet_count: 1 },
243
- { user, greet_count: 1 },
244
- ]);
245
- });
246
-
247
- test('instance dataSource.register methods', async () => {
248
- const user = 'instanceTest1';
249
-
250
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
251
-
252
- const workflowID = randomUUID();
253
- await expect(DBOS.withNextWorkflowID(workflowID, () => regInstanceWorkflow(user))).resolves.toMatchObject([
254
- { user, greet_count: 1 },
255
- { user, greet_count: 1 },
256
- ]);
257
- });
258
-
259
- test('invoke-reg-tx-fun-outside-wf', async () => {
260
- const user = 'outsideWfUser' + Date.now();
261
- const result = await regInsertFunction(user);
262
- expect(result).toMatchObject({ user, greet_count: 1 });
263
-
264
- const txResults = await userDB.query('SELECT * FROM dbos.transaction_completion WHERE output LIKE $1', [
265
- `%${user}%`,
266
- ]);
267
- expect(txResults.rows.length).toBe(0);
268
- });
269
-
270
- test('invoke-reg-tx-static-method-outside-wf', async () => {
271
- const user = 'outsideWfUser' + Date.now();
272
- const result = await StaticClass.insertFunction(user);
273
- expect(result).toMatchObject({ user, greet_count: 1 });
274
-
275
- const txResults = await userDB.query('SELECT * FROM dbos.transaction_completion WHERE output LIKE $1', [
276
- `%${user}%`,
277
- ]);
278
- expect(txResults.rows.length).toBe(0);
279
- });
280
-
281
- test('invoke-reg-tx-inst-method-outside-wf', async () => {
282
- const user = 'outsideWfUser' + Date.now();
283
- const instance = new InstanceClass();
284
- const result = await instance.insertFunction(user);
285
- expect(result).toMatchObject({ user, greet_count: 1 });
286
-
287
- const txResults = await userDB.query('SELECT * FROM dbos.transaction_completion WHERE output LIKE $1', [
288
- `%${user}%`,
289
- ]);
290
- expect(txResults.rows.length).toBe(0);
291
- });
292
-
293
- test('duplicate execution replays the winner result', async () => {
294
- await userDB.query('DELETE FROM race_side_effects');
295
- raceState.callCount = 0;
296
-
297
- // A loser whose body succeeds: the collision happens on the result-recording insert.
298
- raceState.plantWinner = true;
299
- raceState.plantWinnerError = false;
300
- raceState.fail = false;
301
- const wfid1 = randomUUID();
302
- await expect(DBOS.withNextWorkflowID(wfid1, () => regRaceWorkflow())).resolves.toBe('winner-result');
303
- expect(raceState.callCount).toBe(1); // the loser did run its body
304
-
305
- // A loser whose body fails: the collision moves to the error-recording insert,
306
- // and the winner's result still wins over the loser's own error.
307
- raceState.plantWinner = true;
308
- raceState.plantWinnerError = false;
309
- raceState.fail = true;
310
- const wfid2 = randomUUID();
311
- await expect(DBOS.withNextWorkflowID(wfid2, () => regRaceWorkflow())).resolves.toBe('winner-result');
312
- expect(raceState.callCount).toBe(2);
313
-
314
- // A winner that recorded an error: the loser replays that throw instead of its own result.
315
- raceState.plantWinner = true;
316
- raceState.plantWinnerError = true;
317
- raceState.fail = false;
318
- const wfid3 = randomUUID();
319
- const winnerError = await throws(() => DBOS.withNextWorkflowID(wfid3, () => regRaceWorkflow()));
320
- expect((winnerError as Error).message).toBe('winner-error');
321
- expect(raceState.callCount).toBe(3);
322
-
323
- // Every loser's writes were discarded, and completion cleared the winners' checkpoints.
324
- const { rows: tags } = await userDB.query<{ tag: string }>('SELECT tag FROM race_side_effects');
325
- expect(tags).toHaveLength(0);
326
- const { rows: txOutput } = await userDB.query<transaction_completion>(
327
- 'SELECT * FROM dbos.transaction_completion WHERE workflow_id = ANY($1)',
328
- [[wfid1, wfid2, wfid3]],
329
- );
330
- expect(txOutput).toHaveLength(0);
331
- });
332
-
333
- /** Completion clears the checkpoints, so the workflow reads them back while it still runs. */
334
- test('transactions record their outcomes as checkpoints', async () => {
335
- const user = 'checkpointProbe';
336
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
337
- const workflowID = randomUUID();
338
- const message = await DBOS.withNextWorkflowID(workflowID, () => regCheckpointProbeWorkflow(user));
339
- expect(message).toMatch(/^test error \d+$/);
340
-
341
- expect(probeState.rows).toHaveLength(2);
342
- const [output, error] = probeState.rows;
343
- expect(output).toMatchObject({ workflow_id: workflowID, function_num: 0, error: null });
344
- expect(SuperJSON.parse(output.output!)).toMatchObject({ user, greet_count: 1 });
345
- expect(error).toMatchObject({ workflow_id: workflowID, function_num: 1, output: null });
346
- expect(SuperJSON.parse<Error>(error.error!).message).toBe(message);
347
- expect(await readCompletions(workflowID)).toHaveLength(0);
348
- });
349
-
350
- /** The checkpoint may be the new owner's only record of the transaction, so a stale execution's cleanup rolls back. */
351
- test('a stale execution keeps the checkpoints at completion', async () => {
352
- const user = 'handOffTest';
353
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
354
- const handedOff = new Promise<void>((resolve) => (handOffState.signal = resolve));
355
- const workflowID = randomUUID();
356
- const execution = DBOS.withNextWorkflowID(workflowID, () => regHandOffWorkflow(user));
357
- // Surfaces the workflow's own error if it fails before handing off.
358
- await Promise.race([handedOff, execution]);
359
-
360
- // Its outcome is refused too, so the execution parks until the workflow is cancelled.
361
- await DBOS.cancelWorkflow(workflowID);
362
- await expect(execution).rejects.toThrow(/has been cancelled/);
363
- expect(await readCompletions(workflowID)).toHaveLength(1);
364
- });
365
-
366
- /** A stale execution cannot apply a step the new owner also runs, nor leave an outcome for it. */
367
- test('an execution that loses the workflow mid-transaction rolls back', async () => {
368
- await userDB.query('DELETE FROM race_side_effects');
369
- const workflowID = randomUUID();
370
- await expect(DBOS.withNextWorkflowID(workflowID, () => regStaleWorkflow())).resolves.toBe('conflicted');
371
-
372
- const { rows: tags } = await userDB.query<{ tag: string }>('SELECT tag FROM race_side_effects');
373
- expect(tags).toHaveLength(0);
374
- expect(staleState.completionsAtConflict).toBe(0);
375
- const sysDB = new Client(sysConfig);
376
- try {
377
- await sysDB.connect();
378
- const { rows: steps } = await sysDB.query('SELECT * FROM dbos.operation_outputs WHERE workflow_uuid = $1', [
379
- workflowID,
380
- ]);
381
- expect(steps).toHaveLength(0);
382
- } finally {
383
- await sysDB.end();
384
- }
385
- });
386
- });
387
-
388
- export interface greetings {
389
- name: string;
390
- greet_count: number;
391
- }
392
-
393
- async function createDatabases(createTxCompletions: boolean) {
394
- {
395
- const client = new Client({ ...config, database: 'postgres' });
396
- try {
397
- await client.connect();
398
- await dropDB(client, 'typeorm_ds_test', true);
399
- await dropDB(client, 'typeorm_ds_test_dbos_sys', true);
400
- await dropDB(client, config.database, true);
401
- await ensureDB(client, config.database);
402
- } finally {
403
- await client.end();
404
- }
405
- }
406
-
407
- if (createTxCompletions) {
408
- await TypeOrmDataSource.initializeDBOSSchema(config);
409
- }
410
-
411
- {
412
- const ds = new DataSource({
413
- type: 'postgres',
414
- username: config.user,
415
- database: config.database,
416
- entities: [Greeting],
417
- });
418
- await ds.initialize();
419
- await ds.synchronize();
420
- await ds.query('CREATE TABLE race_side_effects(tag text NOT NULL)');
421
- await ds.destroy();
422
- }
423
- }
424
-
425
- async function insertFunction(user: string) {
426
- type Result = Array<{ greet_count: number }>;
427
- const rows = await dataSource.entityManager.sql<Result>`
428
- INSERT INTO greetings(name, greet_count) VALUES(${user}, 1)
429
- ON CONFLICT(name) DO UPDATE SET greet_count = greetings.greet_count + 1
430
- RETURNING greet_count`;
431
-
432
- const row = rows.length > 0 ? rows[0] : undefined;
433
- return { user, greet_count: row?.greet_count, now: Date.now() };
434
- }
435
-
436
- async function errorFunction(user: string) {
437
- const _result = await insertFunction(user);
438
- throw new Error(`test error ${Date.now()}`);
439
- }
440
-
441
- const raceState = { callCount: 0, plantWinner: false, plantWinnerError: false, fail: false };
442
-
443
- async function raceFunction() {
444
- raceState.callCount += 1;
445
- await dataSource.entityManager.sql`INSERT INTO race_side_effects(tag) VALUES (${`run-${raceState.callCount}`})`;
446
- if (raceState.plantWinner) {
447
- raceState.plantWinner = false;
448
- await plantWinnerCompletion();
449
- }
450
- if (raceState.fail) {
451
- throw new Error("loser's own failure");
452
- }
453
- return `result-${raceState.callCount}`;
454
- }
455
-
456
- // A duplicate execution commits its result while this transaction is still open, so this
457
- // execution's pre-check missed it but its completion insert will collide with it.
458
- async function plantWinnerCompletion() {
459
- const winner = new Client(config);
460
- try {
461
- await winner.connect();
462
- const [column, value] = raceState.plantWinnerError
463
- ? ['error', SuperJSON.stringify(new Error('winner-error'))]
464
- : ['output', SuperJSON.stringify('winner-result')];
465
- await winner.query(
466
- `INSERT INTO dbos.transaction_completion (workflow_id, function_num, ${column}) VALUES ($1, $2, $3)`,
467
- [DBOS.workflowID, DBOS.stepID, value],
468
- );
469
- } finally {
470
- await winner.end();
471
- }
472
- }
473
-
474
- const regRaceFunction = dataSource.registerTransaction(raceFunction);
475
-
476
- async function raceWorkflow() {
477
- return await regRaceFunction();
478
- }
479
-
480
- const regRaceWorkflow = DBOS.registerWorkflow(raceWorkflow);
481
-
482
- async function readFunction(user: string) {
483
- const result = await dataSource.entityManager.findOneBy(Greeting, { name: user });
484
-
485
- return { user, greet_count: result?.greet_count, now: Date.now() };
486
- }
487
-
488
- const regInsertFunction = dataSource.registerTransaction(insertFunction);
489
- const regErrorFunction = dataSource.registerTransaction(errorFunction);
490
-
491
- const sysConfig = { ...config, database: 'typeorm_ds_test_dbos_sys' };
492
-
493
- /** Set the workflow's ownership token, returning the one it replaced. */
494
- async function setOwner(workflowID: string, ownerXid: string | null): Promise<string | null> {
495
- const sysDB = new Client(sysConfig);
496
- try {
497
- await sysDB.connect();
498
- const { rows } = await sysDB.query<{ owner_xid: string | null }>(
499
- 'SELECT owner_xid FROM dbos.workflow_status WHERE workflow_uuid = $1',
500
- [workflowID],
501
- );
502
- await sysDB.query('UPDATE dbos.workflow_status SET owner_xid = $2 WHERE workflow_uuid = $1', [
503
- workflowID,
504
- ownerXid,
505
- ]);
506
- return rows[0].owner_xid;
507
- } finally {
508
- await sysDB.end();
509
- }
510
- }
511
-
512
- /** The checkpoints this data source holds for the workflow, read on a connection of their own. */
513
- async function readCompletions(workflowID: string): Promise<transaction_completion[]> {
514
- const client = new Client(config);
515
- try {
516
- await client.connect();
517
- const { rows } = await client.query<transaction_completion>(
518
- 'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1 ORDER BY function_num',
519
- [workflowID],
520
- );
521
- return rows;
522
- } finally {
523
- await client.end();
524
- }
525
- }
526
-
527
- const probeState = { rows: [] as transaction_completion[] };
528
-
529
- // Reads back the checkpoints its transactions wrote, before completion clears them.
530
- async function checkpointProbeWorkflow(user: string) {
531
- await regInsertFunction(user);
532
- const error = await regErrorFunction(user).then(
533
- () => undefined,
534
- (e: Error) => e,
535
- );
536
- probeState.rows = await readCompletions(DBOS.workflowID!);
537
- return error?.message;
538
- }
539
-
540
- const regCheckpointProbeWorkflow = DBOS.registerWorkflow(checkpointProbeWorkflow);
541
-
542
- const staleState = { originalOwner: null as string | null, completionsAtConflict: -1 };
543
-
544
- // Another execution claims the workflow while this transaction is still open.
545
- async function staleFunction() {
546
- await dataSource.entityManager.sql`INSERT INTO race_side_effects(tag) VALUES ('stale')`;
547
- staleState.originalOwner = await setOwner(DBOS.workflowID!, 'another-execution');
548
- return 'stale';
549
- }
550
-
551
- const regStaleFunction = dataSource.registerTransaction(staleFunction);
552
-
553
- async function staleWorkflow() {
554
- try {
555
- return await regStaleFunction();
556
- } catch (e) {
557
- if (!(e instanceof DBOSWorkflowConflictError)) throw e;
558
- // Read before completion clears it: the conflict must not have been recorded as the step's outcome.
559
- staleState.completionsAtConflict = (await readCompletions(DBOS.workflowID!)).length;
560
- // A real duplicate parks here instead; reclaimed so this execution's outcome lands.
561
- await setOwner(DBOS.workflowID!, staleState.originalOwner);
562
- return 'conflicted';
563
- }
564
- }
565
-
566
- const regStaleWorkflow = DBOS.registerWorkflow(staleWorkflow);
567
-
568
- const handOffState = { signal: () => {} };
569
-
570
- // Loses the workflow after its transaction, so its completion is a stale execution's.
571
- async function handOffWorkflow(user: string) {
572
- const result = await regInsertFunction(user);
573
- await setOwner(DBOS.workflowID!, 'another-execution');
574
- handOffState.signal();
575
- return result;
576
- }
577
-
578
- const regHandOffWorkflow = DBOS.registerWorkflow(handOffWorkflow);
579
- const regReadFunction = dataSource.registerTransaction(readFunction, { readOnly: true });
580
-
581
- class StaticClass {
582
- static async insertFunction(user: string) {
583
- return await insertFunction(user);
584
- }
585
-
586
- static async readFunction(user: string) {
587
- return await readFunction(user);
588
- }
589
- }
590
-
591
- StaticClass.insertFunction = dataSource.registerTransaction(StaticClass.insertFunction);
592
- StaticClass.readFunction = dataSource.registerTransaction(StaticClass.readFunction, { readOnly: true });
593
-
594
- class InstanceClass {
595
- async insertFunction(user: string) {
596
- return await insertFunction(user);
597
- }
598
-
599
- async readFunction(user: string) {
600
- return await readFunction(user);
601
- }
602
- }
603
-
604
- InstanceClass.prototype.insertFunction = dataSource.registerTransaction(
605
- // eslint-disable-next-line @typescript-eslint/unbound-method
606
- InstanceClass.prototype.insertFunction,
607
- );
608
- InstanceClass.prototype.readFunction = dataSource.registerTransaction(
609
- // eslint-disable-next-line @typescript-eslint/unbound-method
610
- InstanceClass.prototype.readFunction,
611
- { readOnly: true },
612
- );
613
-
614
- async function insertWorkflowReg(user: string) {
615
- return await regInsertFunction(user);
616
- }
617
-
618
- async function insertWorkflowRunTx(user: string) {
619
- return await dataSource.runTransaction(() => insertFunction(user), { name: 'insertFunction' });
620
- }
621
-
622
- async function errorWorkflowReg(user: string) {
623
- return await regErrorFunction(user);
624
- }
625
-
626
- async function errorWorkflowRunTx(user: string) {
627
- return await dataSource.runTransaction(() => errorFunction(user), { name: 'errorFunction' });
628
- }
629
-
630
- async function readWorkflowReg(user: string) {
631
- return await regReadFunction(user);
632
- }
633
-
634
- async function readWorkflowRunTx(user: string) {
635
- return await dataSource.runTransaction(() => readFunction(user), { name: 'readFunction', readOnly: true });
636
- }
637
-
638
- async function staticWorkflow(user: string) {
639
- const result = await StaticClass.insertFunction(user);
640
- const readResult = await StaticClass.readFunction(user);
641
- return [result, readResult];
642
- }
643
-
644
- async function instanceWorkflow(user: string) {
645
- const instance = new InstanceClass();
646
- const result = await instance.insertFunction(user);
647
- const readResult = await instance.readFunction(user);
648
- return [result, readResult];
649
- }
650
-
651
- const regInsertWorkflowReg = DBOS.registerWorkflow(insertWorkflowReg);
652
- const regInsertWorkflowRunTx = DBOS.registerWorkflow(insertWorkflowRunTx);
653
- const regErrorWorkflowReg = DBOS.registerWorkflow(errorWorkflowReg);
654
- const regErrorWorkflowRunTx = DBOS.registerWorkflow(errorWorkflowRunTx);
655
- const regReadWorkflowReg = DBOS.registerWorkflow(readWorkflowReg);
656
- const regReadWorkflowRunTx = DBOS.registerWorkflow(readWorkflowRunTx);
657
- const regStaticWorkflow = DBOS.registerWorkflow(staticWorkflow);
658
- const regInstanceWorkflow = DBOS.registerWorkflow(instanceWorkflow);
659
-
660
- describe('TypeOrmDataSourceCreateTxC', () => {
661
- const userDB = new Pool(config);
662
-
663
- beforeAll(async () => {
664
- await createDatabases(false);
665
- });
666
-
667
- afterAll(async () => {
668
- await userDB.end();
669
- });
670
-
671
- beforeEach(async () => {
672
- DBOS.setConfig({ name: 'typeorm-ds-test' });
673
- await DBOS.launch();
674
- });
675
-
676
- afterEach(async () => {
677
- await DBOS.shutdown();
678
- });
679
-
680
- test('insert dataSource.register function', async () => {
681
- const user = 'helloTest1';
682
-
683
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
684
- const workflowID = randomUUID();
685
-
686
- await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowReg(user))).resolves.toMatchObject({
687
- user,
688
- greet_count: 1,
689
- });
690
-
691
- const { rows } = await userDB.query<transaction_completion>(
692
- 'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
693
- [workflowID],
694
- );
695
- // Completion cleared the checkpoint the transaction wrote.
696
- expect(rows).toHaveLength(0);
697
- });
698
- });
699
-
700
- describe('TypeOrmDataSource migrations', () => {
701
- const userDB = new Pool(config);
702
-
703
- afterAll(async () => {
704
- await userDB.end();
705
- });
706
-
707
- afterEach(async () => {
708
- await DBOS.shutdown();
709
- });
710
-
711
- test('verify-only initialization requires the schema to be migrated out of band', async () => {
712
- const suffix = randomUUID().replace(/-/g, '').slice(0, 8);
713
- const schemaName = `ds_mig_${suffix}`;
714
- const role = `ds_app_${suffix}`;
715
- const admin = new Client({ ...config, database: 'postgres' });
716
- await admin.connect();
717
- try {
718
- await ensureDB(admin, config.database);
719
- } finally {
720
- await admin.end();
721
- }
722
- await userDB.query(`CREATE ROLE "${role}"`);
723
- try {
724
- TypeOrmDataSource.createFromConfig(`verify-only-${suffix}`, config, [Greeting], schemaName, {
725
- runMigrations: false,
726
- });
727
- DBOS.setConfig({ name: 'typeorm-ds-test' });
728
- await expect(DBOS.launch()).rejects.toThrow(/is at transaction schema version 0/);
729
- await DBOS.shutdown();
730
- const { rowCount } = await userDB.query('SELECT 1 FROM pg_namespace WHERE nspname = $1', [schemaName]);
731
- expect(rowCount).toBe(0);
732
-
733
- await TypeOrmDataSource.initializeDBOSSchema(config, schemaName, { applicationRole: role });
734
- await DBOS.launch();
735
- const { rows } = await userDB.query<{ allowed: boolean }>(
736
- `SELECT has_table_privilege($1, $2, 'INSERT') AS allowed`,
737
- [role, `"${schemaName}".transaction_completion`],
738
- );
739
- expect(rows).toEqual([{ allowed: true }]);
740
- } finally {
741
- await userDB.query(`DROP SCHEMA IF EXISTS "${schemaName}" CASCADE`);
742
- await userDB.query(`DROP OWNED BY "${role}"`);
743
- await userDB.query(`DROP ROLE "${role}"`);
744
- }
745
- });
746
- });