@dbos-inc/postgres-datasource 3.0.7-preview.gfe77addda7 → 3.0.8-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 CHANGED
@@ -21,40 +21,50 @@ import { SuperJSON } from 'superjson';
21
21
  export { IsolationLevel, PostgresTransactionOptions };
22
22
 
23
23
  interface PostgresDataSourceContext {
24
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type
25
- client: postgres.TransactionSql<{}>;
24
+ client: postgres.TransactionSql;
26
25
  }
27
26
 
27
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
28
+ type Options = postgres.Options<{}>;
29
+
28
30
  const asyncLocalCtx = new AsyncLocalStorage<PostgresDataSourceContext>();
29
31
 
30
32
  class PostgresTransactionHandler implements DataSourceTransactionHandler {
31
33
  readonly dsType = 'PostgresDataSource';
32
- readonly #db: Sql;
34
+ #dbField: Sql | undefined;
33
35
 
34
36
  constructor(
35
37
  readonly name: string,
36
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type
37
- options: postgres.Options<{}> = {},
38
- ) {
39
- this.#db = postgres(options);
38
+ private readonly options: Options = {},
39
+ ) {}
40
+
41
+ async initialize(): Promise<void> {
42
+ const db = this.#dbField;
43
+ this.#dbField = postgres(this.options);
44
+ await db?.end();
40
45
  }
41
46
 
42
- initialize(): Promise<void> {
43
- return Promise.resolve();
47
+ async destroy(): Promise<void> {
48
+ const db = this.#dbField;
49
+ this.#dbField = undefined;
50
+ await db?.end();
44
51
  }
45
52
 
46
- destroy(): Promise<void> {
47
- return this.#db.end();
53
+ get #db(): Sql {
54
+ if (!this.#dbField) {
55
+ throw new Error(`DataSource ${this.name} is not initialized.`);
56
+ }
57
+ return this.#dbField;
48
58
  }
49
59
 
50
60
  async #checkExecution(
51
61
  workflowID: string,
52
- functionNum: number,
62
+ stepID: number,
53
63
  ): Promise<{ output: string | null } | { error: string } | undefined> {
54
64
  type Result = { output: string | null; error: string | null };
55
65
  const result = await this.#db<Result[]>/*sql*/ `
56
66
  SELECT output, error FROM dbos.transaction_completion
57
- WHERE workflow_id = ${workflowID} AND function_num = ${functionNum}`;
67
+ WHERE workflow_id = ${workflowID} AND function_num = ${stepID}`;
58
68
  if (result.length === 0) {
59
69
  return undefined;
60
70
  }
@@ -63,16 +73,15 @@ class PostgresTransactionHandler implements DataSourceTransactionHandler {
63
73
  }
64
74
 
65
75
  static async #recordOutput(
66
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type
67
- client: postgres.TransactionSql<{}>,
76
+ client: postgres.TransactionSql,
68
77
  workflowID: string,
69
- functionNum: number,
78
+ stepID: number,
70
79
  output: string | null,
71
80
  ): Promise<void> {
72
81
  try {
73
82
  await client/*sql*/ `
74
83
  INSERT INTO dbos.transaction_completion (workflow_id, function_num, output)
75
- VALUES (${workflowID}, ${functionNum}, ${output})`;
84
+ VALUES (${workflowID}, ${stepID}, ${output})`;
76
85
  } catch (error) {
77
86
  if (isPGKeyConflictError(error)) {
78
87
  throw new DBOSWorkflowConflictError(workflowID);
@@ -82,11 +91,11 @@ class PostgresTransactionHandler implements DataSourceTransactionHandler {
82
91
  }
83
92
  }
84
93
 
85
- async #recordError(workflowID: string, functionNum: number, error: string): Promise<void> {
94
+ async #recordError(workflowID: string, stepID: number, error: string): Promise<void> {
86
95
  try {
87
96
  await this.#db/*sql*/ `
88
97
  INSERT INTO dbos.transaction_completion (workflow_id, function_num, error)
89
- VALUES (${workflowID}, ${functionNum}, ${error})`;
98
+ VALUES (${workflowID}, ${stepID}, ${error})`;
90
99
  } catch (error) {
91
100
  if (isPGKeyConflictError(error)) {
92
101
  throw new DBOSWorkflowConflictError(workflowID);
@@ -103,26 +112,24 @@ class PostgresTransactionHandler implements DataSourceTransactionHandler {
103
112
  ...args: Args
104
113
  ): Promise<Return> {
105
114
  const workflowID = DBOS.workflowID;
106
- if (workflowID === undefined) {
107
- throw new Error('Workflow ID is not set.');
108
- }
109
- const functionNum = DBOS.stepID;
110
- if (functionNum === undefined) {
111
- throw new Error('Function Number is not set.');
115
+ const stepID = DBOS.stepID;
116
+ if (workflowID !== undefined && stepID === undefined) {
117
+ throw new Error('DBOS.stepID is undefined inside a workflow.');
112
118
  }
113
119
 
114
120
  const isolationLevel = config?.isolationLevel ? `ISOLATION LEVEL ${config.isolationLevel}` : '';
115
121
  const readOnly = config?.readOnly ?? false;
116
122
  const accessMode = config?.readOnly === undefined ? '' : readOnly ? 'READ ONLY' : 'READ WRITE';
117
- const saveResults = !readOnly && workflowID;
123
+ const saveResults = !readOnly && workflowID !== undefined;
118
124
 
125
+ // Retry loop if appropriate
119
126
  let retryWaitMS = 1;
120
127
  const backoffFactor = 1.5;
121
- const maxRetryWaitMS = 2000;
128
+ const maxRetryWaitMS = 2000; // Maximum wait 2 seconds.
122
129
 
123
130
  while (true) {
124
131
  // Check to see if this tx has already been executed
125
- const previousResult = saveResults ? await this.#checkExecution(workflowID, functionNum) : undefined;
132
+ const previousResult = saveResults ? await this.#checkExecution(workflowID, stepID!) : undefined;
126
133
  if (previousResult) {
127
134
  DBOS.span?.setAttribute('cached', true);
128
135
 
@@ -141,12 +148,7 @@ class PostgresTransactionHandler implements DataSourceTransactionHandler {
141
148
 
142
149
  // save the output of read/write transactions
143
150
  if (saveResults) {
144
- await PostgresTransactionHandler.#recordOutput(
145
- client,
146
- workflowID,
147
- functionNum,
148
- SuperJSON.stringify(result),
149
- );
151
+ await PostgresTransactionHandler.#recordOutput(client, workflowID, stepID!, SuperJSON.stringify(result));
150
152
  }
151
153
 
152
154
  return result;
@@ -162,7 +164,7 @@ class PostgresTransactionHandler implements DataSourceTransactionHandler {
162
164
  } else {
163
165
  if (saveResults) {
164
166
  const message = SuperJSON.stringify(error);
165
- await this.#recordError(workflowID, functionNum, message);
167
+ await this.#recordError(workflowID, stepID!, message);
166
168
  }
167
169
 
168
170
  throw error;
@@ -173,20 +175,18 @@ class PostgresTransactionHandler implements DataSourceTransactionHandler {
173
175
  }
174
176
 
175
177
  export class PostgresDataSource implements DBOSDataSource<PostgresTransactionOptions> {
176
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type
177
- static get client(): postgres.TransactionSql<{}> {
178
+ static get client(): postgres.TransactionSql {
178
179
  if (!DBOS.isInTransaction()) {
179
180
  throw new Error('invalid use of PostgresDataSource.client outside of a DBOS transaction.');
180
181
  }
181
182
  const ctx = asyncLocalCtx.getStore();
182
183
  if (!ctx) {
183
- throw new Error('No async local context found.');
184
+ throw new Error('invalid use of PostgresDataSource.client outside of a DBOS transaction.');
184
185
  }
185
186
  return ctx.client;
186
187
  }
187
188
 
188
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type
189
- static async initializeInternalSchema(options: postgres.Options<{}> = {}): Promise<void> {
189
+ static async initializeInternalSchema(options: Options = {}): Promise<void> {
190
190
  const pg = postgres({ ...options, onnotice: () => {} });
191
191
  try {
192
192
  await pg.unsafe(createTransactionCompletionSchemaPG);
@@ -196,12 +196,12 @@ export class PostgresDataSource implements DBOSDataSource<PostgresTransactionOpt
196
196
  }
197
197
  }
198
198
 
199
- readonly name: string;
200
199
  #provider: PostgresTransactionHandler;
201
200
 
202
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type
203
- constructor(name: string, options: postgres.Options<{}> = {}) {
204
- this.name = name;
201
+ constructor(
202
+ readonly name: string,
203
+ options: Options = {},
204
+ ) {
205
205
  this.#provider = new PostgresTransactionHandler(name, options);
206
206
  registerDataSource(this.#provider);
207
207
  }
@@ -212,10 +212,10 @@ export class PostgresDataSource implements DBOSDataSource<PostgresTransactionOpt
212
212
 
213
213
  registerTransaction<This, Args extends unknown[], Return>(
214
214
  func: (this: This, ...args: Args) => Promise<Return>,
215
- name: string,
216
215
  config?: PostgresTransactionOptions,
216
+ name?: string,
217
217
  ): (this: This, ...args: Args) => Promise<Return> {
218
- return registerTransaction(this.name, func, { name }, config);
218
+ return registerTransaction(this.name, func, { name: name ?? func.name }, config);
219
219
  }
220
220
 
221
221
  transaction(config?: PostgresTransactionOptions) {
@@ -223,14 +223,14 @@ export class PostgresDataSource implements DBOSDataSource<PostgresTransactionOpt
223
223
  const ds = this;
224
224
  return function decorator<This, Args extends unknown[], Return>(
225
225
  _target: object,
226
- propertyKey: string,
226
+ propertyKey: PropertyKey,
227
227
  descriptor: TypedPropertyDescriptor<(this: This, ...args: Args) => Promise<Return>>,
228
228
  ) {
229
229
  if (!descriptor.value) {
230
230
  throw Error('Use of decorator when original method is undefined');
231
231
  }
232
232
 
233
- descriptor.value = ds.registerTransaction(descriptor.value, propertyKey.toString(), config);
233
+ descriptor.value = ds.registerTransaction(descriptor.value, config, String(propertyKey));
234
234
 
235
235
  return descriptor;
236
236
  };
package/package.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "@dbos-inc/postgres-datasource",
3
- "version": "3.0.7-preview.gfe77addda7",
4
- "description": "",
3
+ "version": "3.0.8-preview",
4
+ "description": "DBOS DataSource library for Postgres database client",
5
5
  "license": "MIT",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "homepage": "https://docs.dbos.dev/",
6
9
  "repository": {
7
10
  "type": "git",
8
11
  "url": "https://github.com/dbos-inc/dbos-transact-ts",
9
- "directory": "packages/knex-datasource"
12
+ "directory": "packages/postgres-datasource"
10
13
  },
11
- "homepage": "https://docs.dbos.dev/",
12
- "main": "index.js",
13
14
  "scripts": {
14
15
  "build": "tsc --project tsconfig.json",
15
16
  "test": "jest --detectOpenHandles"
@@ -44,13 +44,19 @@ describe('PostgresDataSource', () => {
44
44
  }
45
45
 
46
46
  await PostgresDataSource.initializeInternalSchema(config);
47
+ });
48
+
49
+ afterAll(async () => {
50
+ await userDB.end();
51
+ });
52
+
53
+ beforeEach(async () => {
47
54
  DBOS.setConfig({ name: 'pg-ds-test' });
48
55
  await DBOS.launch();
49
56
  });
50
57
 
51
- afterAll(async () => {
58
+ afterEach(async () => {
52
59
  await DBOS.shutdown();
53
- await userDB.end();
54
60
  });
55
61
 
56
62
  test('insert dataSource.register function', async () => {
@@ -59,7 +65,7 @@ describe('PostgresDataSource', () => {
59
65
  await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
60
66
  const workflowID = randomUUID();
61
67
 
62
- await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorfklowReg(user))).resolves.toMatchObject({
68
+ await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowReg(user))).resolves.toMatchObject({
63
69
  user,
64
70
  greet_count: 1,
65
71
  });
@@ -81,10 +87,10 @@ describe('PostgresDataSource', () => {
81
87
  await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
82
88
  const workflowID = randomUUID();
83
89
 
84
- const result = await DBOS.withNextWorkflowID(workflowID, () => regInsertWorfklowReg(user));
90
+ const result = await DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowReg(user));
85
91
  expect(result).toMatchObject({ user, greet_count: 1 });
86
92
 
87
- await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorfklowReg(user))).resolves.toMatchObject(result);
93
+ await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowReg(user))).resolves.toMatchObject(result);
88
94
  });
89
95
 
90
96
  test('insert dataSource.runAsTx function', async () => {
@@ -93,7 +99,7 @@ describe('PostgresDataSource', () => {
93
99
  await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
94
100
  const workflowID = randomUUID();
95
101
 
96
- await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorfklowRunTx(user))).resolves.toMatchObject({
102
+ await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowRunTx(user))).resolves.toMatchObject({
97
103
  user,
98
104
  greet_count: 1,
99
105
  });
@@ -115,10 +121,10 @@ describe('PostgresDataSource', () => {
115
121
  await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
116
122
  const workflowID = randomUUID();
117
123
 
118
- const result = await DBOS.withNextWorkflowID(workflowID, () => regInsertWorfklowRunTx(user));
124
+ const result = await DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowRunTx(user));
119
125
  expect(result).toMatchObject({ user, greet_count: 1 });
120
126
 
121
- await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorfklowRunTx(user))).resolves.toMatchObject(
127
+ await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowRunTx(user))).resolves.toMatchObject(
122
128
  result,
123
129
  );
124
130
  });
@@ -281,6 +287,40 @@ describe('PostgresDataSource', () => {
281
287
  { user, greet_count: 1 },
282
288
  ]);
283
289
  });
290
+
291
+ test('invoke-reg-tx-fun-outside-wf', async () => {
292
+ const user = 'outsideWfUser' + Date.now();
293
+ const result = await regInsertFunction(user);
294
+ expect(result).toMatchObject({ user, greet_count: 1 });
295
+
296
+ const txResults = await userDB.query('SELECT * FROM dbos.transaction_completion WHERE output LIKE $1', [
297
+ `%${user}%`,
298
+ ]);
299
+ expect(txResults.rows.length).toBe(0);
300
+ });
301
+
302
+ test('invoke-reg-tx-static-method-outside-wf', async () => {
303
+ const user = 'outsideWfUser' + Date.now();
304
+ const result = await StaticClass.insertFunction(user);
305
+ expect(result).toMatchObject({ user, greet_count: 1 });
306
+
307
+ const txResults = await userDB.query('SELECT * FROM dbos.transaction_completion WHERE output LIKE $1', [
308
+ `%${user}%`,
309
+ ]);
310
+ expect(txResults.rows.length).toBe(0);
311
+ });
312
+
313
+ test('invoke-reg-tx-inst-method-outside-wf', async () => {
314
+ const user = 'outsideWfUser' + Date.now();
315
+ const instance = new InstanceClass();
316
+ const result = await instance.insertFunction(user);
317
+ expect(result).toMatchObject({ user, greet_count: 1 });
318
+
319
+ const txResults = await userDB.query('SELECT * FROM dbos.transaction_completion WHERE output LIKE $1', [
320
+ `%${user}%`,
321
+ ]);
322
+ expect(txResults.rows.length).toBe(0);
323
+ });
284
324
  });
285
325
 
286
326
  export interface greetings {
@@ -300,9 +340,8 @@ async function insertFunction(user: string) {
300
340
  }
301
341
 
302
342
  async function errorFunction(user: string) {
303
- const result = await insertFunction(user);
343
+ const _result = await insertFunction(user);
304
344
  throw new Error(`test error ${Date.now()}`);
305
- return result;
306
345
  }
307
346
 
308
347
  async function readFunction(user: string) {
@@ -314,9 +353,9 @@ async function readFunction(user: string) {
314
353
  return { user, greet_count: row?.greet_count, now: Date.now() };
315
354
  }
316
355
 
317
- const regInsertFunction = dataSource.registerTransaction(insertFunction, 'insertFunction');
318
- const regErrorFunction = dataSource.registerTransaction(errorFunction, 'errorFunction');
319
- const regReadFunction = dataSource.registerTransaction(readFunction, 'readFunction', { readOnly: true });
356
+ const regInsertFunction = dataSource.registerTransaction(insertFunction);
357
+ const regErrorFunction = dataSource.registerTransaction(errorFunction);
358
+ const regReadFunction = dataSource.registerTransaction(readFunction, { readOnly: true });
320
359
 
321
360
  class StaticClass {
322
361
  static async insertFunction(user: string) {
@@ -328,8 +367,8 @@ class StaticClass {
328
367
  }
329
368
  }
330
369
 
331
- StaticClass.insertFunction = dataSource.registerTransaction(StaticClass.insertFunction, 'insertFunction');
332
- StaticClass.readFunction = dataSource.registerTransaction(StaticClass.readFunction, 'readFunction');
370
+ StaticClass.insertFunction = dataSource.registerTransaction(StaticClass.insertFunction);
371
+ StaticClass.readFunction = dataSource.registerTransaction(StaticClass.readFunction, { readOnly: true });
333
372
 
334
373
  class InstanceClass {
335
374
  async insertFunction(user: string) {
@@ -344,12 +383,11 @@ class InstanceClass {
344
383
  InstanceClass.prototype.insertFunction = dataSource.registerTransaction(
345
384
  // eslint-disable-next-line @typescript-eslint/unbound-method
346
385
  InstanceClass.prototype.insertFunction,
347
- 'insertFunction',
348
386
  );
349
387
  InstanceClass.prototype.readFunction = dataSource.registerTransaction(
350
388
  // eslint-disable-next-line @typescript-eslint/unbound-method
351
389
  InstanceClass.prototype.readFunction,
352
- 'readFunction',
390
+ { readOnly: true },
353
391
  );
354
392
 
355
393
  async function insertWorkflowReg(user: string) {
@@ -389,8 +427,8 @@ async function instanceWorkflow(user: string) {
389
427
  return [result, readResult];
390
428
  }
391
429
 
392
- const regInsertWorfklowReg = DBOS.registerWorkflow(insertWorkflowReg, 'insertWorkflowReg');
393
- const regInsertWorfklowRunTx = DBOS.registerWorkflow(insertWorkflowRunTx, 'insertWorkflowRunTx');
430
+ const regInsertWorkflowReg = DBOS.registerWorkflow(insertWorkflowReg, 'insertWorkflowReg');
431
+ const regInsertWorkflowRunTx = DBOS.registerWorkflow(insertWorkflowRunTx, 'insertWorkflowRunTx');
394
432
  const regErrorWorkflowReg = DBOS.registerWorkflow(errorWorkflowReg, 'errorWorkflowReg');
395
433
  const regErrorWorkflowRunTx = DBOS.registerWorkflow(errorWorkflowRunTx, 'errorWorkflowRunTx');
396
434
  const regReadWorkflowReg = DBOS.registerWorkflow(readWorkflowReg, 'readWorkflowReg');