@dbos-inc/knex-datasource 3.0.8-preview → 3.0.8-preview.g493d2d1c2b

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
@@ -9,8 +9,6 @@ import {
9
9
  runTransaction,
10
10
  DBOSDataSource,
11
11
  registerDataSource,
12
- createTransactionCompletionSchemaPG,
13
- createTransactionCompletionTablePG,
14
12
  } from '@dbos-inc/dbos-sdk/datasource';
15
13
  import { AsyncLocalStorage } from 'async_hooks';
16
14
  import knex, { type Knex } from 'knex';
@@ -20,7 +18,6 @@ interface transaction_completion {
20
18
  workflow_id: string;
21
19
  function_num: number;
22
20
  output: string | null;
23
- error: string | null;
24
21
  }
25
22
 
26
23
  interface KnexDataSourceContext {
@@ -31,79 +28,50 @@ export type TransactionConfig = Pick<Knex.TransactionConfig, 'isolationLevel' |
31
28
 
32
29
  const asyncLocalCtx = new AsyncLocalStorage<KnexDataSourceContext>();
33
30
 
34
- class KnexTransactionHandler implements DataSourceTransactionHandler {
31
+ class KnexDSTH implements DataSourceTransactionHandler {
32
+ readonly name: string;
35
33
  readonly dsType = 'KnexDataSource';
36
- #knexDBField: Knex | undefined;
34
+ readonly #knexDB: Knex;
37
35
 
38
- constructor(
39
- readonly name: string,
40
- private readonly config: Knex.Config,
41
- ) {}
42
-
43
- async initialize(): Promise<void> {
44
- const knexDB = this.#knexDBField;
45
- this.#knexDBField = knex(this.config);
46
- await knexDB?.destroy();
36
+ constructor(name: string, config: Knex.Config) {
37
+ this.name = name;
38
+ this.#knexDB = knex(config);
47
39
  }
48
40
 
49
- async destroy(): Promise<void> {
50
- const knexDB = this.#knexDBField;
51
- this.#knexDBField = undefined;
52
- await knexDB?.destroy();
41
+ initialize(): Promise<void> {
42
+ return Promise.resolve();
53
43
  }
54
44
 
55
- get #knexDB() {
56
- if (!this.#knexDBField) {
57
- throw new Error(`DataSource ${this.name} is not initialized.`);
58
- }
59
- return this.#knexDBField;
45
+ destroy(): Promise<void> {
46
+ return this.#knexDB.destroy();
60
47
  }
61
48
 
62
- async #checkExecution(
49
+ static async #checkExecution(
50
+ client: Knex.Transaction,
63
51
  workflowID: string,
64
- stepID: number,
65
- ): Promise<{ output: string | null } | { error: string } | undefined> {
66
- const result = await this.#knexDB<transaction_completion>('transaction_completion')
52
+ functionNum: number,
53
+ ): Promise<{ output: string | null } | undefined> {
54
+ const result = await client<transaction_completion>('transaction_completion')
67
55
  .withSchema('dbos')
68
- .select('output', 'error')
56
+ .select('output')
69
57
  .where({
70
58
  workflow_id: workflowID,
71
- function_num: stepID,
59
+ function_num: functionNum,
72
60
  })
73
61
  .first();
74
- if (result === undefined) {
75
- return undefined;
76
- }
77
- const { output, error } = result;
78
- return error !== null ? { error } : { output };
79
- }
80
-
81
- async #recordError(workflowID: string, stepID: number, error: string): Promise<void> {
82
- try {
83
- await this.#knexDB<transaction_completion>('transaction_completion').withSchema('dbos').insert({
84
- workflow_id: workflowID,
85
- function_num: stepID,
86
- error,
87
- });
88
- } catch (error) {
89
- if (isPGKeyConflictError(error)) {
90
- throw new DBOSWorkflowConflictError(workflowID);
91
- } else {
92
- throw error;
93
- }
94
- }
62
+ return result === undefined ? undefined : { output: result.output };
95
63
  }
96
64
 
97
65
  static async #recordOutput(
98
66
  client: Knex.Transaction,
99
67
  workflowID: string,
100
- stepID: number,
68
+ functionNum: number,
101
69
  output: string | null,
102
70
  ): Promise<void> {
103
71
  try {
104
72
  await client<transaction_completion>('transaction_completion').withSchema('dbos').insert({
105
73
  workflow_id: workflowID,
106
- function_num: stepID,
74
+ function_num: functionNum,
107
75
  output,
108
76
  });
109
77
  } catch (error) {
@@ -122,61 +90,67 @@ class KnexTransactionHandler implements DataSourceTransactionHandler {
122
90
  ...args: Args
123
91
  ): Promise<Return> {
124
92
  const workflowID = DBOS.workflowID;
125
- const stepID = DBOS.stepID;
126
- if (workflowID !== undefined && stepID === undefined) {
127
- throw new Error('DBOS.stepID is undefined inside a workflow.');
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.');
128
99
  }
129
100
 
130
101
  const readOnly = config?.readOnly ?? false;
131
- const saveResults = !readOnly && workflowID !== undefined;
132
-
133
- // Retry loop if appropriate
134
102
  let retryWaitMS = 1;
135
103
  const backoffFactor = 1.5;
136
- const maxRetryWaitMS = 2000; // Maximum wait 2 seconds.
104
+ const maxRetryWaitMS = 2000;
137
105
 
138
106
  while (true) {
139
- // Check to see if this tx has already been executed
140
- const previousResult = saveResults ? await this.#checkExecution(workflowID, stepID!) : undefined;
141
- if (previousResult) {
142
- DBOS.span?.setAttribute('cached', true);
143
-
144
- if ('error' in previousResult) {
145
- throw SuperJSON.parse(previousResult.error);
146
- }
147
- return (previousResult.output ? SuperJSON.parse(previousResult.output) : null) as Return;
148
- }
149
-
150
107
  try {
151
108
  const result = await this.#knexDB.transaction<Return>(
152
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
+
153
117
  // execute user's transaction function
154
118
  const result = await asyncLocalCtx.run({ client }, async () => {
155
119
  return (await func.call(target, ...args)) as Return;
156
120
  });
157
121
 
158
122
  // save the output of read/write transactions
159
- if (saveResults) {
160
- await KnexTransactionHandler.#recordOutput(client, workflowID, stepID!, SuperJSON.stringify(result));
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
161
130
  }
162
131
 
163
132
  return result;
164
133
  },
165
134
  { isolationLevel: config?.isolationLevel, readOnly: config?.readOnly },
166
135
  );
136
+ // TODO: span.setStatus({ code: SpanStatusCode.OK });
137
+ // TODO: this.tracer.endSpan(span);
167
138
 
168
139
  return result;
169
140
  } catch (error) {
170
141
  if (isPGRetriableTransactionError(error)) {
171
- DBOS.span?.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMS }, performance.now());
142
+ // TODO: span.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMillis }, performance.now());
172
143
  await new Promise((resolve) => setTimeout(resolve, retryWaitMS));
173
144
  retryWaitMS = Math.min(retryWaitMS * backoffFactor, maxRetryWaitMS);
174
145
  continue;
175
146
  } else {
176
- if (saveResults) {
177
- const message = SuperJSON.stringify(error);
178
- await this.#recordError(workflowID, stepID!, message);
179
- }
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.
180
154
 
181
155
  throw error;
182
156
  }
@@ -192,7 +166,7 @@ export class KnexDataSource implements DBOSDataSource<TransactionConfig> {
192
166
  }
193
167
  const ctx = asyncLocalCtx.getStore();
194
168
  if (!ctx) {
195
- throw new Error('invalid use of KnexDataSource.client outside of a DBOS transaction.');
169
+ throw new Error('No async local context found.');
196
170
  }
197
171
  return ctx.client;
198
172
  }
