@dbos-inc/typeorm-datasource 3.0.11-preview.gc9233b8190 → 3.0.16-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.
@@ -0,0 +1,37 @@
1
+ import { PoolConfig } from 'pg';
2
+ import { DBOSDataSource, PGTransactionConfig } from '@dbos-inc/dbos-sdk/datasource';
3
+ import { EntityManager } from 'typeorm';
4
+ export interface TypeORMTransactionConfig extends PGTransactionConfig {
5
+ name?: string;
6
+ }
7
+ export declare class TypeOrmDataSource implements DBOSDataSource<TypeORMTransactionConfig> {
8
+ #private;
9
+ readonly name: string;
10
+ static get entityManager(): EntityManager;
11
+ get entityManager(): EntityManager;
12
+ static initializeDBOSSchema(config: PoolConfig): Promise<void>;
13
+ constructor(name: string, config: PoolConfig, entities: Function[]);
14
+ /**
15
+ * Run `func` as a transaction against this DataSource
16
+ * @param func Function to run within a transactional context
17
+ * @param funcName Name to record for the transaction
18
+ * @param config Transaction configuration (isolation, etc)
19
+ * @returns Return value from `func`
20
+ */
21
+ runTransaction<T>(func: () => Promise<T>, config?: TypeORMTransactionConfig): Promise<T>;
22
+ /**
23
+ * Register function as DBOS transaction, to be called within the context
24
+ * of a transaction on this data source.
25
+ *
26
+ * @param func Function to wrap
27
+ * @param target Name of function
28
+ * @param config Transaction settings
29
+ * @returns Wrapped function, to be called instead of `func`
30
+ */
31
+ registerTransaction<This, Args extends unknown[], Return>(func: (this: This, ...args: Args) => Promise<Return>, config?: TypeORMTransactionConfig): (this: This, ...args: Args) => Promise<Return>;
32
+ /**
33
+ * Decorator establishing function as a transaction
34
+ */
35
+ transaction(config?: TypeORMTransactionConfig): <This, Args extends unknown[], Return>(_target: object, propertyKey: PropertyKey, descriptor: TypedPropertyDescriptor<(this: This, ...args: Args) => Promise<Return>>) => TypedPropertyDescriptor<(this: This, ...args: Args) => Promise<Return>>;
36
+ }
37
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAEhC,OAAO,EAQL,cAAc,EAEd,mBAAmB,EACpB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAc,aAAa,EAAE,MAAM,SAAS,CAAC;AAIpD,MAAM,WAAW,wBAAyB,SAAQ,mBAAmB;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AA8MD,qBAAa,iBAAkB,YAAW,cAAc,CAAC,wBAAwB,CAAC;;IAsC9E,QAAQ,CAAC,IAAI,EAAE,MAAM;IArBvB,MAAM,KAAK,aAAa,kBAEvB;IAED,IAAI,aAAa,kBAEhB;WAEY,oBAAoB,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;gBAazD,IAAI,EAAE,MAAM,EACrB,MAAM,EAAE,UAAU,EAElB,QAAQ,EAAE,QAAQ,EAAE;IAMtB;;;;;;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,GAChC,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC;IAIjD;;OAEG;IACH,WAAW,CAAC,MAAM,CAAC,EAAE,wBAAwB,mDAIhC,MAAM,eACF,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;CAcvF"}
package/dist/index.js ADDED
@@ -0,0 +1,238 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TypeOrmDataSource = void 0;
4
+ const dbos_sdk_1 = require("@dbos-inc/dbos-sdk");
5
+ const datasource_1 = require("@dbos-inc/dbos-sdk/datasource");
6
+ const typeorm_1 = require("typeorm");
7
+ const async_hooks_1 = require("async_hooks");
8
+ const superjson_1 = require("superjson");
9
+ const asyncLocalCtx = new async_hooks_1.AsyncLocalStorage();
10
+ class TypeOrmTransactionHandler {
11
+ name;
12
+ config;
13
+ entities;
14
+ dsType = 'TypeOrm';
15
+ #dataSourceField;
16
+ constructor(name, config,
17
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
18
+ entities) {
19
+ this.name = name;
20
+ this.config = config;
21
+ this.entities = entities;
22
+ }
23
+ static async createInstance(config,
24
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
25
+ entities) {
26
+ const ds = new typeorm_1.DataSource({
27
+ type: 'postgres',
28
+ entities: entities,
29
+ url: config.connectionString,
30
+ host: config.host,
31
+ port: config.port,
32
+ username: config.user,
33
+ // password: config.password,
34
+ database: config.database,
35
+ // ssl: config.ssl,
36
+ connectTimeoutMS: config.connectionTimeoutMillis,
37
+ poolSize: config.max,
38
+ });
39
+ await ds.initialize();
40
+ return ds;
41
+ }
42
+ async initialize() {
43
+ const ds = this.#dataSourceField;
44
+ this.#dataSourceField = await TypeOrmTransactionHandler.createInstance(this.config, this.entities);
45
+ await ds?.destroy();
46
+ }
47
+ async destroy() {
48
+ const ds = this.#dataSourceField;
49
+ this.#dataSourceField = undefined;
50
+ await ds?.destroy();
51
+ }
52
+ get #dataSource() {
53
+ if (!this.#dataSourceField) {
54
+ throw new Error(`DataSource ${this.name} is not initialized.`);
55
+ }
56
+ return this.#dataSourceField;
57
+ }
58
+ async #checkExecution(workflowID, stepID) {
59
+ const rows = await this.#dataSource.query(`SELECT output, error FROM dbos.transaction_completion
60
+ WHERE workflow_id=$1 AND function_num=$2;`, [workflowID, stepID]);
61
+ if (rows.length !== 1) {
62
+ return undefined;
63
+ }
64
+ if (rows[0].output === null) {
65
+ return undefined;
66
+ }
67
+ const { output, error } = rows[0];
68
+ return error !== null ? { error } : { output };
69
+ }
70
+ static async #recordOutput(entityManager, workflowID, stepID, output) {
71
+ try {
72
+ await entityManager.query(`INSERT INTO dbos.transaction_completion (workflow_id, function_num, output)
73
+ VALUES ($1, $2, $3)`, [workflowID, stepID, output]);
74
+ }
75
+ catch (error) {
76
+ if ((0, datasource_1.isPGKeyConflictError)(error)) {
77
+ throw new dbos_sdk_1.DBOSWorkflowConflictError(workflowID);
78
+ }
79
+ else {
80
+ throw error;
81
+ }
82
+ }
83
+ }
84
+ async #recordError(workflowID, stepID, error) {
85
+ try {
86
+ await this.#dataSource.query(`INSERT INTO dbos.transaction_completion (workflow_id, function_num, error)
87
+ VALUES ($1, $2, $3)`, [workflowID, stepID, error]);
88
+ }
89
+ catch (error) {
90
+ if ((0, datasource_1.isPGKeyConflictError)(error)) {
91
+ throw new dbos_sdk_1.DBOSWorkflowConflictError(workflowID);
92
+ }
93
+ else {
94
+ throw error;
95
+ }
96
+ }
97
+ }
98
+ /* Required by base class */
99
+ async invokeTransactionFunction(config, target, func, ...args) {
100
+ const workflowID = dbos_sdk_1.DBOS.workflowID;
101
+ const stepID = dbos_sdk_1.DBOS.stepID;
102
+ if (workflowID !== undefined && stepID === undefined) {
103
+ throw new Error('DBOS.stepID is undefined inside a workflow.');
104
+ }
105
+ const isolationLevel = config?.isolationLevel ?? 'READ COMMITTED';
106
+ const readOnly = config?.readOnly ? true : false;
107
+ const saveResults = !readOnly && workflowID !== undefined;
108
+ // Retry loop if appropriate
109
+ let retryWaitMS = 1;
110
+ const backoffFactor = 1.5;
111
+ const maxRetryWaitMS = 2000; // Maximum wait 2 seconds.
112
+ while (true) {
113
+ // Check to see if this tx has already been executed
114
+ const previousResult = saveResults ? await this.#checkExecution(workflowID, stepID) : undefined;
115
+ if (previousResult) {
116
+ dbos_sdk_1.DBOS.span?.setAttribute('cached', true);
117
+ if ('error' in previousResult) {
118
+ throw superjson_1.SuperJSON.parse(previousResult.error);
119
+ }
120
+ return (previousResult.output ? superjson_1.SuperJSON.parse(previousResult.output) : null);
121
+ }
122
+ try {
123
+ const result = await this.#dataSource.transaction(isolationLevel, async (entityManager) => {
124
+ if (readOnly) {
125
+ await entityManager.query('SET TRANSACTION READ ONLY');
126
+ }
127
+ const result = await asyncLocalCtx.run({ entityManager, owner: this }, async () => {
128
+ return await func.call(target, ...args);
129
+ });
130
+ // save the output of read/write transactions
131
+ if (saveResults) {
132
+ await TypeOrmTransactionHandler.#recordOutput(entityManager, workflowID, stepID, superjson_1.SuperJSON.stringify(result));
133
+ }
134
+ return result;
135
+ });
136
+ return result;
137
+ }
138
+ catch (error) {
139
+ if ((0, datasource_1.isPGRetriableTransactionError)(error)) {
140
+ dbos_sdk_1.DBOS.span?.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMS }, performance.now());
141
+ // Retry serialization failures.
142
+ await new Promise((resolve) => setTimeout(resolve, retryWaitMS));
143
+ retryWaitMS = Math.min(retryWaitMS * backoffFactor, maxRetryWaitMS);
144
+ continue;
145
+ }
146
+ else {
147
+ if (saveResults) {
148
+ const message = superjson_1.SuperJSON.stringify(error);
149
+ await this.#recordError(workflowID, stepID, message);
150
+ }
151
+ throw error;
152
+ }
153
+ }
154
+ }
155
+ }
156
+ }
157
+ class TypeOrmDataSource {
158
+ name;
159
+ // User calls this... DBOS not directly involved...
160
+ static #getEntityManager(p) {
161
+ if (!dbos_sdk_1.DBOS.isInTransaction()) {
162
+ throw new Error('Invalid use of TypeOrmDataSource.entityManager outside of a DBOS transaction');
163
+ }
164
+ const ctx = asyncLocalCtx.getStore();
165
+ if (!ctx) {
166
+ throw new Error('Invalid use of TypeOrmDataSource.entityManager outside of a DBOS transaction');
167
+ }
168
+ if (p && p !== ctx.owner) {
169
+ throw new Error('Invalid retrieval of `TypeOrmDataSource.entityManager` from the wrong instance');
170
+ }
171
+ return ctx.entityManager;
172
+ }
173
+ static get entityManager() {
174
+ return TypeOrmDataSource.#getEntityManager(undefined);
175
+ }
176
+ get entityManager() {
177
+ return TypeOrmDataSource.#getEntityManager(this.#provider);
178
+ }
179
+ static async initializeDBOSSchema(config) {
180
+ const ds = await TypeOrmTransactionHandler.createInstance(config, []);
181
+ try {
182
+ await ds.query(datasource_1.createTransactionCompletionSchemaPG);
183
+ await ds.query(datasource_1.createTransactionCompletionTablePG);
184
+ }
185
+ finally {
186
+ await ds.destroy();
187
+ }
188
+ }
189
+ #provider;
190
+ constructor(name, config,
191
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
192
+ entities) {
193
+ this.name = name;
194
+ this.#provider = new TypeOrmTransactionHandler(name, config, entities);
195
+ (0, datasource_1.registerDataSource)(this.#provider);
196
+ }
197
+ /**
198
+ * Run `func` as a transaction against this DataSource
199
+ * @param func Function to run within a transactional context
200
+ * @param funcName Name to record for the transaction
201
+ * @param config Transaction configuration (isolation, etc)
202
+ * @returns Return value from `func`
203
+ */
204
+ async runTransaction(func, config) {
205
+ return await (0, datasource_1.runTransaction)(func, config?.name ?? func.name, { dsName: this.name, config });
206
+ }
207
+ /**
208
+ * Register function as DBOS transaction, to be called within the context
209
+ * of a transaction on this data source.
210
+ *
211
+ * @param func Function to wrap
212
+ * @param target Name of function
213
+ * @param config Transaction settings
214
+ * @returns Wrapped function, to be called instead of `func`
215
+ */
216
+ registerTransaction(func, config) {
217
+ return (0, datasource_1.registerTransaction)(this.name, func, { name: config?.name ?? func.name }, config);
218
+ }
219
+ /**
220
+ * Decorator establishing function as a transaction
221
+ */
222
+ transaction(config) {
223
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
224
+ const ds = this;
225
+ return function decorator(_target, propertyKey, descriptor) {
226
+ if (!descriptor.value) {
227
+ throw new Error('Use of decorator when original method is undefined');
228
+ }
229
+ descriptor.value = ds.registerTransaction(descriptor.value, {
230
+ ...config,
231
+ name: config?.name ?? String(propertyKey),
232
+ });
233
+ return descriptor;
234
+ };
235
+ }
236
+ }
237
+ exports.TypeOrmDataSource = TypeOrmDataSource;
238
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
package/index.ts ADDED
@@ -0,0 +1,324 @@
1
+ import { PoolConfig } from 'pg';
2
+ import { DBOS, DBOSWorkflowConflictError } from '@dbos-inc/dbos-sdk';
3
+ import {
4
+ type DataSourceTransactionHandler,
5
+ createTransactionCompletionSchemaPG,
6
+ createTransactionCompletionTablePG,
7
+ isPGRetriableTransactionError,
8
+ isPGKeyConflictError,
9
+ registerTransaction,
10
+ runTransaction,
11
+ DBOSDataSource,
12
+ registerDataSource,
13
+ PGTransactionConfig,
14
+ } from '@dbos-inc/dbos-sdk/datasource';
15
+ import { DataSource, EntityManager } from 'typeorm';
16
+ import { AsyncLocalStorage } from 'async_hooks';
17
+ import { SuperJSON } from 'superjson';
18
+
19
+ export interface TypeORMTransactionConfig extends PGTransactionConfig {
20
+ name?: string;
21
+ }
22
+
23
+ interface DBOSTypeOrmLocalCtx {
24
+ entityManager: EntityManager;
25
+ owner: TypeOrmTransactionHandler;
26
+ }
27
+
28
+ const asyncLocalCtx = new AsyncLocalStorage<DBOSTypeOrmLocalCtx>();
29
+
30
+ interface transaction_completion {
31
+ workflow_id: string;
32
+ function_num: number;
33
+ output: string | null;
34
+ error: string | null;
35
+ }
36
+
37
+ class TypeOrmTransactionHandler implements DataSourceTransactionHandler {
38
+ readonly dsType = 'TypeOrm';
39
+ #dataSourceField: DataSource | undefined;
40
+
41
+ constructor(
42
+ readonly name: string,
43
+ private readonly config: PoolConfig,
44
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
45
+ private readonly entities: Function[],
46
+ ) {}
47
+
48
+ static async createInstance(
49
+ config: PoolConfig,
50
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
51
+ entities: Function[],
52
+ ): Promise<DataSource> {
53
+ const ds = new DataSource({
54
+ type: 'postgres',
55
+ entities: entities,
56
+ url: config.connectionString,
57
+ host: config.host,
58
+ port: config.port,
59
+ username: config.user,
60
+ // password: config.password,
61
+ database: config.database,
62
+ // ssl: config.ssl,
63
+ connectTimeoutMS: config.connectionTimeoutMillis,
64
+ poolSize: config.max,
65
+ });
66
+ await ds.initialize();
67
+ return ds;
68
+ }
69
+
70
+ async initialize(): Promise<void> {
71
+ const ds = this.#dataSourceField;
72
+ this.#dataSourceField = await TypeOrmTransactionHandler.createInstance(this.config, this.entities);
73
+ await ds?.destroy();
74
+ }
75
+
76
+ async destroy(): Promise<void> {
77
+ const ds = this.#dataSourceField;
78
+ this.#dataSourceField = undefined;
79
+ await ds?.destroy();
80
+ }
81
+
82
+ get #dataSource() {
83
+ if (!this.#dataSourceField) {
84
+ throw new Error(`DataSource ${this.name} is not initialized.`);
85
+ }
86
+ return this.#dataSourceField;
87
+ }
88
+
89
+ async #checkExecution(
90
+ workflowID: string,
91
+ stepID: number,
92
+ ): Promise<{ output: string | null } | { error: string } | undefined> {
93
+ type TxOutputRow = Pick<transaction_completion, 'output' | 'error'>;
94
+ const rows = await this.#dataSource.query<TxOutputRow[]>(
95
+ `SELECT output, error FROM dbos.transaction_completion
96
+ WHERE workflow_id=$1 AND function_num=$2;`,
97
+ [workflowID, stepID],
98
+ );
99
+
100
+ if (rows.length !== 1) {
101
+ return undefined;
102
+ }
103
+
104
+ if (rows[0].output === null) {
105
+ return undefined;
106
+ }
107
+
108
+ const { output, error } = rows[0];
109
+ return error !== null ? { error } : { output };
110
+ }
111
+
112
+ static async #recordOutput(
113
+ entityManager: EntityManager,
114
+ workflowID: string,
115
+ stepID: number,
116
+ output: string,
117
+ ): Promise<void> {
118
+ try {
119
+ await entityManager.query(
120
+ `INSERT INTO dbos.transaction_completion (workflow_id, function_num, output)
121
+ VALUES ($1, $2, $3)`,
122
+ [workflowID, stepID, output],
123
+ );
124
+ } catch (error) {
125
+ if (isPGKeyConflictError(error)) {
126
+ throw new DBOSWorkflowConflictError(workflowID);
127
+ } else {
128
+ throw error;
129
+ }
130
+ }
131
+ }
132
+
133
+ async #recordError(workflowID: string, stepID: number, error: string): Promise<void> {
134
+ try {
135
+ await this.#dataSource.query(
136
+ `INSERT INTO dbos.transaction_completion (workflow_id, function_num, error)
137
+ VALUES ($1, $2, $3)`,
138
+ [workflowID, stepID, error],
139
+ );
140
+ } catch (error) {
141
+ if (isPGKeyConflictError(error)) {
142
+ throw new DBOSWorkflowConflictError(workflowID);
143
+ } else {
144
+ throw error;
145
+ }
146
+ }
147
+ }
148
+
149
+ /* Required by base class */
150
+ async invokeTransactionFunction<This, Args extends unknown[], Return>(
151
+ config: PGTransactionConfig | undefined,
152
+ target: This,
153
+ func: (this: This, ...args: Args) => Promise<Return>,
154
+ ...args: Args
155
+ ): Promise<Return> {
156
+ const workflowID = DBOS.workflowID;
157
+ const stepID = DBOS.stepID;
158
+ if (workflowID !== undefined && stepID === undefined) {
159
+ throw new Error('DBOS.stepID is undefined inside a workflow.');
160
+ }
161
+
162
+ const isolationLevel = config?.isolationLevel ?? 'READ COMMITTED';
163
+ const readOnly = config?.readOnly ? true : false;
164
+ const saveResults = !readOnly && workflowID !== undefined;
165
+
166
+ // Retry loop if appropriate
167
+ let retryWaitMS = 1;
168
+ const backoffFactor = 1.5;
169
+ const maxRetryWaitMS = 2000; // Maximum wait 2 seconds.
170
+
171
+ while (true) {
172
+ // Check to see if this tx has already been executed
173
+ const previousResult = saveResults ? await this.#checkExecution(workflowID, stepID!) : undefined;
174
+ if (previousResult) {
175
+ DBOS.span?.setAttribute('cached', true);
176
+
177
+ if ('error' in previousResult) {
178
+ throw SuperJSON.parse(previousResult.error);
179
+ }
180
+ return (previousResult.output ? SuperJSON.parse(previousResult.output) : null) as Return;
181
+ }
182
+
183
+ try {
184
+ const result = await this.#dataSource.transaction(isolationLevel, async (entityManager: EntityManager) => {
185
+ if (readOnly) {
186
+ await entityManager.query('SET TRANSACTION READ ONLY');
187
+ }
188
+
189
+ const result = await asyncLocalCtx.run({ entityManager, owner: this }, async () => {
190
+ return await func.call(target, ...args);
191
+ });
192
+
193
+ // save the output of read/write transactions
194
+ if (saveResults) {
195
+ await TypeOrmTransactionHandler.#recordOutput(
196
+ entityManager,
197
+ workflowID,
198
+ stepID!,
199
+ SuperJSON.stringify(result),
200
+ );
201
+ }
202
+
203
+ return result;
204
+ });
205
+
206
+ return result;
207
+ } catch (error) {
208
+ if (isPGRetriableTransactionError(error)) {
209
+ DBOS.span?.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMS }, performance.now());
210
+ // Retry serialization failures.
211
+ await new Promise((resolve) => setTimeout(resolve, retryWaitMS));
212
+ retryWaitMS = Math.min(retryWaitMS * backoffFactor, maxRetryWaitMS);
213
+ continue;
214
+ } else {
215
+ if (saveResults) {
216
+ const message = SuperJSON.stringify(error);
217
+ await this.#recordError(workflowID, stepID!, message);
218
+ }
219
+
220
+ throw error;
221
+ }
222
+ }
223
+ }
224
+ }
225
+ }
226
+
227
+ export class TypeOrmDataSource implements DBOSDataSource<TypeORMTransactionConfig> {
228
+ // User calls this... DBOS not directly involved...
229
+ static #getEntityManager(p?: TypeOrmTransactionHandler): EntityManager {
230
+ if (!DBOS.isInTransaction()) {
231
+ throw new Error('Invalid use of TypeOrmDataSource.entityManager outside of a DBOS transaction');
232
+ }
233
+ const ctx = asyncLocalCtx.getStore();
234
+ if (!ctx) {
235
+ throw new Error('Invalid use of TypeOrmDataSource.entityManager outside of a DBOS transaction');
236
+ }
237
+ if (p && p !== ctx.owner) {
238
+ throw new Error('Invalid retrieval of `TypeOrmDataSource.entityManager` from the wrong instance');
239
+ }
240
+
241
+ return ctx.entityManager;
242
+ }
243
+
244
+ static get entityManager() {
245
+ return TypeOrmDataSource.#getEntityManager(undefined);
246
+ }
247
+
248
+ get entityManager() {
249
+ return TypeOrmDataSource.#getEntityManager(this.#provider);
250
+ }
251
+
252
+ static async initializeDBOSSchema(config: PoolConfig): Promise<void> {
253
+ const ds = await TypeOrmTransactionHandler.createInstance(config, []);
254
+ try {
255
+ await ds.query(createTransactionCompletionSchemaPG);
256
+ await ds.query(createTransactionCompletionTablePG);
257
+ } finally {
258
+ await ds.destroy();
259
+ }
260
+ }
261
+
262
+ #provider: TypeOrmTransactionHandler;
263
+
264
+ constructor(
265
+ readonly name: string,
266
+ config: PoolConfig,
267
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
268
+ entities: Function[],
269
+ ) {
270
+ this.#provider = new TypeOrmTransactionHandler(name, config, entities);
271
+ registerDataSource(this.#provider);
272
+ }
273
+
274
+ /**
275
+ * Run `func` as a transaction against this DataSource
276
+ * @param func Function to run within a transactional context
277
+ * @param funcName Name to record for the transaction
278
+ * @param config Transaction configuration (isolation, etc)
279
+ * @returns Return value from `func`
280
+ */
281
+ async runTransaction<T>(func: () => Promise<T>, config?: TypeORMTransactionConfig) {
282
+ return await runTransaction(func, config?.name ?? func.name, { dsName: this.name, config });
283
+ }
284
+
285
+ /**
286
+ * Register function as DBOS transaction, to be called within the context
287
+ * of a transaction on this data source.
288
+ *
289
+ * @param func Function to wrap
290
+ * @param target Name of function
291
+ * @param config Transaction settings
292
+ * @returns Wrapped function, to be called instead of `func`
293
+ */
294
+ registerTransaction<This, Args extends unknown[], Return>(
295
+ func: (this: This, ...args: Args) => Promise<Return>,
296
+ config?: TypeORMTransactionConfig,
297
+ ): (this: This, ...args: Args) => Promise<Return> {
298
+ return registerTransaction(this.name, func, { name: config?.name ?? func.name }, config);
299
+ }
300
+
301
+ /**
302
+ * Decorator establishing function as a transaction
303
+ */
304
+ transaction(config?: TypeORMTransactionConfig) {
305
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
306
+ const ds = this;
307
+ return function decorator<This, Args extends unknown[], Return>(
308
+ _target: object,
309
+ propertyKey: PropertyKey,
310
+ descriptor: TypedPropertyDescriptor<(this: This, ...args: Args) => Promise<Return>>,
311
+ ) {
312
+ if (!descriptor.value) {
313
+ throw new Error('Use of decorator when original method is undefined');
314
+ }
315
+
316
+ descriptor.value = ds.registerTransaction(descriptor.value, {
317
+ ...config,
318
+ name: config?.name ?? String(propertyKey),
319
+ });
320
+
321
+ return descriptor;
322
+ };
323
+ }
324
+ }
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@dbos-inc/typeorm-datasource",
3
- "version": "3.0.11-preview.gc9233b8190",
4
- "main": "dist/src/index.js",
5
- "types": "dist/src/index.d.ts",
3
+ "version": "3.0.16-preview",
4
+ "description": "DBOS DataSource library for TypeORM with PostgreSQL support",
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",
@@ -12,10 +15,6 @@
12
15
  "build": "tsc --project tsconfig.json",
13
16
  "test": "npm run build && jest --detectOpenHandles"
14
17
  },
15
- "keywords": [],
16
- "author": "",
17
- "license": "MIT",
18
- "description": "",
19
18
  "dependencies": {
20
19
  "typeorm": "^0.3.24",
21
20
  "pg": "^8.11.3",
@@ -31,6 +30,6 @@
31
30
  "jest": "^29.7.0",
32
31
  "supertest": "^7.0.0",
33
32
  "ts-jest": "^29.1.4",
34
- "typescript": "^5.3.3"
33
+ "typescript": "^5.4.5"
35
34
  }
36
35
  }
@@ -0,0 +1,31 @@
1
+ import { Client } from 'pg';
2
+ import { TypeOrmDataSource } from '../index';
3
+ import { dropDB, ensureDB } from './test-helpers';
4
+
5
+ describe('TypeOrmDataSource.configure', () => {
6
+ const config = { user: 'postgres', database: 'typeorm_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, true);
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 TypeOrmDataSource.initializeDBOSSchema(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
+ });