@dbos-inc/drizzle-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,31 @@
1
+ import { ClientConfig, PoolConfig } from 'pg';
2
+ import { DBOSDataSource } from '@dbos-inc/dbos-sdk/datasource';
3
+ import { NodePgDatabase } from 'drizzle-orm/node-postgres';
4
+ import { PgTransactionConfig } from 'drizzle-orm/pg-core';
5
+ export type TransactionConfig = Pick<PgTransactionConfig, 'isolationLevel' | 'accessMode'> & {
6
+ name?: string;
7
+ };
8
+ export interface transaction_completion {
9
+ workflow_id: string;
10
+ function_num: number;
11
+ output: string | null;
12
+ error: string | null;
13
+ }
14
+ export declare class DrizzleDataSource implements DBOSDataSource<TransactionConfig> {
15
+ #private;
16
+ readonly name: string;
17
+ static get client(): NodePgDatabase<{
18
+ [key: string]: object;
19
+ }>;
20
+ get client(): NodePgDatabase<{
21
+ [key: string]: object;
22
+ }>;
23
+ static initializeDBOSSchema(config: ClientConfig): Promise<void>;
24
+ constructor(name: string, config: PoolConfig, entities?: {
25
+ [key: string]: object;
26
+ });
27
+ runTransaction<T>(func: () => Promise<T>, config?: TransactionConfig): Promise<T>;
28
+ registerTransaction<This, Args extends unknown[], Return>(func: (this: This, ...args: Args) => Promise<Return>, config?: TransactionConfig): (this: This, ...args: Args) => Promise<Return>;
29
+ transaction(config?: TransactionConfig): <This, Args extends unknown[], Return>(_target: object, propertyKey: PropertyKey, descriptor: TypedPropertyDescriptor<(this: This, ...args: Args) => Promise<Return>>) => TypedPropertyDescriptor<(this: This, ...args: Args) => Promise<Return>>;
30
+ }
31
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,YAAY,EAAQ,UAAU,EAAE,MAAM,IAAI,CAAC;AAE5D,OAAO,EAQL,cAAc,EAEf,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAW,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAGpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAQ1D,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAC,mBAAmB,EAAE,gBAAgB,GAAG,YAAY,CAAC,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAI/G,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAwKD,qBAAa,iBAAkB,YAAW,cAAc,CAAC,iBAAiB,CAAC;;IAsCvE,QAAQ,CAAC,IAAI,EAAE,MAAM;IAtBvB,MAAM,KAAK,MAAM;;OAEhB;IAED,IAAI,MAAM;;OAET;WAEY,oBAAoB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;gBAc3D,IAAI,EAAE,MAAM,EACrB,MAAM,EAAE,UAAU,EAClB,QAAQ,GAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAO;IAMpC,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,iBAAiB;IAI1E,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,iBAAiB,GACzB,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC;IAKjD,WAAW,CAAC,MAAM,CAAC,EAAE,iBAAiB,mDAIzB,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,202 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DrizzleDataSource = void 0;
4
+ const pg_1 = require("pg");
5
+ const dbos_sdk_1 = require("@dbos-inc/dbos-sdk");
6
+ const datasource_1 = require("@dbos-inc/dbos-sdk/datasource");
7
+ const node_postgres_1 = require("drizzle-orm/node-postgres");
8
+ const async_hooks_1 = require("async_hooks");
9
+ const superjson_1 = require("superjson");
10
+ const drizzle_orm_1 = require("drizzle-orm");
11
+ const asyncLocalCtx = new async_hooks_1.AsyncLocalStorage();
12
+ class DrizzleTransactionHandler {
13
+ name;
14
+ config;
15
+ entities;
16
+ dsType = 'drizzle';
17
+ #connection;
18
+ constructor(name, config, entities = {}) {
19
+ this.name = name;
20
+ this.config = config;
21
+ this.entities = entities;
22
+ }
23
+ async initialize() {
24
+ const conn = this.#connection;
25
+ const driver = new pg_1.Pool(this.config);
26
+ const db = (0, node_postgres_1.drizzle)(driver, { schema: this.entities });
27
+ this.#connection = { db, end: () => driver.end() };
28
+ await conn?.end();
29
+ }
30
+ async destroy() {
31
+ const conn = this.#connection;
32
+ this.#connection = undefined;
33
+ await conn?.end();
34
+ }
35
+ get #drizzle() {
36
+ if (!this.#connection) {
37
+ throw new Error(`DataSource ${this.name} is not initialized.`);
38
+ }
39
+ return this.#connection.db;
40
+ }
41
+ async #checkExecution(workflowID, stepID) {
42
+ const statement = (0, drizzle_orm_1.sql) `
43
+ SELECT output, error FROM dbos.transaction_completion
44
+ WHERE workflow_id = ${workflowID} AND function_num = ${stepID}`;
45
+ const result = await this.#drizzle.execute(statement);
46
+ if (result.rows.length !== 1) {
47
+ return undefined;
48
+ }
49
+ const { output, error } = result.rows[0];
50
+ return error !== null ? { error } : { output };
51
+ }
52
+ static async #recordOutput(client, workflowID, stepID, output) {
53
+ try {
54
+ const statement = (0, drizzle_orm_1.sql) `
55
+ INSERT INTO dbos.transaction_completion (workflow_id, function_num, output)
56
+ VALUES (${workflowID}, ${stepID}, ${output})`;
57
+ await client.execute(statement);
58
+ }
59
+ catch (error) {
60
+ if ((0, datasource_1.isPGKeyConflictError)(error)) {
61
+ throw new dbos_sdk_1.DBOSWorkflowConflictError(workflowID);
62
+ }
63
+ else {
64
+ throw error;
65
+ }
66
+ }
67
+ }
68
+ async #recordError(workflowID, stepID, error) {
69
+ try {
70
+ const statement = (0, drizzle_orm_1.sql) `
71
+ INSERT INTO dbos.transaction_completion (workflow_id, function_num, error)
72
+ VALUES (${workflowID}, ${stepID}, ${error})`;
73
+ await this.#drizzle.execute(statement);
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
+ /* Invoke a transaction function, called by the framework */
85
+ async invokeTransactionFunction(config, target, func, ...args) {
86
+ const workflowID = dbos_sdk_1.DBOS.workflowID;
87
+ const stepID = dbos_sdk_1.DBOS.stepID;
88
+ if (workflowID !== undefined && stepID === undefined) {
89
+ throw new Error('DBOS.stepID is undefined inside a workflow.');
90
+ }
91
+ const readOnly = config?.accessMode === 'read only' ? true : false;
92
+ const saveResults = !readOnly && workflowID !== undefined;
93
+ // Retry loop if appropriate
94
+ let retryWaitMS = 1;
95
+ const backoffFactor = 1.5;
96
+ const maxRetryWaitMS = 2000; // Maximum wait 2 seconds.
97
+ while (true) {
98
+ // Check to see if this tx has already been executed
99
+ const previousResult = saveResults ? await this.#checkExecution(workflowID, stepID) : undefined;
100
+ if (previousResult) {
101
+ dbos_sdk_1.DBOS.span?.setAttribute('cached', true);
102
+ if ('error' in previousResult) {
103
+ throw superjson_1.SuperJSON.parse(previousResult.error);
104
+ }
105
+ return (previousResult.output ? superjson_1.SuperJSON.parse(previousResult.output) : null);
106
+ }
107
+ try {
108
+ const result = await this.#drizzle.transaction(async (client) => {
109
+ // execute user's transaction function
110
+ const result = await asyncLocalCtx.run({ client, owner: this }, async () => {
111
+ return await func.call(target, ...args);
112
+ });
113
+ // save the output of read/write transactions
114
+ if (saveResults) {
115
+ await DrizzleTransactionHandler.#recordOutput(client, workflowID, stepID, superjson_1.SuperJSON.stringify(result));
116
+ }
117
+ return result;
118
+ }, { accessMode: config?.accessMode, isolationLevel: config?.isolationLevel });
119
+ return result;
120
+ }
121
+ catch (error) {
122
+ if ((0, datasource_1.isPGRetriableTransactionError)(error)) {
123
+ dbos_sdk_1.DBOS.span?.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMS }, performance.now());
124
+ // Retry serialization failures.
125
+ await new Promise((resolve) => setTimeout(resolve, retryWaitMS));
126
+ retryWaitMS = Math.min(retryWaitMS * backoffFactor, maxRetryWaitMS);
127
+ continue;
128
+ }
129
+ else {
130
+ if (saveResults) {
131
+ const message = superjson_1.SuperJSON.stringify(error);
132
+ await this.#recordError(workflowID, stepID, message);
133
+ }
134
+ throw error;
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+ class DrizzleDataSource {
141
+ name;
142
+ // User calls this... DBOS not directly involved...
143
+ static #getClient(p) {
144
+ if (!dbos_sdk_1.DBOS.isInTransaction()) {
145
+ throw new Error('Invalid use of DrizzleDataSource.client outside of a DBOS transaction');
146
+ }
147
+ const ctx = asyncLocalCtx.getStore();
148
+ if (!ctx) {
149
+ throw new Error('Invalid use of DrizzleDataSource.client outside of a DBOS transaction');
150
+ }
151
+ if (p && p !== ctx.owner) {
152
+ throw new Error('Invalid retrieval of `DrizzleDataSource.client` from the incorrect object');
153
+ }
154
+ return ctx.client;
155
+ }
156
+ static get client() {
157
+ return DrizzleDataSource.#getClient(undefined);
158
+ }
159
+ get client() {
160
+ return DrizzleDataSource.#getClient(this.#provider);
161
+ }
162
+ static async initializeDBOSSchema(config) {
163
+ const client = new pg_1.Client(config);
164
+ try {
165
+ await client.connect();
166
+ await client.query(datasource_1.createTransactionCompletionSchemaPG);
167
+ await client.query(datasource_1.createTransactionCompletionTablePG);
168
+ }
169
+ finally {
170
+ await client.end();
171
+ }
172
+ }
173
+ #provider;
174
+ constructor(name, config, entities = {}) {
175
+ this.name = name;
176
+ this.#provider = new DrizzleTransactionHandler(name, config, entities);
177
+ (0, datasource_1.registerDataSource)(this.#provider);
178
+ }
179
+ async runTransaction(func, config) {
180
+ return await (0, datasource_1.runTransaction)(func, config?.name ?? func.name, { dsName: this.name, config });
181
+ }
182
+ registerTransaction(func, config) {
183
+ return (0, datasource_1.registerTransaction)(this.name, func, { name: config?.name ?? func.name }, config);
184
+ }
185
+ // decorator
186
+ transaction(config) {
187
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
188
+ const ds = this;
189
+ return function decorator(_target, propertyKey, descriptor) {
190
+ if (!descriptor.value) {
191
+ throw new Error('Use of decorator when original method is undefined');
192
+ }
193
+ descriptor.value = ds.registerTransaction(descriptor.value, {
194
+ ...config,
195
+ name: config?.name ?? String(propertyKey),
196
+ });
197
+ return descriptor;
198
+ };
199
+ }
200
+ }
201
+ exports.DrizzleDataSource = DrizzleDataSource;
202
+ //# 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,280 @@
1
+ import { Client, ClientConfig, Pool, 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
+ } from '@dbos-inc/dbos-sdk/datasource';
14
+ import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres';
15
+ import { AsyncLocalStorage } from 'async_hooks';
16
+ import { SuperJSON } from 'superjson';
17
+ import { PgTransactionConfig } from 'drizzle-orm/pg-core';
18
+ import { sql } from 'drizzle-orm';
19
+
20
+ interface DrizzleLocalCtx {
21
+ client: NodePgDatabase<{ [key: string]: object }>;
22
+ owner: DrizzleTransactionHandler;
23
+ }
24
+
25
+ export type TransactionConfig = Pick<PgTransactionConfig, 'isolationLevel' | 'accessMode'> & { name?: string };
26
+
27
+ const asyncLocalCtx = new AsyncLocalStorage<DrizzleLocalCtx>();
28
+
29
+ export interface transaction_completion {
30
+ workflow_id: string;
31
+ function_num: number;
32
+ output: string | null;
33
+ error: string | null;
34
+ }
35
+
36
+ interface DrizzleConnection {
37
+ readonly db: NodePgDatabase<{ [key: string]: object }>;
38
+ end(): Promise<void>;
39
+ }
40
+
41
+ class DrizzleTransactionHandler implements DataSourceTransactionHandler {
42
+ readonly dsType = 'drizzle';
43
+ #connection: DrizzleConnection | undefined;
44
+
45
+ constructor(
46
+ readonly name: string,
47
+ private readonly config: PoolConfig,
48
+ private readonly entities: { [key: string]: object } = {},
49
+ ) {}
50
+
51
+ async initialize(): Promise<void> {
52
+ const conn = this.#connection;
53
+
54
+ const driver = new Pool(this.config);
55
+ const db = drizzle(driver, { schema: this.entities });
56
+ this.#connection = { db, end: () => driver.end() };
57
+
58
+ await conn?.end();
59
+ }
60
+
61
+ async destroy(): Promise<void> {
62
+ const conn = this.#connection;
63
+
64
+ this.#connection = undefined;
65
+
66
+ await conn?.end();
67
+ }
68
+
69
+ get #drizzle(): NodePgDatabase<{ [key: string]: object }> {
70
+ if (!this.#connection) {
71
+ throw new Error(`DataSource ${this.name} is not initialized.`);
72
+ }
73
+ return this.#connection.db;
74
+ }
75
+
76
+ async #checkExecution(
77
+ workflowID: string,
78
+ stepID: number,
79
+ ): Promise<{ output: string | null } | { error: string } | undefined> {
80
+ type Result = { output: string | null; error: string | null };
81
+
82
+ const statement = sql`
83
+ SELECT output, error FROM dbos.transaction_completion
84
+ WHERE workflow_id = ${workflowID} AND function_num = ${stepID}`;
85
+ const result = await this.#drizzle.execute<Result>(statement);
86
+
87
+ if (result.rows.length !== 1) {
88
+ return undefined;
89
+ }
90
+
91
+ const { output, error } = result.rows[0];
92
+ return error !== null ? { error } : { output };
93
+ }
94
+
95
+ static async #recordOutput(
96
+ client: NodePgDatabase<{ [key: string]: object }>,
97
+ workflowID: string,
98
+ stepID: number,
99
+ output: string,
100
+ ): Promise<void> {
101
+ try {
102
+ const statement = sql`
103
+ INSERT INTO dbos.transaction_completion (workflow_id, function_num, output)
104
+ VALUES (${workflowID}, ${stepID}, ${output})`;
105
+ await client.execute(statement);
106
+ } catch (error) {
107
+ if (isPGKeyConflictError(error)) {
108
+ throw new DBOSWorkflowConflictError(workflowID);
109
+ } else {
110
+ throw error;
111
+ }
112
+ }
113
+ }
114
+
115
+ async #recordError(workflowID: string, stepID: number, error: string): Promise<void> {
116
+ try {
117
+ const statement = sql`
118
+ INSERT INTO dbos.transaction_completion (workflow_id, function_num, error)
119
+ VALUES (${workflowID}, ${stepID}, ${error})`;
120
+ await this.#drizzle.execute(statement);
121
+ } catch (error) {
122
+ if (isPGKeyConflictError(error)) {
123
+ throw new DBOSWorkflowConflictError(workflowID);
124
+ } else {
125
+ throw error;
126
+ }
127
+ }
128
+ }
129
+
130
+ /* Invoke a transaction function, called by the framework */
131
+ async invokeTransactionFunction<This, Args extends unknown[], Return>(
132
+ config: TransactionConfig | undefined,
133
+ target: This,
134
+ func: (this: This, ...args: Args) => Promise<Return>,
135
+ ...args: Args
136
+ ): Promise<Return> {
137
+ const workflowID = DBOS.workflowID;
138
+ const stepID = DBOS.stepID;
139
+ if (workflowID !== undefined && stepID === undefined) {
140
+ throw new Error('DBOS.stepID is undefined inside a workflow.');
141
+ }
142
+
143
+ const readOnly = config?.accessMode === 'read only' ? true : false;
144
+ const saveResults = !readOnly && workflowID !== undefined;
145
+
146
+ // Retry loop if appropriate
147
+ let retryWaitMS = 1;
148
+ const backoffFactor = 1.5;
149
+ const maxRetryWaitMS = 2000; // Maximum wait 2 seconds.
150
+
151
+ while (true) {
152
+ // Check to see if this tx has already been executed
153
+ const previousResult = saveResults ? await this.#checkExecution(workflowID, stepID!) : undefined;
154
+ if (previousResult) {
155
+ DBOS.span?.setAttribute('cached', true);
156
+
157
+ if ('error' in previousResult) {
158
+ throw SuperJSON.parse(previousResult.error);
159
+ }
160
+ return (previousResult.output ? SuperJSON.parse(previousResult.output) : null) as Return;
161
+ }
162
+
163
+ try {
164
+ const result = await this.#drizzle.transaction(
165
+ async (client) => {
166
+ // execute user's transaction function
167
+ const result = await asyncLocalCtx.run({ client, owner: this }, async () => {
168
+ return await func.call(target, ...args);
169
+ });
170
+
171
+ // save the output of read/write transactions
172
+ if (saveResults) {
173
+ await DrizzleTransactionHandler.#recordOutput(client, workflowID, stepID!, SuperJSON.stringify(result));
174
+ }
175
+
176
+ return result;
177
+ },
178
+ { accessMode: config?.accessMode, isolationLevel: config?.isolationLevel },
179
+ );
180
+
181
+ return result;
182
+ } catch (error) {
183
+ if (isPGRetriableTransactionError(error)) {
184
+ DBOS.span?.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMS }, performance.now());
185
+ // Retry serialization failures.
186
+ await new Promise((resolve) => setTimeout(resolve, retryWaitMS));
187
+ retryWaitMS = Math.min(retryWaitMS * backoffFactor, maxRetryWaitMS);
188
+ continue;
189
+ } else {
190
+ if (saveResults) {
191
+ const message = SuperJSON.stringify(error);
192
+ await this.#recordError(workflowID, stepID!, message);
193
+ }
194
+
195
+ throw error;
196
+ }
197
+ }
198
+ }
199
+ }
200
+ }
201
+
202
+ export class DrizzleDataSource implements DBOSDataSource<TransactionConfig> {
203
+ // User calls this... DBOS not directly involved...
204
+ static #getClient(p?: DrizzleTransactionHandler): NodePgDatabase<{ [key: string]: object }> {
205
+ if (!DBOS.isInTransaction()) {
206
+ throw new Error('Invalid use of DrizzleDataSource.client outside of a DBOS transaction');
207
+ }
208
+ const ctx = asyncLocalCtx.getStore();
209
+ if (!ctx) {
210
+ throw new Error('Invalid use of DrizzleDataSource.client outside of a DBOS transaction');
211
+ }
212
+ if (p && p !== ctx.owner) {
213
+ throw new Error('Invalid retrieval of `DrizzleDataSource.client` from the incorrect object');
214
+ }
215
+ return ctx.client;
216
+ }
217
+
218
+ static get client() {
219
+ return DrizzleDataSource.#getClient(undefined);
220
+ }
221
+
222
+ get client() {
223
+ return DrizzleDataSource.#getClient(this.#provider);
224
+ }
225
+
226
+ static async initializeDBOSSchema(config: ClientConfig): Promise<void> {
227
+ const client = new Client(config);
228
+ try {
229
+ await client.connect();
230
+ await client.query(createTransactionCompletionSchemaPG);
231
+ await client.query(createTransactionCompletionTablePG);
232
+ } finally {
233
+ await client.end();
234
+ }
235
+ }
236
+
237
+ #provider: DrizzleTransactionHandler;
238
+
239
+ constructor(
240
+ readonly name: string,
241
+ config: PoolConfig,
242
+ entities: { [key: string]: object } = {},
243
+ ) {
244
+ this.#provider = new DrizzleTransactionHandler(name, config, entities);
245
+ registerDataSource(this.#provider);
246
+ }
247
+
248
+ async runTransaction<T>(func: () => Promise<T>, config?: TransactionConfig) {
249
+ return await runTransaction(func, config?.name ?? func.name, { dsName: this.name, config });
250
+ }
251
+
252
+ registerTransaction<This, Args extends unknown[], Return>(
253
+ func: (this: This, ...args: Args) => Promise<Return>,
254
+ config?: TransactionConfig,
255
+ ): (this: This, ...args: Args) => Promise<Return> {
256
+ return registerTransaction(this.name, func, { name: config?.name ?? func.name }, config);
257
+ }
258
+
259
+ // decorator
260
+ transaction(config?: TransactionConfig) {
261
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
262
+ const ds = this;
263
+ return function decorator<This, Args extends unknown[], Return>(
264
+ _target: object,
265
+ propertyKey: PropertyKey,
266
+ descriptor: TypedPropertyDescriptor<(this: This, ...args: Args) => Promise<Return>>,
267
+ ) {
268
+ if (!descriptor.value) {
269
+ throw new Error('Use of decorator when original method is undefined');
270
+ }
271
+
272
+ descriptor.value = ds.registerTransaction(descriptor.value, {
273
+ ...config,
274
+ name: config?.name ?? String(propertyKey),
275
+ });
276
+
277
+ return descriptor;
278
+ };
279
+ }
280
+ }
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@dbos-inc/drizzle-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 Drizzle ORM 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",
@@ -10,14 +13,9 @@
10
13
  },