@@ -200,20 +174,27 @@ export class KnexDataSource implements DBOSDataSource<TransactionConfig> {
200
174
  static async initializeInternalSchema(config: Knex.Config) {
201
175
  const knexDB = knex(config);
202
176
  try {
203
- await knexDB.raw(createTransactionCompletionSchemaPG);
204
- await knexDB.raw(createTransactionCompletionTablePG);
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
+ }
205
187
  } finally {
206
188
  await knexDB.destroy();
207
189
  }
208
190
  }
209
191
 
210
- #provider: KnexTransactionHandler;
192
+ readonly name: string;
193
+ #provider: KnexDSTH;
211
194
 
212
- constructor(
213
- readonly name: string,
214
- config: Knex.Config,
215
- ) {
216
- this.#provider = new KnexTransactionHandler(name, config);
195
+ constructor(name: string, config: Knex.Config) {
196
+ this.name = name;
197
+ this.#provider = new KnexDSTH(name, config);
217
198
  registerDataSource(this.#provider);
218
199
  }
219
200
 
@@ -223,10 +204,10 @@ export class KnexDataSource implements DBOSDataSource<TransactionConfig> {
223
204
 
224
205
  registerTransaction<This, Args extends unknown[], Return>(
225
206
  func: (this: This, ...args: Args) => Promise<Return>,
207
+ name: string,
226
208
  config?: TransactionConfig,
227
- name?: string,
228
209
  ): (this: This, ...args: Args) => Promise<Return> {
229
- return registerTransaction(this.name, func, { name: name ?? func.name }, config);
210
+ return registerTransaction(this.name, func, { name }, config);
230
211
  }
231
212
 
232
213
  transaction(config?: TransactionConfig) {
@@ -234,14 +215,14 @@ export class KnexDataSource implements DBOSDataSource<TransactionConfig> {
234
215
  const ds = this;
235
216
  return function decorator<This, Args extends unknown[], Return>(
236
217
  _target: object,
237
- propertyKey: PropertyKey,
218
+ propertyKey: string,
238
219
  descriptor: TypedPropertyDescriptor<(this: This, ...args: Args) => Promise<Return>>,
239
220
  ) {
240
221
  if (!descriptor.value) {
241
222
  throw Error('Use of decorator when original method is undefined');
242
223
  }
243
224
 
244
- descriptor.value = ds.registerTransaction(descriptor.value, config, String(propertyKey));
225
+ descriptor.value = ds.registerTransaction(descriptor.value, propertyKey.toString(), config);
245
226
 
246
227
  return descriptor;
247
228
  };
package/package.json CHANGED
@@ -1,23 +1,21 @@
1
1
  {
2
2
  "name": "@dbos-inc/knex-datasource",
3
- "version": "3.0.8-preview",
4
- "description": "DBOS DataSource library for Knex ORM with PostgreSQL support",
3
+ "version": "3.0.8-preview.g493d2d1c2b",
4
+ "description": "",
5
5
  "license": "MIT",
6
- "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
8
- "homepage": "https://docs.dbos.dev/",
9
6
  "repository": {
10
7
  "type": "git",
11
8
  "url": "https://github.com/dbos-inc/dbos-transact-ts",
12
9
  "directory": "packages/knex-datasource"
13
10
  },
11
+ "homepage": "https://docs.dbos.dev/",
12
+ "main": "index.js",
14
13
  "scripts": {
15
14
  "build": "tsc --project tsconfig.json",
16
15
  "test": "jest --detectOpenHandles"
17
16
  },
18
17
  "dependencies": {
19
18
  "knex": "^3.1.0",
20
- "pg": "^8.11.3",
21
19
  "superjson": "^1.13"
22
20
  },
23
21
  "peerDependencies": {
@@ -27,6 +25,7 @@
27
25
  "@types/jest": "^29.5.14",
28
26
  "@types/pg": "^8.15.2",
29
27
  "jest": "^29.7.0",
28
+ "pg": "^8.11.3",
30
29
  "typescript": "^5.4.5"
31
30
  }
32
31
  }
@@ -9,7 +9,7 @@ describe('KnexDataSource.configure', () => {
9
9
  const client = new Client({ ...config, database: 'postgres' });
10
10
  try {
11
11
  await client.connect();
12
- await dropDB(client, config.database, true);
12
+ await dropDB(client, config.database);
13
13
  await ensureDB(client, config.database);
14
14
  } finally {
15
15
  await client.end();