@dbos-inc/typeorm-datasource 4.26.5-preview → 4.26.7-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.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -30
- package/dist/index.js.map +1 -1
- package/index.ts +44 -36
- package/package.json +1 -1
- package/tests/datasource.test.ts +93 -0
- package/tests/usersuppliedds.test.ts +24 -4
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAChC,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAChC,OAAO,EAAQ,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAUL,cAAc,EAEd,mBAAmB,EAGpB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAIpD,MAAM,WAAW,wBAAyB,SAAQ,mBAAmB;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAuPD,qBAAa,iBAAkB,YAAW,cAAc,CAAC,wBAAwB,CAAC;;IAyC9E,QAAQ,CAAC,IAAI,EAAE,MAAM;IAxBvB,MAAM,KAAK,aAAa,kBAEvB;IAED,IAAI,aAAa,kBAEhB;WAEY,oBAAoB,CAAC,MAAM,EAAE,UAAU,EAAE,UAAU,GAAE,MAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAYjG;;OAEG;gBAEQ,IAAI,EAAE,MAAM,EACrB,MAAM,CAAC,EAAE,UAAU,EAEnB,QAAQ,CAAC,EAAE,QAAQ,EAAE,EACrB,UAAU,CAAC,EAAE,UAAU,EACvB,UAAU,GAAE,MAAe;IAe7B,MAAM,CAAC,gBAAgB,CACrB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,UAAU,EAElB,QAAQ,EAAE,QAAQ,EAAE,EACpB,UAAU,GAAE,MAAe;IAK7B,MAAM,CAAC,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,EAAE,UAAU,GAAE,MAAe;IAIrF;;;;;;OAMG;IACG,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,wBAAwB;IAIjF;;;;;;;;OAQG;IACH,mBAAmB,CAAC,IAAI,EAAE,IAAI,SAAS,OAAO,EAAE,EAAE,MAAM,EACtD,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,EACpD,MAAM,CAAC,EAAE,wBAAwB,GAAG,YAAY,GAC/C,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC;IAIjD;;OAEG;IACH,WAAW,CAAC,MAAM,CAAC,EAAE,wBAAwB,kDAIjC,MAAM,eACD,WAAW,cACZ,wBAAwB,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,IAAI,KAAK,QAAQ,MAAM,CAAC,CAAC,oCAAxC,IAAI,WAAW,IAAI,KAAK,QAAQ,MAAM,CAAC;CAevF"}
|
package/dist/index.js
CHANGED
|
@@ -87,39 +87,34 @@ class TypeOrmTransactionHandler {
|
|
|
87
87
|
if (rows.length !== 1) {
|
|
88
88
|
return undefined;
|
|
89
89
|
}
|
|
90
|
-
if (rows[0].output === null) {
|
|
91
|
-
return undefined;
|
|
92
|
-
}
|
|
93
90
|
const { output, error } = rows[0];
|
|
94
91
|
return error !== null ? { error } : { output };
|
|
95
92
|
}
|
|
96
93
|
static async #recordOutput(entityManager, workflowID, stepID, output, schemaName) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
throw new dbos_sdk_1.DBOSWorkflowConflictError(workflowID);
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
throw error;
|
|
107
|
-
}
|
|
94
|
+
const rows = await entityManager.query(`INSERT INTO "${schemaName}".transaction_completion (workflow_id, function_num, output)
|
|
95
|
+
VALUES ($1, $2, $3)
|
|
96
|
+
ON CONFLICT (workflow_id, function_num) DO NOTHING
|
|
97
|
+
RETURNING workflow_id`, [workflowID, stepID, output]);
|
|
98
|
+
if (rows.length === 0) {
|
|
99
|
+
throw new datasource_1.DBOSStepAlreadyRecordedError(workflowID, stepID);
|
|
108
100
|
}
|
|
109
101
|
}
|
|
110
102
|
async #recordError(workflowID, stepID, error) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
103
|
+
const rows = await this.dataSource.query(`INSERT INTO "${this.schemaName}".transaction_completion (workflow_id, function_num, error)
|
|
104
|
+
VALUES ($1, $2, $3)
|
|
105
|
+
ON CONFLICT (workflow_id, function_num) DO NOTHING
|
|
106
|
+
RETURNING workflow_id`, [workflowID, stepID, error]);
|
|
107
|
+
if (rows.length === 0) {
|
|
108
|
+
throw new datasource_1.DBOSStepAlreadyRecordedError(workflowID, stepID);
|
|
114
109
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
110
|
+
}
|
|
111
|
+
// A duplicate execution won the race, so its recorded outcome is the durable one.
|
|
112
|
+
async #replayConflictingStep(workflowID, stepID) {
|
|
113
|
+
const recorded = await this.#checkExecution(workflowID, stepID);
|
|
114
|
+
if (recorded === undefined) {
|
|
115
|
+
throw new datasource_1.DBOSError(`Step ${stepID} of workflow ${workflowID} conflicted with a concurrent execution, but no recorded outcome was found`);
|
|
122
116
|
}
|
|
117
|
+
return (0, datasource_1.replayRecordedStep)(recorded);
|
|
123
118
|
}
|
|
124
119
|
/* Required by base class */
|
|
125
120
|
async invokeTransactionFunction(config, target, func, ...args) {
|
|
@@ -139,11 +134,7 @@ class TypeOrmTransactionHandler {
|
|
|
139
134
|
// Check to see if this tx has already been executed
|
|
140
135
|
const previousResult = saveResults ? await this.#checkExecution(workflowID, stepID) : undefined;
|
|
141
136
|
if (previousResult) {
|
|
142
|
-
|
|
143
|
-
if ('error' in previousResult) {
|
|
144
|
-
throw superjson_1.SuperJSON.parse(previousResult.error);
|
|
145
|
-
}
|
|
146
|
-
return (previousResult.output ? superjson_1.SuperJSON.parse(previousResult.output) : null);
|
|
137
|
+
return (0, datasource_1.replayRecordedStep)(previousResult);
|
|
147
138
|
}
|
|
148
139
|
try {
|
|
149
140
|
const result = await this.dataSource.transaction(isolationLevel, async (entityManager) => {
|
|
@@ -162,6 +153,9 @@ class TypeOrmTransactionHandler {
|
|
|
162
153
|
return result;
|
|
163
154
|
}
|
|
164
155
|
catch (error) {
|
|
156
|
+
if (saveResults && error instanceof datasource_1.DBOSStepAlreadyRecordedError) {
|
|
157
|
+
return await this.#replayConflictingStep(workflowID, stepID);
|
|
158
|
+
}
|
|
165
159
|
if ((0, datasource_1.isPGRetriableTransactionError)(error)) {
|
|
166
160
|
dbos_sdk_1.DBOS.span?.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMS }, performance.now());
|
|
167
161
|
// Retry serialization failures.
|
|
@@ -172,7 +166,15 @@ class TypeOrmTransactionHandler {
|
|
|
172
166
|
else {
|
|
173
167
|
if (saveResults) {
|
|
174
168
|
const message = superjson_1.SuperJSON.stringify(error);
|
|
175
|
-
|
|
169
|
+
try {
|
|
170
|
+
await this.#recordError(workflowID, stepID, message);
|
|
171
|
+
}
|
|
172
|
+
catch (recordError) {
|
|
173
|
+
if (recordError instanceof datasource_1.DBOSStepAlreadyRecordedError) {
|
|
174
|
+
return await this.#replayConflictingStep(workflowID, stepID);
|
|
175
|
+
}
|
|
176
|
+
throw recordError;
|
|
177
|
+
}
|
|
176
178
|
}
|
|
177
179
|
throw error;
|
|
178
180
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":";;;AACA,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
|
package/index.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { PoolConfig } from 'pg';
|
|
2
|
-
import { DBOS,
|
|
2
|
+
import { DBOS, FunctionName } from '@dbos-inc/dbos-sdk';
|
|
3
3
|
import {
|
|
4
4
|
type DataSourceTransactionHandler,
|
|
5
5
|
createTransactionCompletionSchemaPG,
|
|
6
6
|
createTransactionCompletionTablePG,
|
|
7
7
|
isPGRetriableTransactionError,
|
|
8
|
-
|
|
8
|
+
DBOSError,
|
|
9
|
+
DBOSStepAlreadyRecordedError,
|
|
10
|
+
replayRecordedStep,
|
|
9
11
|
registerTransaction,
|
|
10
12
|
runTransaction,
|
|
11
13
|
DBOSDataSource,
|
|
@@ -136,10 +138,6 @@ class TypeOrmTransactionHandler implements DataSourceTransactionHandler {
|
|
|
136
138
|
return undefined;
|
|
137
139
|
}
|
|
138
140
|
|
|
139
|
-
if (rows[0].output === null) {
|
|
140
|
-
return undefined;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
141
|
const { output, error } = rows[0];
|
|
144
142
|
return error !== null ? { error } : { output };
|
|
145
143
|
}
|
|
@@ -151,35 +149,40 @@ class TypeOrmTransactionHandler implements DataSourceTransactionHandler {
|
|
|
151
149
|
output: string,
|
|
152
150
|
schemaName: string,
|
|
153
151
|
): Promise<void> {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
} else {
|
|
164
|
-
throw error;
|
|
165
|
-
}
|
|
152
|
+
const rows = await entityManager.query<{ workflow_id: string }[]>(
|
|
153
|
+
`INSERT INTO "${schemaName}".transaction_completion (workflow_id, function_num, output)
|
|
154
|
+
VALUES ($1, $2, $3)
|
|
155
|
+
ON CONFLICT (workflow_id, function_num) DO NOTHING
|
|
156
|
+
RETURNING workflow_id`,
|
|
157
|
+
[workflowID, stepID, output],
|
|
158
|
+
);
|
|
159
|
+
if (rows.length === 0) {
|
|
160
|
+
throw new DBOSStepAlreadyRecordedError(workflowID, stepID);
|
|
166
161
|
}
|
|
167
162
|
}
|
|
168
163
|
|
|
169
164
|
async #recordError(workflowID: string, stepID: number, error: string): Promise<void> {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
165
|
+
const rows = await this.dataSource.query<{ workflow_id: string }[]>(
|
|
166
|
+
`INSERT INTO "${this.schemaName}".transaction_completion (workflow_id, function_num, error)
|
|
167
|
+
VALUES ($1, $2, $3)
|
|
168
|
+
ON CONFLICT (workflow_id, function_num) DO NOTHING
|
|
169
|
+
RETURNING workflow_id`,
|
|
170
|
+
[workflowID, stepID, error],
|
|
171
|
+
);
|
|
172
|
+
if (rows.length === 0) {
|
|
173
|
+
throw new DBOSStepAlreadyRecordedError(workflowID, stepID);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// A duplicate execution won the race, so its recorded outcome is the durable one.
|
|
178
|
+
async #replayConflictingStep<Return>(workflowID: string, stepID: number): Promise<Return> {
|
|
179
|
+
const recorded = await this.#checkExecution(workflowID, stepID);
|
|
180
|
+
if (recorded === undefined) {
|
|
181
|
+
throw new DBOSError(
|
|
182
|
+
`Step ${stepID} of workflow ${workflowID} conflicted with a concurrent execution, but no recorded outcome was found`,
|
|
175
183
|
);
|
|
176
|
-
} catch (error) {
|
|
177
|
-
if (isPGKeyConflictError(error)) {
|
|
178
|
-
throw new DBOSWorkflowConflictError(workflowID);
|
|
179
|
-
} else {
|
|
180
|
-
throw error;
|
|
181
|
-
}
|
|
182
184
|
}
|
|
185
|
+
return replayRecordedStep<Return>(recorded);
|
|
183
186
|
}
|
|
184
187
|
|
|
185
188
|
/* Required by base class */
|
|
@@ -208,12 +211,7 @@ class TypeOrmTransactionHandler implements DataSourceTransactionHandler {
|
|
|
208
211
|
// Check to see if this tx has already been executed
|
|
209
212
|
const previousResult = saveResults ? await this.#checkExecution(workflowID, stepID!) : undefined;
|
|
210
213
|
if (previousResult) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if ('error' in previousResult) {
|
|
214
|
-
throw SuperJSON.parse(previousResult.error);
|
|
215
|
-
}
|
|
216
|
-
return (previousResult.output ? SuperJSON.parse(previousResult.output) : null) as Return;
|
|
214
|
+
return replayRecordedStep<Return>(previousResult);
|
|
217
215
|
}
|
|
218
216
|
|
|
219
217
|
try {
|
|
@@ -242,6 +240,9 @@ class TypeOrmTransactionHandler implements DataSourceTransactionHandler {
|
|
|
242
240
|
|
|
243
241
|
return result;
|
|
244
242
|
} catch (error) {
|
|
243
|
+
if (saveResults && error instanceof DBOSStepAlreadyRecordedError) {
|
|
244
|
+
return await this.#replayConflictingStep<Return>(workflowID, stepID!);
|
|
245
|
+
}
|
|
245
246
|
if (isPGRetriableTransactionError(error)) {
|
|
246
247
|
DBOS.span?.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMS }, performance.now());
|
|
247
248
|
// Retry serialization failures.
|
|
@@ -251,7 +252,14 @@ class TypeOrmTransactionHandler implements DataSourceTransactionHandler {
|
|
|
251
252
|
} else {
|
|
252
253
|
if (saveResults) {
|
|
253
254
|
const message = SuperJSON.stringify(error);
|
|
254
|
-
|
|
255
|
+
try {
|
|
256
|
+
await this.#recordError(workflowID, stepID!, message);
|
|
257
|
+
} catch (recordError) {
|
|
258
|
+
if (recordError instanceof DBOSStepAlreadyRecordedError) {
|
|
259
|
+
return await this.#replayConflictingStep<Return>(workflowID, stepID!);
|
|
260
|
+
}
|
|
261
|
+
throw recordError;
|
|
262
|
+
}
|
|
255
263
|
}
|
|
256
264
|
|
|
257
265
|
throw error;
|
package/package.json
CHANGED
package/tests/datasource.test.ts
CHANGED
|
@@ -307,6 +307,57 @@ describe('TypeOrmDataSource', () => {
|
|
|
307
307
|
]);
|
|
308
308
|
expect(txResults.rows.length).toBe(0);
|
|
309
309
|
});
|
|
310
|
+
|
|
311
|
+
test('duplicate execution replays the winner result', async () => {
|
|
312
|
+
await userDB.query('DELETE FROM race_side_effects');
|
|
313
|
+
raceState.callCount = 0;
|
|
314
|
+
|
|
315
|
+
// A loser whose body succeeds: the collision happens on the result-recording insert.
|
|
316
|
+
raceState.plantWinner = true;
|
|
317
|
+
raceState.plantWinnerError = false;
|
|
318
|
+
raceState.fail = false;
|
|
319
|
+
const wfid1 = randomUUID();
|
|
320
|
+
await expect(DBOS.withNextWorkflowID(wfid1, () => regRaceWorkflow())).resolves.toBe('winner-result');
|
|
321
|
+
expect(raceState.callCount).toBe(1); // the loser did run its body
|
|
322
|
+
|
|
323
|
+
// A loser whose body fails: the collision moves to the error-recording insert,
|
|
324
|
+
// and the winner's result still wins over the loser's own error.
|
|
325
|
+
raceState.plantWinner = true;
|
|
326
|
+
raceState.plantWinnerError = false;
|
|
327
|
+
raceState.fail = true;
|
|
328
|
+
const wfid2 = randomUUID();
|
|
329
|
+
await expect(DBOS.withNextWorkflowID(wfid2, () => regRaceWorkflow())).resolves.toBe('winner-result');
|
|
330
|
+
expect(raceState.callCount).toBe(2);
|
|
331
|
+
|
|
332
|
+
// A winner that recorded an error: the loser replays that throw instead of its own result.
|
|
333
|
+
raceState.plantWinner = true;
|
|
334
|
+
raceState.plantWinnerError = true;
|
|
335
|
+
raceState.fail = false;
|
|
336
|
+
const wfid3 = randomUUID();
|
|
337
|
+
const winnerError = await throws(() => DBOS.withNextWorkflowID(wfid3, () => regRaceWorkflow()));
|
|
338
|
+
expect((winnerError as Error).message).toBe('winner-error');
|
|
339
|
+
expect(raceState.callCount).toBe(3);
|
|
340
|
+
|
|
341
|
+
// Every loser's writes were discarded and the winners' records still stand.
|
|
342
|
+
const { rows: tags } = await userDB.query<{ tag: string }>('SELECT tag FROM race_side_effects');
|
|
343
|
+
expect(tags).toHaveLength(0);
|
|
344
|
+
const { rows: txOutput } = await userDB.query<transaction_completion>(
|
|
345
|
+
'SELECT * FROM dbos.transaction_completion WHERE workflow_id = ANY($1)',
|
|
346
|
+
[[wfid1, wfid2]],
|
|
347
|
+
);
|
|
348
|
+
expect(txOutput).toHaveLength(2);
|
|
349
|
+
for (const row of txOutput) {
|
|
350
|
+
expect(row.error).toBeNull(); // no loser error was ever recorded
|
|
351
|
+
expect(SuperJSON.parse(row.output!)).toBe('winner-result');
|
|
352
|
+
}
|
|
353
|
+
const { rows: txError } = await userDB.query<transaction_completion>(
|
|
354
|
+
'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
|
|
355
|
+
[wfid3],
|
|
356
|
+
);
|
|
357
|
+
expect(txError).toHaveLength(1);
|
|
358
|
+
expect(txError[0].output).toBeNull(); // the loser never overwrote the winner's error with its own result
|
|
359
|
+
expect(SuperJSON.parse<Error>(txError[0].error!).message).toBe('winner-error');
|
|
360
|
+
});
|
|
310
361
|
});
|
|
311
362
|
|
|
312
363
|
export interface greetings {
|
|
@@ -341,6 +392,7 @@ async function createDatabases(createTxCompletions: boolean) {
|
|
|
341
392
|
});
|
|
342
393
|
await ds.initialize();
|
|
343
394
|
await ds.synchronize();
|
|
395
|
+
await ds.query('CREATE TABLE race_side_effects(tag text NOT NULL)');
|
|
344
396
|
await ds.destroy();
|
|
345
397
|
}
|
|
346
398
|
}
|
|
@@ -361,6 +413,47 @@ async function errorFunction(user: string) {
|
|
|
361
413
|
throw new Error(`test error ${Date.now()}`);
|
|
362
414
|
}
|
|
363
415
|
|
|
416
|
+
const raceState = { callCount: 0, plantWinner: false, plantWinnerError: false, fail: false };
|
|
417
|
+
|
|
418
|
+
async function raceFunction() {
|
|
419
|
+
raceState.callCount += 1;
|
|
420
|
+
await dataSource.entityManager.sql`INSERT INTO race_side_effects(tag) VALUES (${`run-${raceState.callCount}`})`;
|
|
421
|
+
if (raceState.plantWinner) {
|
|
422
|
+
raceState.plantWinner = false;
|
|
423
|
+
await plantWinnerCompletion();
|
|
424
|
+
}
|
|
425
|
+
if (raceState.fail) {
|
|
426
|
+
throw new Error("loser's own failure");
|
|
427
|
+
}
|
|
428
|
+
return `result-${raceState.callCount}`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// A duplicate execution commits its result while this transaction is still open, so this
|
|
432
|
+
// execution's pre-check missed it but its completion insert will collide with it.
|
|
433
|
+
async function plantWinnerCompletion() {
|
|
434
|
+
const winner = new Client(config);
|
|
435
|
+
try {
|
|
436
|
+
await winner.connect();
|
|
437
|
+
const [column, value] = raceState.plantWinnerError
|
|
438
|
+
? ['error', SuperJSON.stringify(new Error('winner-error'))]
|
|
439
|
+
: ['output', SuperJSON.stringify('winner-result')];
|
|
440
|
+
await winner.query(
|
|
441
|
+
`INSERT INTO dbos.transaction_completion (workflow_id, function_num, ${column}) VALUES ($1, $2, $3)`,
|
|
442
|
+
[DBOS.workflowID, DBOS.stepID, value],
|
|
443
|
+
);
|
|
444
|
+
} finally {
|
|
445
|
+
await winner.end();
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const regRaceFunction = dataSource.registerTransaction(raceFunction);
|
|
450
|
+
|
|
451
|
+
async function raceWorkflow() {
|
|
452
|
+
return await regRaceFunction();
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const regRaceWorkflow = DBOS.registerWorkflow(raceWorkflow);
|
|
456
|
+
|
|
364
457
|
async function readFunction(user: string) {
|
|
365
458
|
const result = await dataSource.entityManager.findOneBy(Greeting, { name: user });
|
|
366
459
|
|
|
@@ -7,6 +7,7 @@ import { DataSource } from 'typeorm';
|
|
|
7
7
|
import { TypeOrmDataSource } from '..';
|
|
8
8
|
|
|
9
9
|
import { DBOS } from '@dbos-inc/dbos-sdk';
|
|
10
|
+
import { Client } from 'pg';
|
|
10
11
|
import { ensurePGDatabase } from '../../../dist/src/database_utils';
|
|
11
12
|
|
|
12
13
|
const config = { user: process.env.PGUSER || 'postgres', database: 'typeorm_ds_test_datasource' };
|
|
@@ -51,12 +52,31 @@ const workflow = DBOS.registerWorkflow(
|
|
|
51
52
|
{ name: 'testTypeOrm' },
|
|
52
53
|
);
|
|
53
54
|
|
|
55
|
+
// ensurePGDatabase swallows every failure, so verify the postcondition and fail with the reason.
|
|
56
|
+
async function ensureTestDatabase(databaseUrl: string) {
|
|
57
|
+
const notes: string[] = [];
|
|
58
|
+
const record = (msg: string) => {
|
|
59
|
+
notes.push(msg);
|
|
60
|
+
};
|
|
61
|
+
await ensurePGDatabase(databaseUrl, { info: record, warn: record });
|
|
62
|
+
const client = new Client({ connectionString: databaseUrl });
|
|
63
|
+
try {
|
|
64
|
+
await client.connect();
|
|
65
|
+
// CockroachDB connects to a nonexistent database, so hit a catalog that requires it to exist.
|
|
66
|
+
await client.query('SELECT 1 FROM information_schema.schemata LIMIT 1');
|
|
67
|
+
} catch (e) {
|
|
68
|
+
const why = notes.length ? ` [${notes.join('; ')}]` : '';
|
|
69
|
+
throw new Error(`Could not provision the test database: ${(e as Error).message}${why}`);
|
|
70
|
+
} finally {
|
|
71
|
+
await client.end().catch(() => {});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
54
75
|
describe('TypeOrmDataSourceDemo', () => {
|
|
55
76
|
beforeEach(async () => {
|
|
56
|
-
await
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
});
|
|
77
|
+
await ensureTestDatabase(
|
|
78
|
+
`postgresql://${config.user}:${process.env['PGPASSWORD'] || 'dbos'}@${process.env['PGHOST'] || 'localhost'}:${process.env['PGPORT'] || '5432'}/typeorm_demo_ds`,
|
|
79
|
+
);
|
|
60
80
|
await TypeOrmDataSource.initializeDBOSSchema(config);
|
|
61
81
|
await AppDataSource.initialize();
|
|
62
82
|
|