11
14
  "scripts": {
12
15
  "build": "tsc --project tsconfig.json",
13
- "test": "npm run build && jest --detectOpenHandles"
16
+ "test": "jest --detectOpenHandles"
14
17
  },
15
- "keywords": [],
16
- "author": "",
17
- "license": "MIT",
18
- "description": "",
19
18
  "dependencies": {
20
- "drizzle-kit": "^0.31.1",
21
19
  "drizzle-orm": "^0.40.1",
22
20
  "pg": "^8.11.3",
23
21
  "superjson": "^1.13"
@@ -26,12 +24,10 @@
26
24
  "@dbos-inc/dbos-sdk": "*"
27
25
  },
28
26
  "devDependencies": {
29
- "@types/jest": "^29.5.12",
30
- "@types/node": "^20.11.25",
31
- "@types/supertest": "^6.0.2",
27
+ "@types/jest": "^29.5.14",
28
+ "@types/pg": "^8.15.2",
29
+ "drizzle-kit": "^0.31.1",
32
30
  "jest": "^29.7.0",
33
- "supertest": "^7.0.0",
34
- "ts-jest": "^29.1.4",
35
- "typescript": "^5.3.3"
31
+ "typescript": "^5.4.5"
36
32
  }
37
33
  }
@@ -0,0 +1,31 @@
1
+ import { Client } from 'pg';
2
+ import { DrizzleDataSource } from '../index';
3
+ import { dropDB, ensureDB } from './test-helpers';
4
+
5
+ describe('DrizzleDataSource.configure', () => {
6
+ const config = { user: 'postgres', database: 'drizzle_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 DrizzleDataSource.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
+ });