@dbos-inc/knex-datasource 3.0.6-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/index.ts ADDED
@@ -0,0 +1,230 @@
1
+ // using https://github.com/knex/knex
2
+
3
+ import { DBOS, DBOSWorkflowConflictError } from '@dbos-inc/dbos-sdk';
4
+ import {
5
+ type DataSourceTransactionHandler,
6
+ isPGRetriableTransactionError,
7
+ isPGKeyConflictError,
8
+ registerTransaction,
9
+ runTransaction,
10
+ DBOSDataSource,
11
+ registerDataSource,
12
+ } from '@dbos-inc/dbos-sdk/datasource';
13
+ import { AsyncLocalStorage } from 'async_hooks';
14
+ import knex, { type Knex } from 'knex';
15
+ import { SuperJSON } from 'superjson';
16
+
17
+ interface transaction_completion {
18
+ workflow_id: string;
19
+ function_num: number;
20
+ output: string | null;
21
+ }
22
+
23
+ interface KnexDataSourceContext {
24
+ client: Knex.Transaction;
25
+ }
26
+
27
+ export type TransactionConfig = Pick<Knex.TransactionConfig, 'isolationLevel' | 'readOnly'>;
28
+
29
+ const asyncLocalCtx = new AsyncLocalStorage<KnexDataSourceContext>();
30
+
31
+ class KnexDSTH implements DataSourceTransactionHandler {
32
+ readonly name: string;
33
+ readonly dsType = 'KnexDataSource';
34
+ readonly #knexDB: Knex;
35
+
36
+ constructor(name: string, config: Knex.Config) {
37
+ this.name = name;
38
+ this.#knexDB = knex(config);
39
+ }
40
+
41
+ initialize(): Promise<void> {
42
+ return Promise.resolve();
43
+ }
44
+
45
+ destroy(): Promise<void> {
46
+ return this.#knexDB.destroy();
47
+ }
48
+
49
+ static async #checkExecution(
50
+ client: Knex.Transaction,
51
+ workflowID: string,
52
+ functionNum: number,
53
+ ): Promise<{ output: string | null } | undefined> {
54
+ const result = await client<transaction_completion>('transaction_completion')
55
+ .withSchema('dbos')
56
+ .select('output')
57
+ .where({
58
+ workflow_id: workflowID,
59
+ function_num: functionNum,
60
+ })
61
+ .first();
62
+ return result === undefined ? undefined : { output: result.output };
63
+ }
64
+
65
+ static async #recordOutput(
66
+ client: Knex.Transaction,
67
+ workflowID: string,
68
+ functionNum: number,
69
+ output: string | null,
70
+ ): Promise<void> {
71
+ try {
72
+ await client<transaction_completion>('transaction_completion').withSchema('dbos').insert({
73
+ workflow_id: workflowID,
74
+ function_num: functionNum,
75
+ output,
76
+ });
77
+ } catch (error) {
78
+ if (isPGKeyConflictError(error)) {
79
+ throw new DBOSWorkflowConflictError(workflowID);
80
+ } else {
81
+ throw error;
82
+ }
83
+ }
84
+ }
85
+
86
+ async invokeTransactionFunction<This, Args extends unknown[], Return>(
87
+ config: TransactionConfig | undefined,
88
+ target: This,
89
+ func: (this: This, ...args: Args) => Promise<Return>,
90
+ ...args: Args
91
+ ): Promise<Return> {
92
+ const workflowID = DBOS.workflowID;
93
+ if (workflowID === undefined) {
94
+ throw new Error('Workflow ID is not set.');
95
+ }
96
+ const functionNum = DBOS.stepID;
97
+ if (functionNum === undefined) {
98
+ throw new Error('Function Number is not set.');
99
+ }
100
+
101
+ const readOnly = config?.readOnly ?? false;
102
+ let retryWaitMS = 1;
103
+ const backoffFactor = 1.5;
104
+ const maxRetryWaitMS = 2000;
105
+
106
+ while (true) {
107
+ try {
108
+ const result = await this.#knexDB.transaction<Return>(
109
+ async (client) => {
110
+ // Check to see if this tx has already been executed
111
+ const previousResult =
112
+ readOnly || !workflowID ? undefined : await KnexDSTH.#checkExecution(client, workflowID, functionNum);
113
+ if (previousResult) {
114
+ return (previousResult.output ? SuperJSON.parse(previousResult.output) : null) as Return;
115
+ }
116
+
117
+ // execute user's transaction function
118
+ const result = await asyncLocalCtx.run({ client }, async () => {
119
+ return (await func.call(target, ...args)) as Return;
120
+ });
121
+
122
+ // save the output of read/write transactions
123
+ if (!readOnly && workflowID) {
124
+ await KnexDSTH.#recordOutput(client, workflowID, functionNum, SuperJSON.stringify(result));
125
+
126
+ // Note, existing code wraps #recordOutput call in a try/catch block that
127
+ // converts DB error with code 25P02 to DBOSFailedSqlTransactionError.
128
+ // However, existing code doesn't make any logic decisions based on that error type.
129
+ // DBOSFailedSqlTransactionError does stored WF ID and function name, so I assume that info is logged out somewhere
130
+ }
131
+
132
+ return result;
133
+ },
134
+ { isolationLevel: config?.isolationLevel, readOnly: config?.readOnly },
135
+ );
136
+ // TODO: span.setStatus({ code: SpanStatusCode.OK });
137
+ // TODO: this.tracer.endSpan(span);
138
+
139
+ return result;
140
+ } catch (error) {
141
+ if (isPGRetriableTransactionError(error)) {
142
+ // TODO: span.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMillis }, performance.now());
143
+ await new Promise((resolve) => setTimeout(resolve, retryWaitMS));
144
+ retryWaitMS = Math.min(retryWaitMS * backoffFactor, maxRetryWaitMS);
145
+ continue;
146
+ } else {
147
+ // TODO: span.setStatus({ code: SpanStatusCode.ERROR, message: e.message });
148
+ // TODO: this.tracer.endSpan(span);
149
+
150
+ // TODO: currently, we are *not* recording errors in the txOutput table.
151
+ // For normal execution, this is fine because we also store tx step results (output and error) in the sysdb operation output table.
152
+ // However, I'm concerned that we have a dueling execution hole where one tx fails while another succeeds.
153
+ // This implies that we can end up in a situation where the step output records an error but the txOutput table records success.
154
+
155
+ throw error;
156
+ }
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ export class KnexDataSource implements DBOSDataSource<TransactionConfig> {
163
+ static get client(): Knex.Transaction {
164
+ if (!DBOS.isInTransaction()) {
165
+ throw new Error('invalid use of KnexDataSource.client outside of a DBOS transaction.');
166
+ }
167
+ const ctx = asyncLocalCtx.getStore();
168
+ if (!ctx) {
169
+ throw new Error('No async local context found.');
170
+ }
171
+ return ctx.client;
172
+ }
173
+
174
+ static async initializeInternalSchema(config: Knex.Config) {
175
+ const knexDB = knex(config);
176
+ try {
177
+ await knexDB.schema.createSchemaIfNotExists('dbos');
178
+ const exists = await knexDB.schema.withSchema('dbos').hasTable('transaction_completion');
179
+ if (!exists) {
180
+ await knexDB.schema.withSchema('dbos').createTable('transaction_completion', (table) => {
181
+ table.string('workflow_id').notNullable();
182
+ table.integer('function_num').notNullable();
183
+ table.string('output').nullable();
184
+ table.primary(['workflow_id', 'function_num']);
185
+ });
186
+ }
187
+ } finally {
188
+ await knexDB.destroy();
189
+ }
190
+ }
191
+
192
+ readonly name: string;
193
+ #provider: KnexDSTH;
194
+
195
+ constructor(name: string, config: Knex.Config) {
196
+ this.name = name;
197
+ this.#provider = new KnexDSTH(name, config);
198
+ registerDataSource(this.#provider);
199
+ }
200
+
201
+ async runTransaction<T>(callback: () => Promise<T>, funcName: string, config?: TransactionConfig) {
202
+ return await runTransaction(callback, funcName, { dsName: this.name, config });
203
+ }
204
+
205
+ registerTransaction<This, Args extends unknown[], Return>(
206
+ func: (this: This, ...args: Args) => Promise<Return>,
207
+ name: string,
208
+ config?: TransactionConfig,
209
+ ): (this: This, ...args: Args) => Promise<Return> {
210
+ return registerTransaction(this.name, func, { name }, config);
211
+ }
212
+
213
+ transaction(config?: TransactionConfig) {
214
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
215
+ const ds = this;
216
+ return function decorator<This, Args extends unknown[], Return>(
217
+ _target: object,
218
+ propertyKey: string,
219
+ descriptor: TypedPropertyDescriptor<(this: This, ...args: Args) => Promise<Return>>,
220
+ ) {
221
+ if (!descriptor.value) {
222
+ throw Error('Use of decorator when original method is undefined');
223
+ }
224
+
225
+ descriptor.value = ds.registerTransaction(descriptor.value, propertyKey.toString(), config);
226
+
227
+ return descriptor;
228
+ };
229
+ }
230
+ }
package/jest.config.js ADDED
@@ -0,0 +1,8 @@
1
+ /** @type {import('ts-jest').JestConfigWithTsJest} */
2
+ module.exports = {
3
+ preset: 'ts-jest',
4
+ testEnvironment: 'node',
5
+ testRegex: '((\\.|/)(test|spec))\\.ts?$',
6
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
7
+ modulePaths: ['./'],
8
+ };
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@dbos-inc/knex-datasource",
3
+ "version": "3.0.6-preview",
4
+ "description": "",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/dbos-inc/dbos-transact-ts",
9
+ "directory": "packages/knex-datasource"
10
+ },
11
+ "homepage": "https://docs.dbos.dev/",
12
+ "main": "index.js",
13
+ "scripts": {
14
+ "build": "tsc --project tsconfig.json",
15
+ "test": "jest --detectOpenHandles"
16
+ },
17
+ "dependencies": {
18
+ "knex": "^3.1.0",
19
+ "superjson": "^1.13"
20
+ },
21
+ "peerDependencies": {
22
+ "@dbos-inc/dbos-sdk": "*"
23
+ },
24
+ "devDependencies": {
25
+ "@types/jest": "^29.5.14",
26
+ "@types/pg": "^8.15.2",
27
+ "jest": "^29.7.0",
28
+ "pg": "^8.11.3",
29
+ "typescript": "^5.4.5"
30
+ }
31
+ }
@@ -0,0 +1,31 @@
1
+ import { Client } from 'pg';
2
+ import { KnexDataSource } from '../index';
3
+ import { dropDB, ensureDB } from './test-helpers';
4
+
5
+ describe('KnexDataSource.configure', () => {
6
+ const config = { user: 'postgres', database: 'knex_ds_config_test' };
7
+
8
+ beforeAll(async () => {
9
+ const client = new Client({ ...config, database: 'postgres' });
10
+ try {
11
+ await client.connect();
12
+ await dropDB(client, config.database);
13
+ await ensureDB(client, config.database);
14
+ } finally {
15
+ await client.end();
16
+ }
17
+ });
18
+
19
+ test('configure creates tx outputs table', async () => {
20
+ await KnexDataSource.initializeInternalSchema({ client: 'pg', connection: config });
21
+
22
+ const client = new Client(config);
23
+ try {
24
+ await client.connect();
25
+ const result = await client.query('SELECT workflow_id, function_num, output FROM dbos.transaction_completion');
26
+ expect(result.rows.length).toBe(0);
27
+ } finally {
28
+ await client.end();
29
+ }
30
+ });
31
+ });
@@ -0,0 +1,310 @@
1
+ import { DBOS } from '@dbos-inc/dbos-sdk';
2
+ import { Client, Pool } from 'pg';
3
+ import { KnexDataSource } from '..';
4
+ import { dropDB, ensureDB } from './test-helpers';
5
+ import { randomUUID } from 'crypto';
6
+ import SuperJSON from 'superjson';
7
+
8
+ const config = { client: 'pg', connection: { user: 'postgres', database: 'knex_ds_test_userdb' } };
9
+ const dataSource = new KnexDataSource('app-db', config);
10
+
11
+ interface transaction_completion {
12
+ workflow_id: string;
13
+ function_num: number;
14
+ output: string | null;
15
+ }
16
+
17
+ describe('KnexDataSource', () => {
18
+ const userDB = new Pool(config.connection);
19
+
20
+ beforeAll(async () => {
21
+ {
22
+ const client = new Client({ ...config.connection, database: 'postgres' });
23
+ try {
24
+ await client.connect();
25
+ await dropDB(client, 'knex_ds_test');
26
+ await dropDB(client, 'knex_ds_test_dbos_sys');
27
+ await dropDB(client, config.connection.database);
28
+ await ensureDB(client, config.connection.database);
29
+ } finally {
30
+ await client.end();
31
+ }
32
+ }
33
+
34
+ {
35
+ const client = await userDB.connect();
36
+ try {
37
+ await client.query(
38
+ 'CREATE TABLE greetings(name text NOT NULL, greet_count integer DEFAULT 0, PRIMARY KEY(name))',
39
+ );
40
+ } finally {
41
+ client.release();
42
+ }
43
+ }
44
+
45
+ await KnexDataSource.initializeInternalSchema(config);
46
+ DBOS.setConfig({ name: 'knex-ds-test' });
47
+ await DBOS.launch();
48
+ });
49
+
50
+ afterAll(async () => {
51
+ await DBOS.shutdown();
52
+ await userDB.end();
53
+ });
54
+
55
+ test('insert dataSource.register function', async () => {
56
+ const user = 'helloTest1';
57
+
58
+ await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
59
+ const workflowID = randomUUID();
60
+
61
+ await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorfklowReg(user))).resolves.toEqual({
62
+ user,
63
+ greet_count: 1,
64
+ });
65
+
66
+ const { rows } = await userDB.query<transaction_completion>(
67
+ 'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
68
+ [workflowID],
69
+ );
70
+ expect(rows.length).toBe(1);
71
+ expect(rows[0].workflow_id).toBe(workflowID);
72
+ expect(rows[0].function_num).toBe(0);
73
+ expect(rows[0].output).not.toBeNull();
74
+ expect(SuperJSON.parse(rows[0].output!)).toEqual({ user, greet_count: 1 });
75
+ });
76
+
77
+ test('insert dataSource.runAsTx function', async () => {
78
+ const user = 'helloTest2';
79
+
80
+ await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
81
+ const workflowID = randomUUID();
82
+
83
+ await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorfklowRunTx(user))).resolves.toEqual({
84
+ user,
85
+ greet_count: 1,
86
+ });
87
+
88
+ const { rows } = await userDB.query<transaction_completion>(
89
+ 'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
90
+ [workflowID],
91
+ );
92
+ expect(rows.length).toBe(1);
93
+ expect(rows[0].workflow_id).toBe(workflowID);
94
+ expect(rows[0].function_num).toBe(0);
95
+ expect(rows[0].output).not.toBeNull();
96
+ expect(SuperJSON.parse(rows[0].output!)).toEqual({ user, greet_count: 1 });
97
+ });
98
+
99
+ test('error dataSource.register function', async () => {
100
+ const user = 'errorTest1';
101
+
102
+ await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
103
+ await userDB.query('INSERT INTO greetings("name","greet_count") VALUES($1,10);', [user]);
104
+ const workflowID = randomUUID();
105
+
106
+ await expect(DBOS.withNextWorkflowID(workflowID, () => regErrorWorkflowReg(user))).rejects.toThrow('test error');
107
+
108
+ const { rows: txOutput } = await userDB.query<transaction_completion>(
109
+ 'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
110
+ [workflowID],
111
+ );
112
+ expect(txOutput.length).toBe(0);
113
+
114
+ const { rows } = await userDB.query<greetings>('SELECT * FROM greetings WHERE name = $1', [user]);
115
+ expect(rows.length).toBe(1);
116
+ expect(rows[0].greet_count).toBe(10);
117
+ });
118
+
119
+ test('error dataSource.runAsTx function', async () => {
120
+ const user = 'errorTest2';
121
+
122
+ await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
123
+ await userDB.query('INSERT INTO greetings("name","greet_count") VALUES($1,10);', [user]);
124
+ const workflowID = randomUUID();
125
+
126
+ await expect(DBOS.withNextWorkflowID(workflowID, () => regErrorWorkflowRunTx(user))).rejects.toThrow('test error');
127
+
128
+ const { rows: txOutput } = await userDB.query<transaction_completion>(
129
+ 'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
130
+ [workflowID],
131
+ );
132
+ expect(txOutput.length).toBe(0);
133
+
134
+ const { rows } = await userDB.query<greetings>('SELECT * FROM greetings WHERE name = $1', [user]);
135
+ expect(rows.length).toBe(1);
136
+ expect(rows[0].greet_count).toBe(10);
137
+ });
138
+
139
+ test('readonly dataSource.register function', async () => {
140
+ const user = 'readTest1';
141
+
142
+ await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
143
+ await userDB.query('INSERT INTO greetings("name","greet_count") VALUES($1,10);', [user]);
144
+
145
+ const workflowID = randomUUID();
146
+ await expect(DBOS.withNextWorkflowID(workflowID, () => regReadWorkflowReg(user))).resolves.toEqual({
147
+ user,
148
+ greet_count: 10,
149
+ });
150
+
151
+ const { rows } = await userDB.query<transaction_completion>(
152
+ 'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
153
+ [workflowID],
154
+ );
155
+ expect(rows.length).toBe(0);
156
+ });
157
+
158
+ test('readonly dataSource.runAsTx function', async () => {
159
+ const user = 'readTest2';
160
+
161
+ await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
162
+ await userDB.query('INSERT INTO greetings("name","greet_count") VALUES($1,10);', [user]);
163
+
164
+ const workflowID = randomUUID();
165
+ await expect(DBOS.withNextWorkflowID(workflowID, () => regReadWorkflowRunTx(user))).resolves.toEqual({
166
+ user,
167
+ greet_count: 10,
168
+ });
169
+
170
+ const { rows } = await userDB.query('SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1', [
171
+ workflowID,
172
+ ]);
173
+ expect(rows.length).toBe(0);
174
+ });
175
+
176
+ test('static dataSource.register methods', async () => {
177
+ const user = 'staticTest1';
178
+
179
+ await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
180
+
181
+ const workflowID = randomUUID();
182
+ await expect(DBOS.withNextWorkflowID(workflowID, () => regStaticWorkflow(user))).resolves.toEqual([
183
+ { user, greet_count: 1 },
184
+ { user, greet_count: 1 },
185
+ ]);
186
+ });
187
+
188
+ test('instance dataSource.register methods', async () => {
189
+ const user = 'instanceTest1';
190
+
191
+ await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
192
+
193
+ const workflowID = randomUUID();
194
+ await expect(DBOS.withNextWorkflowID(workflowID, () => regInstanceWorkflow(user))).resolves.toEqual([
195
+ { user, greet_count: 1 },
196
+ { user, greet_count: 1 },
197
+ ]);
198
+ });
199
+ });
200
+
201
+ export interface greetings {
202
+ name: string;
203
+ greet_count: number;
204
+ }
205
+
206
+ async function insertFunction(user: string) {
207
+ const rows = await KnexDataSource.client<greetings>('greetings')
208
+ .insert({ name: user, greet_count: 1 })
209
+ .onConflict('name')
210
+ .merge({ greet_count: KnexDataSource.client.raw('greetings.greet_count + 1') })
211
+ .returning('greet_count');
212
+ const row = rows.length > 0 ? rows[0] : undefined;
213
+
214
+ return { user, greet_count: row?.greet_count };
215
+ }
216
+
217
+ async function errorFunction(user: string) {
218
+ const result = await insertFunction(user);
219
+ throw new Error('test error');
220
+ return result;
221
+ }
222
+
223
+ async function readFunction(user: string) {
224
+ const row = await KnexDataSource.client<greetings>('greetings').select('greet_count').where('name', user).first();
225
+ return { user, greet_count: row?.greet_count };
226
+ }
227
+
228
+ const regInsertFunction = dataSource.registerTransaction(insertFunction, 'insertFunction');
229
+ const regErrorFunction = dataSource.registerTransaction(errorFunction, 'errorFunction');
230
+ const regReadFunction = dataSource.registerTransaction(readFunction, 'readFunction', { readOnly: true });
231
+
232
+ class StaticClass {
233
+ static async insertFunction(user: string) {
234
+ return await insertFunction(user);
235
+ }
236
+
237
+ static async readFunction(user: string) {
238
+ return await readFunction(user);
239
+ }
240
+ }
241
+
242
+ StaticClass.insertFunction = dataSource.registerTransaction(StaticClass.insertFunction, 'insertFunction');
243
+ StaticClass.readFunction = dataSource.registerTransaction(StaticClass.readFunction, 'readFunction');
244
+
245
+ class InstanceClass {
246
+ async insertFunction(user: string) {
247
+ return await insertFunction(user);
248
+ }
249
+
250
+ async readFunction(user: string) {
251
+ return await readFunction(user);
252
+ }
253
+ }
254
+
255
+ InstanceClass.prototype.insertFunction = dataSource.registerTransaction(
256
+ // eslint-disable-next-line @typescript-eslint/unbound-method
257
+ InstanceClass.prototype.insertFunction,
258
+ 'insertFunction',
259
+ );
260
+ InstanceClass.prototype.readFunction = dataSource.registerTransaction(
261
+ // eslint-disable-next-line @typescript-eslint/unbound-method
262
+ InstanceClass.prototype.readFunction,
263
+ 'readFunction',
264
+ );
265
+
266
+ async function insertWorkflowReg(user: string) {
267
+ return await regInsertFunction(user);
268
+ }
269
+
270
+ async function insertWorkflowRunTx(user: string) {
271
+ return await dataSource.runTransaction(() => insertFunction(user), 'insertFunction');
272
+ }
273
+
274
+ async function errorWorkflowReg(user: string) {
275
+ return await regErrorFunction(user);
276
+ }
277
+
278
+ async function errorWorkflowRunTx(user: string) {
279
+ return await dataSource.runTransaction(() => errorFunction(user), 'errorFunction');
280
+ }
281
+
282
+ async function readWorkflowReg(user: string) {
283
+ return await regReadFunction(user);
284
+ }
285
+
286
+ async function readWorkflowRunTx(user: string) {
287
+ return await dataSource.runTransaction(() => readFunction(user), 'readFunction', { readOnly: true });
288
+ }
289
+
290
+ async function staticWorkflow(user: string) {
291
+ const result = await StaticClass.insertFunction(user);
292
+ const readResult = await StaticClass.readFunction(user);
293
+ return [result, readResult];
294
+ }
295
+
296
+ async function instanceWorkflow(user: string) {
297
+ const instance = new InstanceClass();
298
+ const result = await instance.insertFunction(user);
299
+ const readResult = await instance.readFunction(user);
300
+ return [result, readResult];
301
+ }
302
+
303
+ const regInsertWorfklowReg = DBOS.registerWorkflow(insertWorkflowReg, 'insertWorkflowReg');
304
+ const regInsertWorfklowRunTx = DBOS.registerWorkflow(insertWorkflowRunTx, 'insertWorkflowRunTx');
305
+ const regErrorWorkflowReg = DBOS.registerWorkflow(errorWorkflowReg, 'errorWorkflowReg');
306
+ const regErrorWorkflowRunTx = DBOS.registerWorkflow(errorWorkflowRunTx, 'errorWorkflowRunTx');
307
+ const regReadWorkflowReg = DBOS.registerWorkflow(readWorkflowReg, 'readWorkflowReg');
308
+ const regReadWorkflowRunTx = DBOS.registerWorkflow(readWorkflowRunTx, 'readWorkflowRunTx');
309
+ const regStaticWorkflow = DBOS.registerWorkflow(staticWorkflow, 'staticWorkflow');
310
+ const regInstanceWorkflow = DBOS.registerWorkflow(instanceWorkflow, 'instanceWorkflow');
@@ -0,0 +1,12 @@
1
+ import { Client } from 'pg';
2
+
3
+ export async function ensureDB(client: Client, name: string) {
4
+ const results = await client.query('SELECT 1 FROM pg_database WHERE datname = $1', [name]);
5
+ if (results.rows.length === 0) {
6
+ await client.query(`CREATE DATABASE ${name}`);
7
+ }
8
+ }
9
+
10
+ export async function dropDB(client: Client, name: string) {
11
+ await client.query(`DROP DATABASE IF EXISTS ${name}`);
12
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ /* Visit https://aka.ms/tsconfig to read more about this file */
2
+ {
3
+ "extends": "../../tsconfig.shared.json",
4
+ "compilerOptions": {
5
+ "outDir": "./dist"
6
+ },
7
+ "include": ["index.ts"],
8
+ "exclude": ["dist", "tests"]
9
+ }