@dbos-inc/prisma-datasource 3.0.35-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,27 @@
1
+ import { FunctionName } from '@dbos-inc/dbos-sdk';
2
+ import { DBOSDataSource } from '@dbos-inc/dbos-sdk/datasource';
3
+ type PrismaLike = {
4
+ $connect: () => Promise<void>;
5
+ $disconnect: () => Promise<void>;
6
+ $queryRawUnsafe<T = unknown>(query: unknown, ...values: unknown[]): Promise<T>;
7
+ $executeRawUnsafe(query: unknown, ...values: unknown[]): Promise<number>;
8
+ };
9
+ type TransactionIsolationLevel = 'ReadUncommitted' | 'ReadCommitted' | 'RepeatableRead' | 'Serializable';
10
+ export type TransactionConfig = {
11
+ isolationLevel?: TransactionIsolationLevel;
12
+ readOnly?: boolean;
13
+ name?: string;
14
+ };
15
+ export declare class PrismaDataSource<PrismaClient> implements DBOSDataSource<TransactionConfig> {
16
+ #private;
17
+ readonly name: string;
18
+ get client(): PrismaClient;
19
+ static initializeDBOSSchema(prisma: PrismaLike): Promise<void>;
20
+ static uninitializeDBOSSchema(prisma: PrismaLike): Promise<void>;
21
+ constructor(name: string, prismaAccess: PrismaLike | (() => PrismaLike));
22
+ runTransaction<T>(func: () => Promise<T>, config?: TransactionConfig): Promise<T>;
23
+ registerTransaction<This, Args extends unknown[], Return>(func: (this: This, ...args: Args) => Promise<Return>, config?: TransactionConfig & FunctionName): (this: This, ...args: Args) => Promise<Return>;
24
+ 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>>;
25
+ }
26
+ export {};
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AACA,OAAO,EAAmC,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACnF,OAAO,EAML,cAAc,EAIf,MAAM,+BAA+B,CAAC;AAIvC,KAAK,UAAU,GAAG;IAChB,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,eAAe,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC/E,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC1E,CAAC;AAaF,KAAK,yBAAyB,GAAG,iBAAiB,GAAG,eAAe,GAAG,gBAAgB,GAAG,cAAc,CAAC;AAEzG,MAAM,MAAM,iBAAiB,GAAG;IAC9B,cAAc,CAAC,EAAE,yBAAyB,CAAC;IAC3C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAoKF,qBAAa,gBAAgB,CAAC,YAAY,CAAE,YAAW,cAAc,CAAC,iBAAiB,CAAC;;IA8BpF,QAAQ,CAAC,IAAI,EAAE,MAAM;IAjBvB,IAAI,MAAM,IAAI,YAAY,CAEzB;WAEY,oBAAoB,CAAC,MAAM,EAAE,UAAU;WAKvC,sBAAsB,CAAC,MAAM,EAAE,UAAU;gBAQ3C,IAAI,EAAE,MAAM,EACrB,YAAY,EAAE,UAAU,GAAG,CAAC,MAAM,UAAU,CAAC;IAMzC,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,GAAG,YAAY,GACxC,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC;IAIjD,WAAW,CAAC,MAAM,CAAC,EAAE,iBAAiB,kDAI1B,MAAM,eACD,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;CAevF"}
package/dist/index.js ADDED
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PrismaDataSource = void 0;
4
+ const dbos_sdk_1 = require("@dbos-inc/dbos-sdk");
5
+ const datasource_1 = require("@dbos-inc/dbos-sdk/datasource");
6
+ const async_hooks_1 = require("async_hooks");
7
+ const superjson_1 = require("superjson");
8
+ const asyncLocalCtx = new async_hooks_1.AsyncLocalStorage();
9
+ class PrismaTransactionHandler {
10
+ name;
11
+ prismaAccess;
12
+ dsType = 'PrismaDataSource';
13
+ constructor(name, prismaAccess) {
14
+ this.name = name;
15
+ this.prismaAccess = prismaAccess;
16
+ }
17
+ get #prismaDB() {
18
+ if (!this.prismaAccess) {
19
+ throw new Error(`DataSource ${this.name} is not initialized.`);
20
+ }
21
+ if (typeof this.prismaAccess === 'function') {
22
+ const p = this.prismaAccess();
23
+ if (!p) {
24
+ throw new Error(`DataSource ${this.name} is not initialized.`);
25
+ }
26
+ return p;
27
+ }
28
+ return this.prismaAccess;
29
+ }
30
+ initialize() {
31
+ return Promise.resolve();
32
+ }
33
+ destroy() {
34
+ return Promise.resolve();
35
+ }
36
+ async #checkExecution(client, workflowID, stepID) {
37
+ const result = await client.$queryRawUnsafe(`SELECT output, error FROM dbos.transaction_completion
38
+ WHERE workflow_id = $1 AND function_num = $2`, workflowID, stepID);
39
+ if (result?.[0] === undefined) {
40
+ return undefined;
41
+ }
42
+ const { output, error } = result[0];
43
+ return error !== null ? { error } : { output };
44
+ }
45
+ async #recordError(workflowID, stepID, error) {
46
+ try {
47
+ await this.#prismaDB.$executeRawUnsafe(`INSERT INTO dbos.transaction_completion (workflow_id, function_num, error)
48
+ VALUES ($1, $2, $3)`, workflowID, stepID, error);
49
+ }
50
+ catch (error) {
51
+ if ((0, datasource_1.isPGKeyConflictError)(error)) {
52
+ throw new dbos_sdk_1.DBOSWorkflowConflictError(workflowID);
53
+ }
54
+ else {
55
+ throw error;
56
+ }
57
+ }
58
+ }
59
+ static async #recordOutput(client, workflowID, stepID, output) {
60
+ try {
61
+ await client.$executeRawUnsafe(`INSERT INTO dbos.transaction_completion (workflow_id, function_num, output)
62
+ VALUES ($1, $2, $3)`, workflowID, stepID, output);
63
+ }
64
+ catch (error) {
65
+ if ((0, datasource_1.isPGKeyConflictError)(error)) {
66
+ throw new dbos_sdk_1.DBOSWorkflowConflictError(workflowID);
67
+ }
68
+ else {
69
+ throw error;
70
+ }
71
+ }
72
+ }
73
+ async invokeTransactionFunction(config, target, func, ...args) {
74
+ const workflowID = dbos_sdk_1.DBOS.workflowID;
75
+ const stepID = dbos_sdk_1.DBOS.stepID;
76
+ if (workflowID !== undefined && stepID === undefined) {
77
+ throw new Error('DBOS.stepID is undefined inside a workflow.');
78
+ }
79
+ const readOnly = config?.readOnly ?? false;
80
+ const saveResults = !readOnly && workflowID !== undefined;
81
+ // Retry loop if appropriate
82
+ let retryWaitMS = 1;
83
+ const backoffFactor = 1.5;
84
+ const maxRetryWaitMS = 2000; // Maximum wait 2 seconds.
85
+ while (true) {
86
+ // Check to see if this tx has already been executed
87
+ const previousResult = saveResults ? await this.#checkExecution(this.#prismaDB, workflowID, stepID) : undefined;
88
+ if (previousResult) {
89
+ dbos_sdk_1.DBOS.span?.setAttribute('cached', true);
90
+ if ('error' in previousResult) {
91
+ throw superjson_1.SuperJSON.parse(previousResult.error);
92
+ }
93
+ return (previousResult.output ? superjson_1.SuperJSON.parse(previousResult.output) : null);
94
+ }
95
+ try {
96
+ const result = (await this.#prismaDB.$transaction(async (client) => {
97
+ // execute user's transaction function
98
+ const result = await asyncLocalCtx.run({ client, owner: this }, async () => {
99
+ return (await func.call(target, ...args));
100
+ });
101
+ // save the output of read/write transactions
102
+ if (saveResults) {
103
+ await PrismaTransactionHandler.#recordOutput(client, workflowID, stepID, superjson_1.SuperJSON.stringify(result));
104
+ }
105
+ return result;
106
+ }, { isolationLevel: config?.isolationLevel }));
107
+ return result;
108
+ }
109
+ catch (error) {
110
+ if ((0, datasource_1.isPGRetriableTransactionError)(error)) {
111
+ dbos_sdk_1.DBOS.span?.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMS }, performance.now());
112
+ await new Promise((resolve) => setTimeout(resolve, retryWaitMS));
113
+ retryWaitMS = Math.min(retryWaitMS * backoffFactor, maxRetryWaitMS);
114
+ continue;
115
+ }
116
+ else {
117
+ if (saveResults) {
118
+ const message = superjson_1.SuperJSON.stringify(error);
119
+ await this.#recordError(workflowID, stepID, message);
120
+ }
121
+ throw error;
122
+ }
123
+ }
124
+ }
125
+ }
126
+ }
127
+ class PrismaDataSource {
128
+ name;
129
+ static #getClient(p) {
130
+ if (!dbos_sdk_1.DBOS.isInTransaction()) {
131
+ throw new Error('invalid use of PrismaDataSource.client outside of a DBOS transaction.');
132
+ }
133
+ const ctx = asyncLocalCtx.getStore();
134
+ if (!ctx) {
135
+ throw new Error('invalid use of PrismaDataSource.client outside of a DBOS transaction.');
136
+ }
137
+ if (p && p !== ctx.owner)
138
+ throw new Error('Request of `PrismaDataSource.client` from the wrong object.');
139
+ return ctx.client;
140
+ }
141
+ get client() {
142
+ return PrismaDataSource.#getClient(this.#provider);
143
+ }
144
+ static async initializeDBOSSchema(prisma) {
145
+ await prisma.$queryRawUnsafe(datasource_1.createTransactionCompletionSchemaPG);
146
+ await prisma.$queryRawUnsafe(datasource_1.createTransactionCompletionTablePG);
147
+ }
148
+ static async uninitializeDBOSSchema(prisma) {
149
+ await prisma.$executeRawUnsafe('DROP TABLE IF EXISTS dbos.transaction_completion;');
150
+ await prisma.$executeRawUnsafe('DROP SCHEMA IF EXISTS dbos;');
151
+ }
152
+ #provider;
153
+ constructor(name, prismaAccess) {
154
+ this.name = name;
155
+ this.#provider = new PrismaTransactionHandler(name, prismaAccess);
156
+ (0, datasource_1.registerDataSource)(this.#provider);
157
+ }
158
+ async runTransaction(func, config) {
159
+ return await (0, datasource_1.runTransaction)(func, config?.name ?? func.name, { dsName: this.name, config });
160
+ }
161
+ registerTransaction(func, config) {
162
+ return (0, datasource_1.registerTransaction)(this.name, func, config);
163
+ }
164
+ transaction(config) {
165
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
166
+ const ds = this;
167
+ return function decorator(target, propertyKey, descriptor) {
168
+ if (!descriptor.value) {
169
+ throw Error('Use of decorator when original method is undefined');
170
+ }
171
+ descriptor.value = ds.registerTransaction(descriptor.value, {
172
+ ...config,
173
+ name: config?.name ?? String(propertyKey),
174
+ ctorOrProto: target,
175
+ });
176
+ return descriptor;
177
+ };
178
+ }
179
+ }
180
+ exports.PrismaDataSource = PrismaDataSource;
181
+ //# 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,274 @@
1
+
2
+ import { DBOS, DBOSWorkflowConflictError, FunctionName } from '@dbos-inc/dbos-sdk';
3
+ import {
4
+ type DataSourceTransactionHandler,
5
+ isPGRetriableTransactionError,
6
+ isPGKeyConflictError,
7
+ registerTransaction,
8
+ runTransaction,
9
+ DBOSDataSource,
10
+ registerDataSource,
11
+ createTransactionCompletionSchemaPG,
12
+ createTransactionCompletionTablePG,
13
+ } from '@dbos-inc/dbos-sdk/datasource';
14
+ import { AsyncLocalStorage } from 'async_hooks';
15
+ import { SuperJSON } from 'superjson';
16
+
17
+ type PrismaLike = {
18
+ $connect: () => Promise<void>;
19
+ $disconnect: () => Promise<void>;
20
+ $queryRawUnsafe<T = unknown>(query: unknown, ...values: unknown[]): Promise<T>;
21
+ $executeRawUnsafe(query: unknown, ...values: unknown[]): Promise<number>;
22
+ };
23
+
24
+ type PrismaLikeTx = {
25
+ $queryRawUnsafe<T = unknown>(query: TemplateStringsArray | string, ...values: unknown[]): Promise<T>;
26
+ $executeRawUnsafe(query: TemplateStringsArray | string, ...values: unknown[]): Promise<number>;
27
+ $transaction: (tf: (tx: PrismaLike) => Promise<unknown>, config?: unknown) => Promise<unknown>;
28
+ };
29
+
30
+ interface PrismaDataSourceContext {
31
+ client: PrismaLike;
32
+ owner: PrismaTransactionHandler;
33
+ }
34
+
35
+ type TransactionIsolationLevel = 'ReadUncommitted' | 'ReadCommitted' | 'RepeatableRead' | 'Serializable';
36
+
37
+ export type TransactionConfig = {
38
+ isolationLevel?: TransactionIsolationLevel;
39
+ readOnly?: boolean;
40
+ name?: string;
41
+ };
42
+
43
+ const asyncLocalCtx = new AsyncLocalStorage<PrismaDataSourceContext>();
44
+
45
+ class PrismaTransactionHandler implements DataSourceTransactionHandler {
46
+ readonly dsType = 'PrismaDataSource';
47
+
48
+ constructor(
49
+ readonly name: string,
50
+ private readonly prismaAccess: PrismaLike | (() => PrismaLike),
51
+ ) {}
52
+
53
+ get #prismaDB() {
54
+ if (!this.prismaAccess) {
55
+ throw new Error(`DataSource ${this.name} is not initialized.`);
56
+ }
57
+ if (typeof this.prismaAccess === 'function') {
58
+ const p = this.prismaAccess();
59
+ if (!p) {
60
+ throw new Error(`DataSource ${this.name} is not initialized.`);
61
+ }
62
+ return p;
63
+ }
64
+ return this.prismaAccess;
65
+ }
66
+
67
+ initialize(): Promise<void> {
68
+ return Promise.resolve();
69
+ }
70
+
71
+ destroy(): Promise<void> {
72
+ return Promise.resolve();
73
+ }
74
+
75
+ async #checkExecution(
76
+ client: PrismaLike,
77
+ workflowID: string,
78
+ stepID: number,
79
+ ): Promise<{ output: string | null } | { error: string } | undefined> {
80
+ type Result = { output: string | null; error: string | null };
81
+ const result = await client.$queryRawUnsafe<Result[]>(
82
+ `SELECT output, error FROM dbos.transaction_completion
83
+ WHERE workflow_id = $1 AND function_num = $2`,
84
+ workflowID,
85
+ stepID,
86
+ );
87
+ if (result?.[0] === undefined) {
88
+ return undefined;
89
+ }
90
+ const { output, error } = result[0];
91
+ return error !== null ? { error } : { output };
92
+ }
93
+
94
+ async #recordError(workflowID: string, stepID: number, error: string): Promise<void> {
95
+ try {
96
+ await this.#prismaDB.$executeRawUnsafe(
97
+ `INSERT INTO dbos.transaction_completion (workflow_id, function_num, error)
98
+ VALUES ($1, $2, $3)`,
99
+ workflowID,
100
+ stepID,
101
+ error,
102
+ );
103
+ } catch (error) {
104
+ if (isPGKeyConflictError(error)) {
105
+ throw new DBOSWorkflowConflictError(workflowID);
106
+ } else {
107
+ throw error;
108
+ }
109
+ }
110
+ }
111
+
112
+ static async #recordOutput(
113
+ client: PrismaLike,
114
+ workflowID: string,
115
+ stepID: number,
116
+ output: string | null,
117
+ ): Promise<void> {
118
+ try {
119
+ await client.$executeRawUnsafe(
120
+ `INSERT INTO dbos.transaction_completion (workflow_id, function_num, output)
121
+ VALUES ($1, $2, $3)`,
122
+ workflowID,
123
+ stepID,
124
+ output,
125
+ );
126
+ } catch (error) {
127
+ if (isPGKeyConflictError(error)) {
128
+ throw new DBOSWorkflowConflictError(workflowID);
129
+ } else {
130
+ throw error;
131
+ }
132
+ }
133
+ }
134
+
135
+ async invokeTransactionFunction<This, Args extends unknown[], Return>(
136
+ config: TransactionConfig | undefined,
137
+ target: This,
138
+ func: (this: This, ...args: Args) => Promise<Return>,
139
+ ...args: Args
140
+ ): Promise<Return> {
141
+ const workflowID = DBOS.workflowID;
142
+ const stepID = DBOS.stepID;
143
+ if (workflowID !== undefined && stepID === undefined) {
144
+ throw new Error('DBOS.stepID is undefined inside a workflow.');
145
+ }
146
+
147
+ const readOnly = config?.readOnly ?? false;
148
+ const saveResults = !readOnly && workflowID !== undefined;
149
+
150
+ // Retry loop if appropriate
151
+ let retryWaitMS = 1;
152
+ const backoffFactor = 1.5;
153
+ const maxRetryWaitMS = 2000; // Maximum wait 2 seconds.
154
+
155
+ while (true) {
156
+ // Check to see if this tx has already been executed
157
+ const previousResult = saveResults ? await this.#checkExecution(this.#prismaDB, workflowID, stepID!) : undefined;
158
+ if (previousResult) {
159
+ DBOS.span?.setAttribute('cached', true);
160
+
161
+ if ('error' in previousResult) {
162
+ throw SuperJSON.parse(previousResult.error);
163
+ }
164
+ return (previousResult.output ? SuperJSON.parse(previousResult.output) : null) as Return;
165
+ }
166
+
167
+ try {
168
+ const result = (await (this.#prismaDB as unknown as PrismaLikeTx).$transaction(
169
+ async (client) => {
170
+ // execute user's transaction function
171
+ const result = await asyncLocalCtx.run({ client, owner: this }, async () => {
172
+ return (await func.call(target, ...args)) as Return;
173
+ });
174
+
175
+ // save the output of read/write transactions
176
+ if (saveResults) {
177
+ await PrismaTransactionHandler.#recordOutput(client, workflowID, stepID!, SuperJSON.stringify(result));
178
+ }
179
+
180
+ return result;
181
+ },
182
+ { isolationLevel: config?.isolationLevel },
183
+ )) as Return;
184
+
185
+ return result;
186
+ } catch (error) {
187
+ if (isPGRetriableTransactionError(error)) {
188
+ DBOS.span?.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMS }, performance.now());
189
+ await new Promise((resolve) => setTimeout(resolve, retryWaitMS));
190
+ retryWaitMS = Math.min(retryWaitMS * backoffFactor, maxRetryWaitMS);
191
+ continue;
192
+ } else {
193
+ if (saveResults) {
194
+ const message = SuperJSON.stringify(error);
195
+ await this.#recordError(workflowID, stepID!, message);
196
+ }
197
+
198
+ throw error;
199
+ }
200
+ }
201
+ }
202
+ }
203
+ }
204
+
205
+ export class PrismaDataSource<PrismaClient> implements DBOSDataSource<TransactionConfig> {
206
+ static #getClient(p?: PrismaTransactionHandler) {
207
+ if (!DBOS.isInTransaction()) {
208
+ throw new Error('invalid use of PrismaDataSource.client outside of a DBOS transaction.');
209
+ }
210
+ const ctx = asyncLocalCtx.getStore();
211
+ if (!ctx) {
212
+ throw new Error('invalid use of PrismaDataSource.client outside of a DBOS transaction.');
213
+ }
214
+ if (p && p !== ctx.owner) throw new Error('Request of `PrismaDataSource.client` from the wrong object.');
215
+ return ctx.client;
216
+ }
217
+
218
+ get client(): PrismaClient {
219
+ return PrismaDataSource.#getClient(this.#provider) as PrismaClient;
220
+ }
221
+
222
+ static async initializeDBOSSchema(prisma: PrismaLike) {
223
+ await prisma.$queryRawUnsafe(createTransactionCompletionSchemaPG);
224
+ await prisma.$queryRawUnsafe(createTransactionCompletionTablePG);
225
+ }
226
+
227
+ static async uninitializeDBOSSchema(prisma: PrismaLike) {
228
+ await prisma.$executeRawUnsafe('DROP TABLE IF EXISTS dbos.transaction_completion;');
229
+ await prisma.$executeRawUnsafe('DROP SCHEMA IF EXISTS dbos;');
230
+ }
231
+
232
+ #provider: PrismaTransactionHandler;
233
+
234
+ constructor(
235
+ readonly name: string,
236
+ prismaAccess: PrismaLike | (() => PrismaLike),
237
+ ) {
238
+ this.#provider = new PrismaTransactionHandler(name, prismaAccess);
239
+ registerDataSource(this.#provider);
240
+ }
241
+
242
+ async runTransaction<T>(func: () => Promise<T>, config?: TransactionConfig) {
243
+ return await runTransaction(func, config?.name ?? func.name, { dsName: this.name, config });
244
+ }
245
+
246
+ registerTransaction<This, Args extends unknown[], Return>(
247
+ func: (this: This, ...args: Args) => Promise<Return>,
248
+ config?: TransactionConfig & FunctionName,
249
+ ): (this: This, ...args: Args) => Promise<Return> {
250
+ return registerTransaction(this.name, func, config);
251
+ }
252
+
253
+ transaction(config?: TransactionConfig) {
254
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
255
+ const ds = this;
256
+ return function decorator<This, Args extends unknown[], Return>(
257
+ target: object,
258
+ propertyKey: PropertyKey,
259
+ descriptor: TypedPropertyDescriptor<(this: This, ...args: Args) => Promise<Return>>,
260
+ ) {
261
+ if (!descriptor.value) {
262
+ throw Error('Use of decorator when original method is undefined');
263
+ }
264
+
265
+ descriptor.value = ds.registerTransaction(descriptor.value, {
266
+ ...config,
267
+ name: config?.name ?? String(propertyKey),
268
+ ctorOrProto: target,
269
+ });
270
+
271
+ return descriptor;
272
+ };
273
+ }
274
+ }
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,36 @@
1
+ {
2
+ "name": "@dbos-inc/prisma-datasource",
3
+ "version": "3.0.35-preview",
4
+ "description": "DBOS Data Source library for Prisma ORM with PostgreSQL support",
5
+ "license": "MIT",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "homepage": "https://docs.dbos.dev/",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/dbos-inc/dbos-transact-ts",
12
+ "directory": "packages/prisma-datasource"
13
+ },
14
+ "prisma": {
15
+ "schema": "./tests/prisma/schema.prisma"
16
+ },
17
+ "scripts": {
18
+ "build": "tsc --project tsconfig.json",
19
+ "test": "npx prisma generate && tsc --project tsconfig.json && jest --detectOpenHandles"
20
+ },
21
+ "dependencies": {
22
+ "pg": "^8.11.3",
23
+ "superjson": "^1.13"
24
+ },
25
+ "peerDependencies": {
26
+ "@dbos-inc/dbos-sdk": "*"
27
+ },
28
+ "devDependencies": {
29
+ "@prisma/client": "^5.15.0",
30
+ "@types/jest": "^29.5.14",
31
+ "@types/pg": "^8.15.2",
32
+ "jest": "^29.7.0",
33
+ "prisma": "^5.15.0",
34
+ "typescript": "^5.4.5"
35
+ }
36
+ }
@@ -0,0 +1,82 @@
1
+ import { Client } from 'pg';
2
+ import { PrismaDataSource } from '../index';
3
+ import { dropDB, ensureDB } from './test-helpers';
4
+
5
+ import { PrismaClient } from '@prisma/client';
6
+
7
+ const config = { user: 'postgres', database: 'prisma_ds_test' };
8
+
9
+ process.env['DATABASE_URL'] =
10
+ process.env['DATABSE_URL'] ||
11
+ `postgresql://${config.user}:${process.env['PGPASSWORD'] || 'dbos'}@${process.env['PGHOST'] || 'localhost'}:${process.env['PGPORT'] || '5432'}/${config.database}`;
12
+
13
+ describe('PrismaDataSource.initializeDBOSSchema', () => {
14
+ const prisma = new PrismaClient();
15
+
16
+ beforeEach(async () => {
17
+ const client = new Client({ ...config, database: 'postgres' });
18
+ try {
19
+ await client.connect();
20
+ await dropDB(client, config.database, true);
21
+ await ensureDB(client, config.database);
22
+ } finally {
23
+ await client.end();
24
+ }
25
+ await prisma.$connect();
26
+ });
27
+
28
+ afterEach(async () => {
29
+ await prisma.$disconnect();
30
+ });
31
+
32
+ async function queryTxCompletionTable(client: Client) {
33
+ const result = await client.query(
34
+ /*sql*/
35
+ 'SELECT workflow_id, function_num, output, error FROM dbos.transaction_completion',
36
+ );
37
+ return result.rowCount;
38
+ }
39
+
40
+ async function txCompletionTableExists(client: Client) {
41
+ const result = await client.query<{ exists: boolean }>(
42
+ /*sql*/
43
+ "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'dbos' AND table_name = 'transaction_completion');",
44
+ );
45
+ if (result.rowCount !== 1) throw new Error(`unexpected rowcount ${result.rowCount}`);
46
+ return result.rows[0].exists;
47
+ }
48
+
49
+ test('initializeDBOSSchema-with-config', async () => {
50
+ await PrismaDataSource.initializeDBOSSchema(prisma);
51
+
52
+ const client = new Client(config);
53
+ try {
54
+ await client.connect();
55
+ await expect(txCompletionTableExists(client)).resolves.toBe(true);
56
+ await expect(queryTxCompletionTable(client)).resolves.toEqual(0);
57
+
58
+ await PrismaDataSource.uninitializeDBOSSchema(prisma);
59
+
60
+ await expect(txCompletionTableExists(client)).resolves.toBe(false);
61
+ } finally {
62
+ await client.end();
63
+ }
64
+ });
65
+
66
+ test('initializeDBOSSchema-with-prisma', async () => {
67
+ const client = new Client(config);
68
+ try {
69
+ await PrismaDataSource.initializeDBOSSchema(prisma);
70
+ await client.connect();
71
+
72
+ await expect(txCompletionTableExists(client)).resolves.toBe(true);
73
+ await expect(queryTxCompletionTable(client)).resolves.toEqual(0);
74
+
75
+ await PrismaDataSource.uninitializeDBOSSchema(prisma);
76
+
77
+ await expect(txCompletionTableExists(client)).resolves.toBe(false);
78
+ } finally {
79
+ await client.end();
80
+ }
81
+ });
82
+ });
@@ -0,0 +1,464 @@
1
+ import { DBOS } from '@dbos-inc/dbos-sdk';
2
+ import { Client, Pool } from 'pg';
3
+ import { PrismaDataSource } from '..';
4
+ import { dropDB, ensureDB } from './test-helpers';
5
+ import { randomUUID } from 'crypto';
6
+ import SuperJSON from 'superjson';
7
+ import { PrismaClient } from '@prisma/client';
8
+
9
+ const config = { user: 'postgres', database: 'prisma_ds_test' };
10
+
11
+ process.env['DATABASE_URL'] =
12
+ process.env['DATABSE_URL'] ||
13
+ `postgresql://${config.user}:${process.env['PGPASSWORD'] || 'dbos'}@${process.env['PGHOST'] || 'localhost'}:${process.env['PGPORT'] || '5432'}/${config.database}`;
14
+
15
+ const prisma = new PrismaClient();
16
+
17
+ const dataSource = new PrismaDataSource<PrismaClient>('app-db', prisma);
18
+
19
+ interface transaction_completion {
20
+ workflow_id: string;
21
+ function_num: number;
22
+ output: string | null;
23
+ error: string | null;
24
+ }
25
+
26
+ describe('PrismaDataSource', () => {
27
+ const userDB = new Pool(config);
28
+
29
+ beforeAll(async () => {
30
+ {
31
+ const client = new Client({ ...config, database: 'postgres' });
32
+ try {
33
+ await client.connect();
34
+ await dropDB(client, 'prisma_ds_test', true);
35
+ await dropDB(client, 'prisma_ds_test_dbos_sys', true);
36
+ await dropDB(client, config.database, true);
37
+ await ensureDB(client, config.database);
38
+ } finally {
39
+ await client.end();
40
+ }
41
+ }
42
+
43
+ {
44
+ const client = await userDB.connect();
45
+ try {
46
+ await client.query(
47
+ 'CREATE TABLE greetings(name text NOT NULL, greet_count integer DEFAULT 0, PRIMARY KEY(name))',
48
+ );
49
+ } finally {
50
+ client.release();
51
+ }
52
+ }
53
+
54
+ await PrismaDataSource.initializeDBOSSchema(prisma);
55
+ });
56
+
57
+ afterAll(async () => {
58
+ await userDB.end();
59
+ });
60
+
61
+ beforeEach(async () => {
62
+ DBOS.setConfig({ name: 'prisma-ds-test' });
63
+ await DBOS.launch();
64
+ });
65
+
66
+ afterEach(async () => {
67
+ await DBOS.shutdown();
68
+ });
69
+
70
+ test('insert dataSource.register function', async () => {
71
+ const user = 'helloTest1';
72
+
73
+ await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
74
+ const workflowID = randomUUID();
75
+
76
+ await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowReg(user))).resolves.toMatchObject({
77
+ user,
78
+ greet_count: 1,
79
+ });
80
+
81
+ const { rows } = await userDB.query<transaction_completion>(
82
+ 'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
83
+ [workflowID],
84
+ );
85
+ expect(rows.length).toBe(1);
86
+ expect(rows[0].workflow_id).toBe(workflowID);
87
+ expect(rows[0].function_num).toBe(0);
88
+ expect(rows[0].output).not.toBeNull();
89
+ expect(SuperJSON.parse(rows[0].output!)).toMatchObject({ user, greet_count: 1 });
90
+ });
91
+
92
+ test('rerun insert dataSource.register function', async () => {
93
+ const user = 'rerunTest1';
94
+
95
+ await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
96
+ const workflowID = randomUUID();
97
+
98
+ const result = await DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowReg(user));
99
+ expect(result).toMatchObject({ user, greet_count: 1 });
100
+
101
+ await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowReg(user))).resolves.toMatchObject(result);
102
+ });
103
+
104
+ test('insert dataSource.runAsTx function', async () => {
105
+ const user = 'helloTest2';
106
+
107
+ await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
108
+ const workflowID = randomUUID();
109
+
110
+ await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowRunTx(user))).resolves.toMatchObject({
111
+ user,
112
+ greet_count: 1,
113
+ });
114
+
115
+ const { rows } = await userDB.query<transaction_completion>(
116
+ 'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
117
+ [workflowID],
118
+ );
119
+ expect(rows.length).toBe(1);
120
+ expect(rows[0].workflow_id).toBe(workflowID);
121
+ expect(rows[0].function_num).toBe(0);
122
+ expect(rows[0].output).not.toBeNull();
123
+ expect(SuperJSON.parse(rows[0].output!)).toMatchObject({ user, greet_count: 1 });
124
+ });
125
+
126
+ test('rerun insert dataSource.runAsTx function', async () => {
127
+ const user = 'rerunTest2';
128
+
129
+ await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
130
+ const workflowID = randomUUID();
131
+
132
+ const result = await DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowRunTx(user));
133
+ expect(result).toMatchObject({ user, greet_count: 1 });
134
+
135
+ await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowRunTx(user))).resolves.toMatchObject(
136
+ result,
137
+ );
138
+ });
139
+
140
+ async function throws<R>(func: () => Promise<R>): Promise<unknown> {
141
+ try {
142
+ await func();
143
+ fail('Expected function to throw an error');
144
+ } catch (error) {
145
+ return error;
146
+ }
147
+ }
148
+
149
+ test('error dataSource.register function', async () => {
150
+ const user = 'errorTest1';
151
+
152
+ await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
153
+ await userDB.query('INSERT INTO greetings("name","greet_count") VALUES($1,10);', [user]);
154
+ const workflowID = randomUUID();
155
+
156
+ const error = await throws(() => DBOS.withNextWorkflowID(workflowID, () => regErrorWorkflowReg(user)));
157
+ expect(error).toBeInstanceOf(Error);
158
+ expect((error as Error).message).toMatch(/^test error \d+$/);
159
+
160
+ const { rows } = await userDB.query<greetings>('SELECT * FROM greetings WHERE name = $1', [user]);
161
+ expect(rows.length).toBe(1);
162
+ expect(rows[0].greet_count).toBe(10);
163
+
164
+ const { rows: txOutput } = await userDB.query<transaction_completion>(
165
+ 'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
166
+ [workflowID],
167
+ );
168
+ expect(txOutput.length).toBe(1);
169
+ expect(txOutput[0].workflow_id).toBe(workflowID);
170
+ expect(txOutput[0].function_num).toBe(0);
171
+ expect(txOutput[0].output).toBeNull();
172
+ expect(txOutput[0].error).not.toBeNull();
173
+ const $error = SuperJSON.parse(txOutput[0].error!);
174
+ expect($error).toBeInstanceOf(Error);
175
+ expect(($error as Error).message).toMatch(/^test error \d+$/);
176
+ });
177
+
178
+ test('rerun error dataSource.register function', async () => {
179
+ const user = 'rerunErrorTest1';
180
+
181
+ await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
182
+ await userDB.query('INSERT INTO greetings("name","greet_count") VALUES($1,10);', [user]);
183
+ const workflowID = randomUUID();
184
+
185
+ const error = await throws(() => DBOS.withNextWorkflowID(workflowID, () => regErrorWorkflowReg(user)));
186
+ expect(error).toBeInstanceOf(Error);
187
+ expect((error as Error).message).toMatch(/^test error \d+$/);
188
+
189
+ const error2 = await throws(() => DBOS.withNextWorkflowID(workflowID, () => regErrorWorkflowReg(user)));
190
+ expect(error2).toBeInstanceOf(Error);
191
+ expect((error2 as Error).message).toMatch((error as Error).message);
192
+ });
193
+
194
+ test('error dataSource.runAsTx function', async () => {
195
+ const user = 'errorTest2';
196
+
197
+ await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
198
+ await userDB.query('INSERT INTO greetings("name","greet_count") VALUES($1,10);', [user]);
199
+ const workflowID = randomUUID();
200
+
201
+ const error = await throws(() => DBOS.withNextWorkflowID(workflowID, () => regErrorWorkflowRunTx(user)));
202
+ expect(error).toBeInstanceOf(Error);
203
+ expect((error as Error).message).toMatch(/^test error \d+$/);
204
+
205
+ const { rows } = await userDB.query<greetings>('SELECT * FROM greetings WHERE name = $1', [user]);
206
+ expect(rows.length).toBe(1);
207
+ expect(rows[0].greet_count).toBe(10);
208
+
209
+ const { rows: txOutput } = await userDB.query<transaction_completion>(
210
+ 'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
211
+ [workflowID],
212
+ );
213
+ expect(txOutput.length).toBe(1);
214
+ expect(txOutput[0].workflow_id).toBe(workflowID);
215
+ expect(txOutput[0].function_num).toBe(0);
216
+ expect(txOutput[0].output).toBeNull();
217
+ expect(txOutput[0].error).not.toBeNull();
218
+ const $error = SuperJSON.parse(txOutput[0].error!);
219
+ expect($error).toBeInstanceOf(Error);
220
+ expect(($error as Error).message).toMatch(/^test error \d+$/);
221
+ });
222
+
223
+ test('rerun error dataSource.runAsTx function', async () => {
224
+ const user = 'rerunErrorTest2';
225
+
226
+ await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
227
+ await userDB.query('INSERT INTO greetings("name","greet_count") VALUES($1,10);', [user]);
228
+ const workflowID = randomUUID();
229
+
230
+ const error = await throws(() => DBOS.withNextWorkflowID(workflowID, () => regErrorWorkflowRunTx(user)));
231
+ expect(error).toBeInstanceOf(Error);
232
+ expect((error as Error).message).toMatch(/^test error \d+$/);
233
+
234
+ const error2 = await throws(() => DBOS.withNextWorkflowID(workflowID, () => regErrorWorkflowRunTx(user)));
235
+ expect(error2).toBeInstanceOf(Error);
236
+ expect((error2 as Error).message).toMatch((error as Error).message);
237
+ });
238
+
239
+ test('readonly dataSource.register function', async () => {
240
+ const user = 'readTest1';
241
+
242
+ await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
243
+ await userDB.query('INSERT INTO greetings("name","greet_count") VALUES($1,10);', [user]);
244
+
245
+ const workflowID = randomUUID();
246
+ await expect(DBOS.withNextWorkflowID(workflowID, () => regReadWorkflowReg(user))).resolves.toMatchObject({
247
+ user,
248
+ greet_count: 10,
249
+ });
250
+
251
+ const { rows } = await userDB.query('SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1', [
252
+ workflowID,
253
+ ]);
254
+ expect(rows.length).toBe(0);
255
+ });
256
+
257
+ test('readonly dataSource.runAsTx function', async () => {
258
+ const user = 'readTest2';
259
+
260
+ await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
261
+ await userDB.query('INSERT INTO greetings("name","greet_count") VALUES($1,10);', [user]);
262
+
263
+ const workflowID = randomUUID();
264
+ await expect(DBOS.withNextWorkflowID(workflowID, () => regReadWorkflowRunTx(user))).resolves.toMatchObject({
265
+ user,
266
+ greet_count: 10,
267
+ });
268
+
269
+ const { rows } = await userDB.query('SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1', [
270
+ workflowID,
271
+ ]);
272
+ expect(rows.length).toBe(0);
273
+ });
274
+
275
+ test('static dataSource.register methods', async () => {
276
+ const user = 'staticTest1';
277
+
278
+ await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
279
+
280
+ const workflowID = randomUUID();
281
+ await expect(DBOS.withNextWorkflowID(workflowID, () => regStaticWorkflow(user))).resolves.toMatchObject([
282
+ { user, greet_count: 1 },
283
+ { user, greet_count: 1 },
284
+ ]);
285
+ });
286
+
287
+ test('instance dataSource.register methods', async () => {
288
+ const user = 'instanceTest1';
289
+
290
+ await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
291
+
292
+ const workflowID = randomUUID();
293
+ await expect(DBOS.withNextWorkflowID(workflowID, () => regInstanceWorkflow(user))).resolves.toMatchObject([
294
+ { user, greet_count: 1 },
295
+ { user, greet_count: 1 },
296
+ ]);
297
+ });
298
+
299
+ test('invoke-reg-tx-fun-outside-wf', async () => {
300
+ const user = 'outsideWfUser' + Date.now();
301
+ const result = await regInsertFunction(user);
302
+ expect(result).toMatchObject({ user, greet_count: 1 });
303
+
304
+ const txResults = await userDB.query('SELECT * FROM dbos.transaction_completion WHERE output LIKE $1', [
305
+ `%${user}%`,
306
+ ]);
307
+ expect(txResults.rows.length).toBe(0);
308
+ });
309
+
310
+ test('invoke-reg-tx-static-method-outside-wf', async () => {
311
+ const user = 'outsideWfUser' + Date.now();
312
+ const result = await StaticClass.insertFunction(user);
313
+ expect(result).toMatchObject({ user, greet_count: 1 });
314
+
315
+ const txResults = await userDB.query('SELECT * FROM dbos.transaction_completion WHERE output LIKE $1', [
316
+ `%${user}%`,
317
+ ]);
318
+ expect(txResults.rows.length).toBe(0);
319
+ });
320
+
321
+ test('invoke-reg-tx-inst-method-outside-wf', async () => {
322
+ const user = 'outsideWfUser' + Date.now();
323
+ const instance = new InstanceClass();
324
+ const result = await instance.insertFunction(user);
325
+ expect(result).toMatchObject({ user, greet_count: 1 });
326
+
327
+ const txResults = await userDB.query('SELECT * FROM dbos.transaction_completion WHERE output LIKE $1', [
328
+ `%${user}%`,
329
+ ]);
330
+ expect(txResults.rows.length).toBe(0);
331
+ });
332
+ });
333
+
334
+ export interface greetings {
335
+ name: string;
336
+ greet_count: number;
337
+ }
338
+
339
+ async function insertFunction(user: string) {
340
+ const existing = await dataSource.client.dbosHello.findUnique({
341
+ where: { name: user },
342
+ select: { greet_count: true },
343
+ });
344
+
345
+ let greet_count: number;
346
+
347
+ if (!existing) {
348
+ const created = await dataSource.client.dbosHello.create({
349
+ data: { name: user, greet_count: 1 },
350
+ select: { greet_count: true },
351
+ });
352
+ greet_count = created.greet_count;
353
+ } else {
354
+ const updated = await dataSource.client.dbosHello.update({
355
+ where: { name: user },
356
+ data: { greet_count: { increment: 1 } },
357
+ select: { greet_count: true },
358
+ });
359
+ greet_count = updated.greet_count;
360
+ }
361
+
362
+ return {
363
+ user,
364
+ greet_count,
365
+ now: Date.now(),
366
+ };
367
+ }
368
+
369
+ async function errorFunction(user: string) {
370
+ const _result = await insertFunction(user);
371
+ throw new Error(`test error ${Date.now()}`);
372
+ }
373
+
374
+ async function readFunction(user: string) {
375
+ const row = await dataSource.client.dbosHello.findUnique({
376
+ where: { name: user },
377
+ select: { greet_count: true },
378
+ });
379
+
380
+ return { user, greet_count: row?.greet_count, now: Date.now() };
381
+ }
382
+
383
+ const regInsertFunction = dataSource.registerTransaction(insertFunction);
384
+ const regErrorFunction = dataSource.registerTransaction(errorFunction);
385
+ const regReadFunction = dataSource.registerTransaction(readFunction, { readOnly: true });
386
+
387
+ class StaticClass {
388
+ static async insertFunction(user: string) {
389
+ return await insertFunction(user);
390
+ }
391
+
392
+ static async readFunction(user: string) {
393
+ return await readFunction(user);
394
+ }
395
+ }
396
+
397
+ StaticClass.insertFunction = dataSource.registerTransaction(StaticClass.insertFunction);
398
+ StaticClass.readFunction = dataSource.registerTransaction(StaticClass.readFunction, { readOnly: true });
399
+
400
+ class InstanceClass {
401
+ async insertFunction(user: string) {
402
+ return await insertFunction(user);
403
+ }
404
+
405
+ async readFunction(user: string) {
406
+ return await readFunction(user);
407
+ }
408
+ }
409
+
410
+ InstanceClass.prototype.insertFunction = dataSource.registerTransaction(
411
+ // eslint-disable-next-line @typescript-eslint/unbound-method
412
+ InstanceClass.prototype.insertFunction,
413
+ );
414
+ InstanceClass.prototype.readFunction = dataSource.registerTransaction(
415
+ // eslint-disable-next-line @typescript-eslint/unbound-method
416
+ InstanceClass.prototype.readFunction,
417
+ { readOnly: true },
418
+ );
419
+
420
+ async function insertWorkflowReg(user: string) {
421
+ return await regInsertFunction(user);
422
+ }
423
+
424
+ async function insertWorkflowRunTx(user: string) {
425
+ return await dataSource.runTransaction(() => insertFunction(user), { name: 'insertFunction' });
426
+ }
427
+
428
+ async function errorWorkflowReg(user: string) {
429
+ return await regErrorFunction(user);
430
+ }
431
+
432
+ async function errorWorkflowRunTx(user: string) {
433
+ return await dataSource.runTransaction(() => errorFunction(user), { name: 'errorFunction' });
434
+ }
435
+
436
+ async function readWorkflowReg(user: string) {
437
+ return await regReadFunction(user);
438
+ }
439
+
440
+ async function readWorkflowRunTx(user: string) {
441
+ return await dataSource.runTransaction(() => readFunction(user), { name: 'readFunction', readOnly: true });
442
+ }
443
+
444
+ async function staticWorkflow(user: string) {
445
+ const result = await StaticClass.insertFunction(user);
446
+ const readResult = await StaticClass.readFunction(user);
447
+ return [result, readResult];
448
+ }
449
+
450
+ async function instanceWorkflow(user: string) {
451
+ const instance = new InstanceClass();
452
+ const result = await instance.insertFunction(user);
453
+ const readResult = await instance.readFunction(user);
454
+ return [result, readResult];
455
+ }
456
+
457
+ const regInsertWorkflowReg = DBOS.registerWorkflow(insertWorkflowReg);
458
+ const regInsertWorkflowRunTx = DBOS.registerWorkflow(insertWorkflowRunTx);
459
+ const regErrorWorkflowReg = DBOS.registerWorkflow(errorWorkflowReg);
460
+ const regErrorWorkflowRunTx = DBOS.registerWorkflow(errorWorkflowRunTx);
461
+ const regReadWorkflowReg = DBOS.registerWorkflow(readWorkflowReg);
462
+ const regReadWorkflowRunTx = DBOS.registerWorkflow(readWorkflowRunTx);
463
+ const regStaticWorkflow = DBOS.registerWorkflow(staticWorkflow);
464
+ const regInstanceWorkflow = DBOS.registerWorkflow(instanceWorkflow);
@@ -0,0 +1,17 @@
1
+ // This is your Prisma schema file,
2
+ // learn more about it in the docs: https://pris.ly/d/prisma-schema
3
+
4
+ generator client {
5
+ provider = "prisma-client-js"
6
+ }
7
+
8
+ datasource db {
9
+ provider = "postgresql"
10
+ url = env("DATABASE_URL")
11
+ }
12
+
13
+ model DbosHello {
14
+ @@map("greetings")
15
+ name String @id
16
+ greet_count Int
17
+ }
@@ -0,0 +1,13 @@
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, force: boolean = false) {
11
+ const withForce = force ? ' WITH (FORCE)' : '';
12
+ await client.query(`DROP DATABASE IF EXISTS ${name} ${withForce}`);
13
+ }
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
+ }