@dbos-inc/drizzle-datasource 5.3.4-preview → 5.3.6-preview

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbos-inc/drizzle-datasource",
3
- "version": "5.3.4-preview",
3
+ "version": "5.3.6-preview",
4
4
  "description": "DBOS DataSource library for Drizzle ORM with PostgreSQL support",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
package/index.ts DELETED
@@ -1,368 +0,0 @@
1
- import { Client, ClientConfig, Pool, PoolClient, PoolConfig } from 'pg';
2
- import { DBOS, FunctionName } from '@dbos-inc/dbos-sdk';
3
- import {
4
- type DataSourceTransactionHandler,
5
- isPGRetriableTransactionError,
6
- DBOSError,
7
- DBOSStepAlreadyRecordedError,
8
- DBOSWorkflowConflictError,
9
- assertStillOwnsWorkflow,
10
- replayRecordedStep,
11
- registerTransaction,
12
- runTransaction,
13
- DBOSDataSource,
14
- registerDataSource,
15
- DataSourceMigrationOptions,
16
- DataSourceSQLExecutor,
17
- initializeDataSourceSchemaPG,
18
- migrateDataSourcePG,
19
- verifyDataSourcePG,
20
- } from '@dbos-inc/dbos-sdk/datasource';
21
- import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres';
22
- import { AsyncLocalStorage } from 'async_hooks';
23
- import { SuperJSON } from 'superjson';
24
- import { PgTransactionConfig } from 'drizzle-orm/pg-core';
25
- import { sql } from 'drizzle-orm';
26
-
27
- interface DrizzleLocalCtx {
28
- client: NodePgDatabase<{ [key: string]: object }>;
29
- owner: DrizzleTransactionHandler;
30
- }
31
-
32
- export type TransactionConfig = Pick<PgTransactionConfig, 'isolationLevel' | 'accessMode'> & { name?: string };
33
-
34
- const asyncLocalCtx = new AsyncLocalStorage<DrizzleLocalCtx>();
35
-
36
- function drizzleExecutor(db: Pick<NodePgDatabase<{ [key: string]: object }>, 'execute'>): DataSourceSQLExecutor {
37
- return async (text) => (await db.execute<Record<string, unknown>>(sql.raw(text))).rows;
38
- }
39
-
40
- function clientExecutor(client: Pick<Client, 'query'>): DataSourceSQLExecutor {
41
- return async (text) => (await client.query<Record<string, unknown>>(text)).rows;
42
- }
43
-
44
- export interface transaction_completion {
45
- workflow_id: string;
46
- function_num: number;
47
- output: string | null;
48
- error: string | null;
49
- }
50
-
51
- interface DrizzleConnection {
52
- readonly db: NodePgDatabase<{ [key: string]: object }>;
53
- end(): Promise<void>;
54
- }
55
-
56
- class DrizzleTransactionHandler implements DataSourceTransactionHandler {
57
- #connection: DrizzleConnection | undefined;
58
- readonly schemaName: string;
59
- readonly #userProvidedPool: boolean;
60
-
61
- constructor(
62
- readonly name: string,
63
- private readonly configOrPool: PoolConfig | Pool,
64
- private readonly entities: { [key: string]: object } = {},
65
- schemaName: string = 'dbos',
66
- private readonly options: DataSourceMigrationOptions = {},
67
- ) {
68
- this.schemaName = schemaName;
69
- this.#userProvidedPool = configOrPool instanceof Pool;
70
- }
71
-
72
- async initialize(): Promise<void> {
73
- const conn = this.#connection;
74
-
75
- const driver = this.configOrPool instanceof Pool ? this.configOrPool : new Pool(this.configOrPool);
76
- const db = drizzle({ client: driver, schema: this.entities });
77
- this.#connection = { db, end: this.#userProvidedPool ? async () => {} : () => driver.end() };
78
- await conn?.end();
79
-
80
- if (this.options.runMigrations === false) {
81
- await verifyDataSourcePG(drizzleExecutor(db), this.schemaName, this.name);
82
- return;
83
- }
84
-
85
- try {
86
- await db.transaction((tx) => migrateDataSourcePG(drizzleExecutor(tx), this.schemaName));
87
- } catch (err) {
88
- throw new Error(
89
- `In initialization of 'DrizzleDataSource' ${this.name}: The '${this.schemaName}' transaction schema could not be migrated: ${(err as Error).message}. This should be added to your database migrations.
90
- See: https://docs.dbos.dev/typescript/tutorials/transaction-tutorial#installing-the-dbos-schema`,
91
- );
92
- }
93
- }
94
-
95
- async destroy(): Promise<void> {
96
- const conn = this.#connection;
97
-
98
- this.#connection = undefined;
99
-
100
- await conn?.end();
101
- }
102
-
103
- get #drizzle(): NodePgDatabase<{ [key: string]: object }> {
104
- if (!this.#connection) {
105
- throw new Error(`DataSource ${this.name} is not initialized.`);
106
- }
107
- return this.#connection.db;
108
- }
109
-
110
- async deleteCheckpoints(workflowID: string, startStep: number, beforeCommit?: () => Promise<void>): Promise<void> {
111
- await this.#drizzle.transaction(async (client) => {
112
- await client.execute(sql`
113
- DELETE FROM ${sql.identifier(this.schemaName)}.transaction_completion
114
- WHERE workflow_id = ${workflowID} AND function_num >= ${startStep}`);
115
- await beforeCommit?.();
116
- });
117
- }
118
-
119
- async #checkExecution(
120
- workflowID: string,
121
- stepID: number,
122
- ): Promise<{ output: string | null } | { error: string } | undefined> {
123
- type Result = { output: string | null; error: string | null };
124
-
125
- const statement = sql`
126
- SELECT output, error FROM ${sql.identifier(this.schemaName)}.transaction_completion
127
- WHERE workflow_id = ${workflowID} AND function_num = ${stepID}`;
128
- const result = await this.#drizzle.execute<Result>(statement);
129
-
130
- if (result.rows.length !== 1) {
131
- return undefined;
132
- }
133
-
134
- const { output, error } = result.rows[0];
135
- return error !== null ? { error } : { output };
136
- }
137
-
138
- static async #recordOutput(
139
- client: NodePgDatabase<{ [key: string]: object }>,
140
- workflowID: string,
141
- stepID: number,
142
- output: string,
143
- schemaName: string,
144
- ): Promise<void> {
145
- const statement = sql`
146
- INSERT INTO ${sql.identifier(schemaName)}.transaction_completion (workflow_id, function_num, output)
147
- VALUES (${workflowID}, ${stepID}, ${output})
148
- ON CONFLICT (workflow_id, function_num) DO NOTHING
149
- RETURNING workflow_id`;
150
- const { rows } = await client.execute(statement);
151
- if (rows.length === 0) {
152
- throw new DBOSStepAlreadyRecordedError(workflowID, stepID);
153
- }
154
- // Holding this step's row, so a later owner's insert waits on our commit.
155
- await assertStillOwnsWorkflow(workflowID);
156
- }
157
-
158
- async #recordError(workflowID: string, stepID: number, error: string): Promise<void> {
159
- const statement = sql`
160
- INSERT INTO ${sql.identifier(this.schemaName)}.transaction_completion (workflow_id, function_num, error)
161
- VALUES (${workflowID}, ${stepID}, ${error})
162
- ON CONFLICT (workflow_id, function_num) DO NOTHING
163
- RETURNING workflow_id`;
164
- const { rows } = await this.#drizzle.execute(statement);
165
- if (rows.length === 0) {
166
- throw new DBOSStepAlreadyRecordedError(workflowID, stepID);
167
- }
168
- }
169
-
170
- // A duplicate execution won the race, so its recorded outcome is the durable one.
171
- async #replayConflictingStep<Return>(workflowID: string, stepID: number): Promise<Return> {
172
- const recorded = await this.#checkExecution(workflowID, stepID);
173
- if (recorded === undefined) {
174
- throw new DBOSError(
175
- `Step ${stepID} of workflow ${workflowID} conflicted with a concurrent execution, but no recorded outcome was found`,
176
- );
177
- }
178
- return replayRecordedStep<Return>(recorded);
179
- }
180
-
181
- /* Invoke a transaction function, called by the framework */
182
- async invokeTransactionFunction<This, Args extends unknown[], Return>(
183
- config: TransactionConfig | undefined,
184
- target: This,
185
- func: (this: This, ...args: Args) => Promise<Return>,
186
- ...args: Args
187
- ): Promise<Return> {
188
- const workflowID = DBOS.workflowID;
189
- const stepID = DBOS.stepID;
190
- if (workflowID !== undefined && stepID === undefined) {
191
- throw new Error('DBOS.stepID is undefined inside a workflow.');
192
- }
193
-
194
- const readOnly = config?.accessMode === 'read only' ? true : false;
195
- const saveResults = !readOnly && workflowID !== undefined;
196
-
197
- // Retry loop if appropriate
198
- let retryWaitMS = 1;
199
- const backoffFactor = 1.5;
200
- const maxRetryWaitMS = 2000; // Maximum wait 2 seconds.
201
-
202
- while (true) {
203
- // Check to see if this tx has already been executed
204
- const previousResult = saveResults ? await this.#checkExecution(workflowID, stepID!) : undefined;
205
- if (previousResult) {
206
- return replayRecordedStep<Return>(previousResult);
207
- }
208
-
209
- try {
210
- const result = await this.#drizzle.transaction(
211
- async (client) => {
212
- // execute user's transaction function
213
- const result = await asyncLocalCtx.run({ client, owner: this }, async () => {
214
- return await func.call(target, ...args);
215
- });
216
-
217
- // save the output of read/write transactions
218
- if (saveResults) {
219
- await DrizzleTransactionHandler.#recordOutput(
220
- client,
221
- workflowID,
222
- stepID!,
223
- SuperJSON.stringify(result),
224
- this.schemaName,
225
- );
226
- }
227
-
228
- return result;
229
- },
230
- { accessMode: config?.accessMode, isolationLevel: config?.isolationLevel },
231
- );
232
-
233
- return result;
234
- } catch (error) {
235
- if (saveResults && error instanceof DBOSStepAlreadyRecordedError) {
236
- return await this.#replayConflictingStep<Return>(workflowID, stepID!);
237
- }
238
- // The new owner wins; recording an error here would replay it as this step's outcome.
239
- if (error instanceof DBOSWorkflowConflictError) throw error;
240
- if (isPGRetriableTransactionError(error)) {
241
- DBOS.span?.addEvent('TXN SERIALIZATION FAILURE', { retryWaitMillis: retryWaitMS }, performance.now());
242
- // Retry serialization failures.
243
- await new Promise((resolve) => setTimeout(resolve, retryWaitMS));
244
- retryWaitMS = Math.min(retryWaitMS * backoffFactor, maxRetryWaitMS);
245
- continue;
246
- } else {
247
- if (saveResults) {
248
- const message = SuperJSON.stringify(error);
249
- try {
250
- await this.#recordError(workflowID, stepID!, message);
251
- } catch (recordError) {
252
- if (recordError instanceof DBOSStepAlreadyRecordedError) {
253
- return await this.#replayConflictingStep<Return>(workflowID, stepID!);
254
- }
255
- throw recordError;
256
- }
257
- }
258
-
259
- throw error;
260
- }
261
- }
262
- }
263
- }
264
- }
265
-
266
- export class DrizzleDataSource<CT = NodePgDatabase<{ [key: string]: object }>>
267
- implements DBOSDataSource<TransactionConfig>
268
- {
269
- // User calls this... DBOS not directly involved...
270
- static #getClient(p?: DrizzleTransactionHandler): NodePgDatabase<{ [key: string]: object }> {
271
- if (!DBOS.isInTransaction()) {
272
- throw new Error('Invalid use of DrizzleDataSource.client outside of a DBOS transaction');
273
- }
274
- const ctx = asyncLocalCtx.getStore();
275
- if (!ctx) {
276
- throw new Error('Invalid use of DrizzleDataSource.client outside of a DBOS transaction');
277
- }
278
- if (p && p !== ctx.owner) {
279
- throw new Error('Invalid retrieval of `DrizzleDataSource.client` from the incorrect object');
280
- }
281
- return ctx.client;
282
- }
283
-
284
- static get client() {
285
- return DrizzleDataSource.#getClient(undefined);
286
- }
287
-
288
- get client() {
289
- return DrizzleDataSource.#getClient(this.#provider) as CT;
290
- }
291
-
292
- /**
293
- * Create or migrate the DBOS transaction schema, typically with a privileged role,
294
- * optionally granting `applicationRole` the minimal permissions to use it.
295
- * A provided client runs the statements in its current transaction, if any; a config gets a fresh transaction.
296
- */
297
- static async initializeDBOSSchema(
298
- configOrClient: ClientConfig | Client | PoolClient,
299
- schemaName: string = 'dbos',
300
- options: { applicationRole?: string } = {},
301
- ): Promise<void> {
302
- if (typeof configOrClient === 'object' && 'query' in configOrClient) {
303
- await initializeDataSourceSchemaPG(clientExecutor(configOrClient), schemaName, options.applicationRole);
304
- } else {
305
- const client = new Client(configOrClient);
306
- try {
307
- await client.connect();
308
- await client.query('BEGIN');
309
- try {
310
- await initializeDataSourceSchemaPG(clientExecutor(client), schemaName, options.applicationRole);
311
- await client.query('COMMIT');
312
- } catch (err) {
313
- await client.query('ROLLBACK');
314
- throw err;
315
- }
316
- } finally {
317
- await client.end();
318
- }
319
- }
320
- }
321
-
322
- #provider: DrizzleTransactionHandler;
323
-
324
- constructor(
325
- readonly name: string,
326
- configOrPool: PoolConfig | Pool,
327
- entities: { [key: string]: object } = {},
328
- schemaName: string = 'dbos',
329
- options: DataSourceMigrationOptions = {},
330
- ) {
331
- this.#provider = new DrizzleTransactionHandler(name, configOrPool, entities, schemaName, options);
332
- registerDataSource(this.#provider);
333
- }
334
-
335
- async runTransaction<T>(func: () => Promise<T>, config?: TransactionConfig) {
336
- return await runTransaction(func, config?.name ?? func.name, { dsName: this.name, config });
337
- }
338
-
339
- registerTransaction<This, Args extends unknown[], Return>(
340
- func: (this: This, ...args: Args) => Promise<Return>,
341
- config?: TransactionConfig & FunctionName,
342
- ): (this: This, ...args: Args) => Promise<Return> {
343
- return registerTransaction(this.name, func, config);
344
- }
345
-
346
- // decorator
347
- transaction(config?: TransactionConfig) {
348
- // eslint-disable-next-line @typescript-eslint/no-this-alias
349
- const ds = this;
350
- return function decorator<This, Args extends unknown[], Return>(
351
- target: object,
352
- propertyKey: PropertyKey,
353
- descriptor: TypedPropertyDescriptor<(this: This, ...args: Args) => Promise<Return>>,
354
- ) {
355
- if (!descriptor.value) {
356
- throw new Error('Use of decorator when original method is undefined');
357
- }
358
-
359
- descriptor.value = ds.registerTransaction(descriptor.value, {
360
- ...config,
361
- ctorOrProto: target,
362
- name: config?.name ?? String(propertyKey),
363
- });
364
-
365
- return descriptor;
366
- };
367
- }
368
- }
package/jest.config.js DELETED
@@ -1,8 +0,0 @@
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
- };
@@ -1,31 +0,0 @@
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
- });
@@ -1,130 +0,0 @@
1
- import { DBOS } from '@dbos-inc/dbos-sdk';
2
- import { Client, Pool } from 'pg';
3
- import { DrizzleDataSource } from '..';
4
- import { dropDB, ensureDB } from './test-helpers';
5
- import { randomUUID } from 'crypto';
6
- import { pgTable, text, integer } from 'drizzle-orm/pg-core';
7
- import { sql } from 'drizzle-orm';
8
-
9
- const config = { user: 'postgres', database: 'drizzle_pool_test_userdb' };
10
-
11
- const greetingsTable = pgTable('greetings', {
12
- name: text('name').primaryKey().notNull(),
13
- greet_count: integer('greet_count').default(0),
14
- });
15
-
16
- const customPool = new Pool(config);
17
- const poolDataSource = new DrizzleDataSource('pool-db', customPool);
18
-
19
- async function insertFunction(user: string) {
20
- const result = await poolDataSource.client
21
- .insert(greetingsTable)
22
- .values({ name: user, greet_count: 1 })
23
- .onConflictDoUpdate({
24
- target: greetingsTable.name,
25
- set: {
26
- greet_count: sql`${greetingsTable.greet_count} + 1`,
27
- },
28
- })
29
- .returning({ greet_count: greetingsTable.greet_count });
30
- const row = result.length > 0 ? result[0] : undefined;
31
- return { user, greet_count: row?.greet_count };
32
- }
33
-
34
- const regInsertFunction = poolDataSource.registerTransaction(insertFunction);
35
-
36
- async function insertWorkflow(user: string) {
37
- return await regInsertFunction(user);
38
- }
39
-
40
- const regInsertWorkflow = DBOS.registerWorkflow(insertWorkflow);
41
-
42
- async function currentDatabaseFunction() {
43
- const result = await poolDataSource.client.execute<{ current_database: string }>(sql`SELECT current_database()`);
44
- return result.rows[0].current_database;
45
- }
46
-
47
- const regCurrentDatabaseFunction = poolDataSource.registerTransaction(currentDatabaseFunction);
48
-
49
- interface transaction_completion {
50
- workflow_id: string;
51
- function_num: number;
52
- output: string | null;
53
- error: string | null;
54
- }
55
-
56
- describe('DrizzleDataSource with custom Pool', () => {
57
- beforeAll(async () => {
58
- const client = new Client({ ...config, database: 'postgres' });
59
- try {
60
- await client.connect();
61
- await dropDB(client, 'drizzle_pool_test', true);
62
- await dropDB(client, 'drizzle_pool_test_dbos_sys', true);
63
- await dropDB(client, config.database, true);
64
- await ensureDB(client, config.database);
65
- } finally {
66
- await client.end();
67
- }
68
-
69
- const poolClient = await customPool.connect();
70
- try {
71
- await DrizzleDataSource.initializeDBOSSchema(poolClient);
72
- } finally {
73
- poolClient.release();
74
- }
75
-
76
- await customPool.query(
77
- `CREATE TABLE IF NOT EXISTS greetings (name TEXT PRIMARY KEY NOT NULL, greet_count INTEGER DEFAULT 0)`,
78
- );
79
- });
80
-
81
- afterAll(async () => {
82
- await customPool.end();
83
- });
84
-
85
- beforeEach(async () => {
86
- DBOS.setConfig({ name: 'drizzle-pool-test' });
87
- await DBOS.launch();
88
- });
89
-
90
- afterEach(async () => {
91
- await DBOS.shutdown();
92
- });
93
-
94
- test('insert with custom pool', async () => {
95
- const user = 'poolTest1';
96
- await customPool.query('DELETE FROM greetings WHERE name = $1', [user]);
97
- const workflowID = randomUUID();
98
-
99
- await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflow(user))).resolves.toMatchObject({
100
- user,
101
- greet_count: 1,
102
- });
103
-
104
- const { rows } = await customPool.query<transaction_completion>(
105
- 'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
106
- [workflowID],
107
- );
108
- // Completion cleared the checkpoint the transaction wrote.
109
- expect(rows).toHaveLength(0);
110
- });
111
-
112
- test('transaction runs against the custom pool database', async () => {
113
- // Guards against the datasource creating its own connection to the default database
114
- // instead of using the caller's pool (drizzle-orm 1.0 positional-overload regression).
115
- const dbName = await regCurrentDatabaseFunction();
116
- expect(dbName).toBe(config.database);
117
- });
118
-
119
- test('custom pool is not closed after DBOS shutdown', async () => {
120
- // Explicitly shut down DBOS to trigger destroy() on all datasources
121
- await DBOS.shutdown();
122
- // The custom pool should still be usable after DBOS.shutdown()
123
- // because DrizzleDataSource should not close a user-provided pool
124
- const { rows } = await customPool.query<{ val: number }>('SELECT 1 as val');
125
- expect(rows[0].val).toBe(1);
126
- // Re-launch so the afterEach shutdown doesn't fail
127
- DBOS.setConfig({ name: 'drizzle-pool-test' });
128
- await DBOS.launch();
129
- });
130
- });
@@ -1,754 +0,0 @@
1
- import { DBOS, DBOSWorkflowConflictError } from '@dbos-inc/dbos-sdk';
2
- import { Client, Pool, PoolConfig } from 'pg';
3
- import { DrizzleDataSource } from '..';
4
- import { dropDB, ensureDB } from './test-helpers';
5
- import { randomUUID } from 'crypto';
6
- import SuperJSON from 'superjson';
7
- import { pgTable, text, integer } from 'drizzle-orm/pg-core';
8
- import { eq, sql } from 'drizzle-orm';
9
-
10
- const config = { user: 'postgres', database: 'drizzle_ds_test_userdb' };
11
- const dataSource = new DrizzleDataSource('app-db', config);
12
-
13
- interface transaction_completion {
14
- workflow_id: string;
15
- function_num: number;
16
- output: string | null;
17
- error: string | null;
18
- }
19
-
20
- async function createSchema(config: PoolConfig) {
21
- const pool = new Pool(config);
22
- try {
23
- await pool.query(
24
- `CREATE TABLE IF NOT EXISTS greetings (name TEXT PRIMARY KEY NOT NULL, greet_count INTEGER DEFAULT 0)`,
25
- );
26
- await pool.query(`CREATE TABLE IF NOT EXISTS race_side_effects (tag TEXT NOT NULL)`);
27
- } finally {
28
- await pool.end();
29
- }
30
- }
31
-
32
- describe('DrizzleDataSource', () => {
33
- const userDB = new Pool(config);
34
-
35
- beforeAll(async () => {
36
- await createDatabases(true);
37
- });
38
-
39
- afterAll(async () => {
40
- await userDB.end();
41
- });
42
-
43
- beforeEach(async () => {
44
- DBOS.setConfig({ name: 'drizzle-ds-test' });
45
- await DBOS.launch();
46
- });
47
-
48
- afterEach(async () => {
49
- await DBOS.shutdown();
50
- });
51
-
52
- test('insert dataSource.register function', async () => {
53
- const user = 'helloTest1';
54
-
55
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
56
- const workflowID = randomUUID();
57
-
58
- await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowReg(user))).resolves.toMatchObject({
59
- user,
60
- greet_count: 1,
61
- });
62
-
63
- const { rows } = await userDB.query<transaction_completion>(
64
- 'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
65
- [workflowID],
66
- );
67
- // Completion cleared the checkpoint the transaction wrote.
68
- expect(rows).toHaveLength(0);
69
- });
70
-
71
- test('rerun insert dataSource.register function', async () => {
72
- const user = 'rerunTest1';
73
-
74
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
75
- const workflowID = randomUUID();
76
-
77
- const result = await DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowReg(user));
78
- expect(result).toMatchObject({ user, greet_count: 1 });
79
-
80
- await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowReg(user))).resolves.toMatchObject(result);
81
- });
82
-
83
- test('insert dataSource.runAsTx function', async () => {
84
- const user = 'helloTest2';
85
-
86
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
87
- const workflowID = randomUUID();
88
-
89
- await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowRunTx(user))).resolves.toMatchObject({
90
- user,
91
- greet_count: 1,
92
- });
93
-
94
- const { rows } = await userDB.query<transaction_completion>(
95
- 'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
96
- [workflowID],
97
- );
98
- // Completion cleared the checkpoint the transaction wrote.
99
- expect(rows).toHaveLength(0);
100
- });
101
-
102
- test('rerun insert dataSource.runAsTx function', async () => {
103
- const user = 'rerunTest2';
104
-
105
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
106
- const workflowID = randomUUID();
107
-
108
- const result = await DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowRunTx(user));
109
- expect(result).toMatchObject({ user, greet_count: 1 });
110
-
111
- await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowRunTx(user))).resolves.toMatchObject(
112
- result,
113
- );
114
- });
115
-
116
- async function throws<R>(func: () => Promise<R>): Promise<unknown> {
117
- try {
118
- await func();
119
- fail('Expected function to throw an error');
120
- } catch (error) {
121
- return error;
122
- }
123
- }
124
-
125
- test('error dataSource.register function', async () => {
126
- const user = 'errorTest1';
127
-
128
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
129
- await userDB.query('INSERT INTO greetings("name","greet_count") VALUES($1,10);', [user]);
130
- const workflowID = randomUUID();
131
-
132
- const error = await throws(() => DBOS.withNextWorkflowID(workflowID, () => regErrorWorkflowReg(user)));
133
- expect(error).toBeInstanceOf(Error);
134
- expect((error as Error).message).toMatch(/^test error \d+$/);
135
-
136
- interface greetings {
137
- name: string;
138
- greet_count: number;
139
- }
140
- const { rows } = await userDB.query<greetings>('SELECT * FROM greetings WHERE name = $1', [user]);
141
- expect(rows.length).toBe(1);
142
- expect(rows[0].greet_count).toBe(10);
143
-
144
- const { rows: txOutput } = await userDB.query<transaction_completion>(
145
- 'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
146
- [workflowID],
147
- );
148
- // Completion cleared the checkpoint the transaction wrote.
149
- expect(txOutput).toHaveLength(0);
150
- });
151
-
152
- test('rerun error dataSource.register function', async () => {
153
- const user = 'rerunErrorTest1';
154
-
155
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
156
- await userDB.query('INSERT INTO greetings("name","greet_count") VALUES($1,10);', [user]);
157
- const workflowID = randomUUID();
158
-
159
- const error = await throws(() => DBOS.withNextWorkflowID(workflowID, () => regErrorWorkflowReg(user)));
160
- expect(error).toBeInstanceOf(Error);
161
- expect((error as Error).message).toMatch(/^test error \d+$/);
162
-
163
- const error2 = await throws(() => DBOS.withNextWorkflowID(workflowID, () => regErrorWorkflowReg(user)));
164
- expect(error2).toBeInstanceOf(Error);
165
- expect((error2 as Error).message).toMatch((error as Error).message);
166
- });
167
-
168
- test('error dataSource.runAsTx function', async () => {
169
- const user = 'errorTest2';
170
-
171
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
172
- await userDB.query('INSERT INTO greetings("name","greet_count") VALUES($1,10);', [user]);
173
- const workflowID = randomUUID();
174
-
175
- const error = await throws(() => DBOS.withNextWorkflowID(workflowID, () => regErrorWorkflowRunTx(user)));
176
- expect(error).toBeInstanceOf(Error);
177
- expect((error as Error).message).toMatch(/^test error \d+$/);
178
-
179
- interface greetings {
180
- name: string;
181
- greet_count: number;
182
- }
183
- const { rows } = await userDB.query<greetings>('SELECT * FROM greetings WHERE name = $1', [user]);
184
- expect(rows.length).toBe(1);
185
- expect(rows[0].greet_count).toBe(10);
186
-
187
- const { rows: txOutput } = await userDB.query<transaction_completion>(
188
- 'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
189
- [workflowID],
190
- );
191
- // Completion cleared the checkpoint the transaction wrote.
192
- expect(txOutput).toHaveLength(0);
193
- });
194
-
195
- test('rerun error dataSource.runAsTx function', async () => {
196
- const user = 'rerunErrorTest2';
197
-
198
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
199
- await userDB.query('INSERT INTO greetings("name","greet_count") VALUES($1,10);', [user]);
200
- const workflowID = randomUUID();
201
-
202
- const error = await throws(() => DBOS.withNextWorkflowID(workflowID, () => regErrorWorkflowRunTx(user)));
203
- expect(error).toBeInstanceOf(Error);
204
- expect((error as Error).message).toMatch(/^test error \d+$/);
205
-
206
- const error2 = await throws(() => DBOS.withNextWorkflowID(workflowID, () => regErrorWorkflowRunTx(user)));
207
- expect(error2).toBeInstanceOf(Error);
208
- expect((error2 as Error).message).toMatch((error as Error).message);
209
- });
210
-
211
- test('readonly dataSource.register function', async () => {
212
- const user = 'readTest1';
213
-
214
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
215
- await userDB.query('INSERT INTO greetings("name","greet_count") VALUES($1,10);', [user]);
216
-
217
- const workflowID = randomUUID();
218
- await expect(DBOS.withNextWorkflowID(workflowID, () => regReadWorkflowReg(user))).resolves.toMatchObject({
219
- user,
220
- greet_count: 10,
221
- });
222
-
223
- const { rows } = await userDB.query('SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1', [
224
- workflowID,
225
- ]);
226
- expect(rows.length).toBe(0);
227
- });
228
-
229
- test('readonly dataSource.runAsTx function', async () => {
230
- const user = 'readTest2';
231
-
232
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
233
- await userDB.query('INSERT INTO greetings("name","greet_count") VALUES($1,10);', [user]);
234
-
235
- const workflowID = randomUUID();
236
- await expect(DBOS.withNextWorkflowID(workflowID, () => regReadWorkflowRunTx(user))).resolves.toMatchObject({
237
- user,
238
- greet_count: 10,
239
- });
240
-
241
- const { rows } = await userDB.query('SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1', [
242
- workflowID,
243
- ]);
244
- expect(rows.length).toBe(0);
245
- });
246
-
247
- test('static dataSource.register methods', async () => {
248
- const user = 'staticTest1';
249
-
250
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
251
-
252
- const workflowID = randomUUID();
253
- await expect(DBOS.withNextWorkflowID(workflowID, () => regStaticWorkflow(user))).resolves.toMatchObject([
254
- { user, greet_count: 1 },
255
- { user, greet_count: 1 },
256
- ]);
257
- });
258
-
259
- test('instance dataSource.register methods', async () => {
260
- const user = 'instanceTest1';
261
-
262
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
263
-
264
- const workflowID = randomUUID();
265
- await expect(DBOS.withNextWorkflowID(workflowID, () => regInstanceWorkflow(user))).resolves.toMatchObject([
266
- { user, greet_count: 1 },
267
- { user, greet_count: 1 },
268
- ]);
269
- });
270
-
271
- test('invoke-reg-tx-fun-outside-wf', async () => {
272
- const user = 'outsideWfUser' + Date.now();
273
- const result = await regInsertFunction(user);
274
- expect(result).toMatchObject({ user, greet_count: 1 });
275
-
276
- const txResults = await userDB.query('SELECT * FROM dbos.transaction_completion WHERE output LIKE $1', [
277
- `%${user}%`,
278
- ]);
279
- expect(txResults.rows.length).toBe(0);
280
- });
281
-
282
- test('invoke-reg-tx-static-method-outside-wf', async () => {
283
- const user = 'outsideWfUser' + Date.now();
284
- const result = await StaticClass.insertFunction(user);
285
- expect(result).toMatchObject({ user, greet_count: 1 });
286
-
287
- const txResults = await userDB.query('SELECT * FROM dbos.transaction_completion WHERE output LIKE $1', [
288
- `%${user}%`,
289
- ]);
290
- expect(txResults.rows.length).toBe(0);
291
- });
292
-
293
- test('invoke-reg-tx-inst-method-outside-wf', async () => {
294
- const user = 'outsideWfUser' + Date.now();
295
- const instance = new InstanceClass();
296
- const result = await instance.insertFunction(user);
297
- expect(result).toMatchObject({ user, greet_count: 1 });
298
-
299
- const txResults = await userDB.query('SELECT * FROM dbos.transaction_completion WHERE output LIKE $1', [
300
- `%${user}%`,
301
- ]);
302
- expect(txResults.rows.length).toBe(0);
303
- });
304
-
305
- test('duplicate execution replays the winner result', async () => {
306
- await userDB.query('DELETE FROM race_side_effects');
307
- raceState.callCount = 0;
308
-
309
- // A loser whose body succeeds: the collision happens on the result-recording insert.
310
- raceState.plantWinner = true;
311
- raceState.plantWinnerError = false;
312
- raceState.fail = false;
313
- const wfid1 = randomUUID();
314
- await expect(DBOS.withNextWorkflowID(wfid1, () => regRaceWorkflow())).resolves.toBe('winner-result');
315
- expect(raceState.callCount).toBe(1); // the loser did run its body
316
-
317
- // A loser whose body fails: the collision moves to the error-recording insert,
318
- // and the winner's result still wins over the loser's own error.
319
- raceState.plantWinner = true;
320
- raceState.plantWinnerError = false;
321
- raceState.fail = true;
322
- const wfid2 = randomUUID();
323
- await expect(DBOS.withNextWorkflowID(wfid2, () => regRaceWorkflow())).resolves.toBe('winner-result');
324
- expect(raceState.callCount).toBe(2);
325
-
326
- // A winner that recorded an error: the loser replays that throw instead of its own result.
327
- raceState.plantWinner = true;
328
- raceState.plantWinnerError = true;
329
- raceState.fail = false;
330
- const wfid3 = randomUUID();
331
- const winnerError = await throws(() => DBOS.withNextWorkflowID(wfid3, () => regRaceWorkflow()));
332
- expect((winnerError as Error).message).toBe('winner-error');
333
- expect(raceState.callCount).toBe(3);
334
-
335
- // Every loser's writes were discarded, and completion cleared the winners' checkpoints.
336
- const { rows: tags } = await userDB.query<{ tag: string }>('SELECT tag FROM race_side_effects');
337
- expect(tags).toHaveLength(0);
338
- const { rows: txOutput } = await userDB.query<transaction_completion>(
339
- 'SELECT * FROM dbos.transaction_completion WHERE workflow_id = ANY($1)',
340
- [[wfid1, wfid2, wfid3]],
341
- );
342
- expect(txOutput).toHaveLength(0);
343
- });
344
-
345
- /** Completion clears the checkpoints, so the workflow reads them back while it still runs. */
346
- test('transactions record their outcomes as checkpoints', async () => {
347
- const user = 'checkpointProbe';
348
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
349
- const workflowID = randomUUID();
350
- const message = await DBOS.withNextWorkflowID(workflowID, () => regCheckpointProbeWorkflow(user));
351
- expect(message).toMatch(/^test error \d+$/);
352
-
353
- expect(probeState.rows).toHaveLength(2);
354
- const [output, error] = probeState.rows;
355
- expect(output).toMatchObject({ workflow_id: workflowID, function_num: 0, error: null });
356
- expect(SuperJSON.parse(output.output!)).toMatchObject({ user, greet_count: 1 });
357
- expect(error).toMatchObject({ workflow_id: workflowID, function_num: 1, output: null });
358
- expect(SuperJSON.parse<Error>(error.error!).message).toBe(message);
359
- expect(await readCompletions(workflowID)).toHaveLength(0);
360
- });
361
-
362
- /** The checkpoint may be the new owner's only record of the transaction, so a stale execution's cleanup rolls back. */
363
- test('a stale execution keeps the checkpoints at completion', async () => {
364
- const user = 'handOffTest';
365
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
366
- const handedOff = new Promise<void>((resolve) => (handOffState.signal = resolve));
367
- const workflowID = randomUUID();
368
- const execution = DBOS.withNextWorkflowID(workflowID, () => regHandOffWorkflow(user));
369
- // Surfaces the workflow's own error if it fails before handing off.
370
- await Promise.race([handedOff, execution]);
371
-
372
- // Its outcome is refused too, so the execution parks until the workflow is cancelled.
373
- await DBOS.cancelWorkflow(workflowID);
374
- await expect(execution).rejects.toThrow(/has been cancelled/);
375
- expect(await readCompletions(workflowID)).toHaveLength(1);
376
- });
377
-
378
- /** A stale execution cannot apply a step the new owner also runs, nor leave an outcome for it. */
379
- test('an execution that loses the workflow mid-transaction rolls back', async () => {
380
- await userDB.query('DELETE FROM race_side_effects');
381
- const workflowID = randomUUID();
382
- await expect(DBOS.withNextWorkflowID(workflowID, () => regStaleWorkflow())).resolves.toBe('conflicted');
383
-
384
- const { rows: tags } = await userDB.query<{ tag: string }>('SELECT tag FROM race_side_effects');
385
- expect(tags).toHaveLength(0);
386
- expect(staleState.completionsAtConflict).toBe(0);
387
- const sysDB = new Client(sysConfig);
388
- try {
389
- await sysDB.connect();
390
- const { rows: steps } = await sysDB.query('SELECT * FROM dbos.operation_outputs WHERE workflow_uuid = $1', [
391
- workflowID,
392
- ]);
393
- expect(steps).toHaveLength(0);
394
- } finally {
395
- await sysDB.end();
396
- }
397
- });
398
- });
399
-
400
- const greetingsTable = pgTable('greetings', {
401
- name: text('name').primaryKey().notNull(),
402
- greet_count: integer('greet_count').default(0),
403
- });
404
-
405
- async function createDatabases(createTxCompletions: boolean) {
406
- {
407
- const client = new Client({ ...config, database: 'postgres' });
408
- try {
409
- await client.connect();
410
- await dropDB(client, 'drizzle_ds_test', true);
411
- await dropDB(client, 'drizzle_ds_test_dbos_sys', true);
412
- await dropDB(client, config.database, true);
413
- await ensureDB(client, config.database);
414
- } finally {
415
- await client.end();
416
- }
417
- }
418
-
419
- if (createTxCompletions) {
420
- await DrizzleDataSource.initializeDBOSSchema(config);
421
- }
422
- await createSchema(config);
423
- }
424
-
425
- async function insertFunction(user: string) {
426
- const result = await DrizzleDataSource.client
427
- .insert(greetingsTable)
428
- .values({ name: user, greet_count: 1 })
429
- .onConflictDoUpdate({
430
- target: greetingsTable.name,
431
- set: {
432
- greet_count: sql`${greetingsTable.greet_count} + 1`,
433
- },
434
- })
435
- .returning({ greet_count: greetingsTable.greet_count });
436
-
437
- const row = result.length > 0 ? result[0] : undefined;
438
- return { user, greet_count: row?.greet_count, now: Date.now() };
439
- }
440
-
441
- async function errorFunction(user: string) {
442
- const _result = await insertFunction(user);
443
- throw new Error(`test error ${Date.now()}`);
444
- }
445
-
446
- const raceState = { callCount: 0, plantWinner: false, plantWinnerError: false, fail: false };
447
-
448
- async function raceFunction() {
449
- raceState.callCount += 1;
450
- await dataSource.client.execute(sql`INSERT INTO race_side_effects(tag) VALUES (${`run-${raceState.callCount}`})`);
451
- if (raceState.plantWinner) {
452
- raceState.plantWinner = false;
453
- await plantWinnerCompletion();
454
- }
455
- if (raceState.fail) {
456
- throw new Error("loser's own failure");
457
- }
458
- return `result-${raceState.callCount}`;
459
- }
460
-
461
- // A duplicate execution commits its result while this transaction is still open, so this
462
- // execution's pre-check missed it but its completion insert will collide with it.
463
- async function plantWinnerCompletion() {
464
- const winner = new Client(config);
465
- try {
466
- await winner.connect();
467
- const [column, value] = raceState.plantWinnerError
468
- ? ['error', SuperJSON.stringify(new Error('winner-error'))]
469
- : ['output', SuperJSON.stringify('winner-result')];
470
- await winner.query(
471
- `INSERT INTO dbos.transaction_completion (workflow_id, function_num, ${column}) VALUES ($1, $2, $3)`,
472
- [DBOS.workflowID, DBOS.stepID, value],
473
- );
474
- } finally {
475
- await winner.end();
476
- }
477
- }
478
-
479
- const regRaceFunction = dataSource.registerTransaction(raceFunction);
480
-
481
- async function raceWorkflow() {
482
- return await regRaceFunction();
483
- }
484
-
485
- const regRaceWorkflow = DBOS.registerWorkflow(raceWorkflow);
486
-
487
- async function readFunction(user: string) {
488
- const result = await dataSource.client
489
- .select({ greet_count: greetingsTable.greet_count })
490
- .from(greetingsTable)
491
- .where(eq(greetingsTable.name, user));
492
- const row = result.length > 0 ? result[0] : undefined;
493
- return { user, greet_count: row?.greet_count, now: Date.now() };
494
- }
495
-
496
- const regInsertFunction = dataSource.registerTransaction(insertFunction);
497
- const regErrorFunction = dataSource.registerTransaction(errorFunction);
498
-
499
- const sysConfig = { ...config, database: 'drizzle_ds_test_dbos_sys' };
500
-
501
- /** Set the workflow's ownership token, returning the one it replaced. */
502
- async function setOwner(workflowID: string, ownerXid: string | null): Promise<string | null> {
503
- const sysDB = new Client(sysConfig);
504
- try {
505
- await sysDB.connect();
506
- const { rows } = await sysDB.query<{ owner_xid: string | null }>(
507
- 'SELECT owner_xid FROM dbos.workflow_status WHERE workflow_uuid = $1',
508
- [workflowID],
509
- );
510
- await sysDB.query('UPDATE dbos.workflow_status SET owner_xid = $2 WHERE workflow_uuid = $1', [
511
- workflowID,
512
- ownerXid,
513
- ]);
514
- return rows[0].owner_xid;
515
- } finally {
516
- await sysDB.end();
517
- }
518
- }
519
-
520
- /** The checkpoints this data source holds for the workflow, read on a connection of their own. */
521
- async function readCompletions(workflowID: string): Promise<transaction_completion[]> {
522
- const client = new Client(config);
523
- try {
524
- await client.connect();
525
- const { rows } = await client.query<transaction_completion>(
526
- 'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1 ORDER BY function_num',
527
- [workflowID],
528
- );
529
- return rows;
530
- } finally {
531
- await client.end();
532
- }
533
- }
534
-
535
- const probeState = { rows: [] as transaction_completion[] };
536
-
537
- // Reads back the checkpoints its transactions wrote, before completion clears them.
538
- async function checkpointProbeWorkflow(user: string) {
539
- await regInsertFunction(user);
540
- const error = await regErrorFunction(user).then(
541
- () => undefined,
542
- (e: Error) => e,
543
- );
544
- probeState.rows = await readCompletions(DBOS.workflowID!);
545
- return error?.message;
546
- }
547
-
548
- const regCheckpointProbeWorkflow = DBOS.registerWorkflow(checkpointProbeWorkflow);
549
-
550
- const staleState = { originalOwner: null as string | null, completionsAtConflict: -1 };
551
-
552
- // Another execution claims the workflow while this transaction is still open.
553
- async function staleFunction() {
554
- await dataSource.client.execute(sql`INSERT INTO race_side_effects(tag) VALUES ('stale')`);
555
- staleState.originalOwner = await setOwner(DBOS.workflowID!, 'another-execution');
556
- return 'stale';
557
- }
558
-
559
- const regStaleFunction = dataSource.registerTransaction(staleFunction);
560
-
561
- async function staleWorkflow() {
562
- try {
563
- return await regStaleFunction();
564
- } catch (e) {
565
- if (!(e instanceof DBOSWorkflowConflictError)) throw e;
566
- // Read before completion clears it: the conflict must not have been recorded as the step's outcome.
567
- staleState.completionsAtConflict = (await readCompletions(DBOS.workflowID!)).length;
568
- // A real duplicate parks here instead; reclaimed so this execution's outcome lands.
569
- await setOwner(DBOS.workflowID!, staleState.originalOwner);
570
- return 'conflicted';
571
- }
572
- }
573
-
574
- const regStaleWorkflow = DBOS.registerWorkflow(staleWorkflow);
575
-
576
- const handOffState = { signal: () => {} };
577
-
578
- // Loses the workflow after its transaction, so its completion is a stale execution's.
579
- async function handOffWorkflow(user: string) {
580
- const result = await regInsertFunction(user);
581
- await setOwner(DBOS.workflowID!, 'another-execution');
582
- handOffState.signal();
583
- return result;
584
- }
585
-
586
- const regHandOffWorkflow = DBOS.registerWorkflow(handOffWorkflow);
587
- const regReadFunction = dataSource.registerTransaction(readFunction, { accessMode: 'read only' });
588
-
589
- class StaticClass {
590
- static async insertFunction(user: string) {
591
- return await insertFunction(user);
592
- }
593
-
594
- static async readFunction(user: string) {
595
- return await readFunction(user);
596
- }
597
- }
598
-
599
- StaticClass.insertFunction = dataSource.registerTransaction(StaticClass.insertFunction);
600
- StaticClass.readFunction = dataSource.registerTransaction(StaticClass.readFunction, {
601
- accessMode: 'read only',
602
- });
603
-
604
- class InstanceClass {
605
- async insertFunction(user: string) {
606
- return await insertFunction(user);
607
- }
608
-
609
- async readFunction(user: string) {
610
- return await readFunction(user);
611
- }
612
- }
613
-
614
- InstanceClass.prototype.insertFunction = dataSource.registerTransaction(
615
- // eslint-disable-next-line @typescript-eslint/unbound-method
616
- InstanceClass.prototype.insertFunction,
617
- );
618
- InstanceClass.prototype.readFunction = dataSource.registerTransaction(
619
- // eslint-disable-next-line @typescript-eslint/unbound-method
620
- InstanceClass.prototype.readFunction,
621
- { accessMode: 'read only' },
622
- );
623
-
624
- async function insertWorkflowReg(user: string) {
625
- return await regInsertFunction(user);
626
- }
627
-
628
- async function insertWorkflowRunTx(user: string) {
629
- return await dataSource.runTransaction(() => insertFunction(user), { name: 'insertFunction' });
630
- }
631
-
632
- async function errorWorkflowReg(user: string) {
633
- return await regErrorFunction(user);
634
- }
635
-
636
- async function errorWorkflowRunTx(user: string) {
637
- return await dataSource.runTransaction(() => errorFunction(user), { name: 'errorFunction' });
638
- }
639
-
640
- async function readWorkflowReg(user: string) {
641
- return await regReadFunction(user);
642
- }
643
-
644
- async function readWorkflowRunTx(user: string) {
645
- return await dataSource.runTransaction(() => readFunction(user), { name: 'readFunction', accessMode: 'read only' });
646
- }
647
-
648
- async function staticWorkflow(user: string) {
649
- const result = await StaticClass.insertFunction(user);
650
- const readResult = await StaticClass.readFunction(user);
651
- return [result, readResult];
652
- }
653
-
654
- async function instanceWorkflow(user: string) {
655
- const instance = new InstanceClass();
656
- const result = await instance.insertFunction(user);
657
- const readResult = await instance.readFunction(user);
658
- return [result, readResult];
659
- }
660
-
661
- const regInsertWorkflowReg = DBOS.registerWorkflow(insertWorkflowReg);
662
- const regInsertWorkflowRunTx = DBOS.registerWorkflow(insertWorkflowRunTx);
663
- const regErrorWorkflowReg = DBOS.registerWorkflow(errorWorkflowReg);
664
- const regErrorWorkflowRunTx = DBOS.registerWorkflow(errorWorkflowRunTx);
665
- const regReadWorkflowReg = DBOS.registerWorkflow(readWorkflowReg);
666
- const regReadWorkflowRunTx = DBOS.registerWorkflow(readWorkflowRunTx);
667
- const regStaticWorkflow = DBOS.registerWorkflow(staticWorkflow);
668
- const regInstanceWorkflow = DBOS.registerWorkflow(instanceWorkflow);
669
-
670
- describe('DrizzleDataSourceCreateTxC', () => {
671
- const userDB = new Pool(config);
672
-
673
- beforeAll(async () => {
674
- await createDatabases(false);
675
- });
676
-
677
- afterAll(async () => {
678
- await userDB.end();
679
- });
680
-
681
- beforeEach(async () => {
682
- DBOS.setConfig({ name: 'drizzle-ds-test' });
683
- await DBOS.launch();
684
- });
685
-
686
- afterEach(async () => {
687
- await DBOS.shutdown();
688
- });
689
-
690
- test('insert dataSource.register function', async () => {
691
- const user = 'helloTest1';
692
-
693
- await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
694
- const workflowID = randomUUID();
695
-
696
- await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowReg(user))).resolves.toMatchObject({
697
- user,
698
- greet_count: 1,
699
- });
700
-
701
- const { rows } = await userDB.query<transaction_completion>(
702
- 'SELECT * FROM dbos.transaction_completion WHERE workflow_id = $1',
703
- [workflowID],
704
- );
705
- // Completion cleared the checkpoint the transaction wrote.
706
- expect(rows).toHaveLength(0);
707
- });
708
- });
709
-
710
- describe('DrizzleDataSource migrations', () => {
711
- const userDB = new Pool(config);
712
-
713
- afterAll(async () => {
714
- await userDB.end();
715
- });
716
-
717
- afterEach(async () => {
718
- await DBOS.shutdown();
719
- });
720
-
721
- test('verify-only initialization requires the schema to be migrated out of band', async () => {
722
- const suffix = randomUUID().replace(/-/g, '').slice(0, 8);
723
- const schemaName = `ds_mig_${suffix}`;
724
- const role = `ds_app_${suffix}`;
725
- const admin = new Client({ ...config, database: 'postgres' });
726
- await admin.connect();
727
- try {
728
- await ensureDB(admin, config.database);
729
- } finally {
730
- await admin.end();
731
- }
732
- await userDB.query(`CREATE ROLE "${role}"`);
733
- try {
734
- new DrizzleDataSource(`verify-only-${suffix}`, config, {}, schemaName, { runMigrations: false });
735
- DBOS.setConfig({ name: 'drizzle-ds-test' });
736
- await expect(DBOS.launch()).rejects.toThrow(/is at transaction schema version 0/);
737
- await DBOS.shutdown();
738
- const { rowCount } = await userDB.query('SELECT 1 FROM pg_namespace WHERE nspname = $1', [schemaName]);
739
- expect(rowCount).toBe(0);
740
-
741
- await DrizzleDataSource.initializeDBOSSchema(config, schemaName, { applicationRole: role });
742
- await DBOS.launch();
743
- const { rows } = await userDB.query<{ allowed: boolean }>(
744
- `SELECT has_table_privilege($1, $2, 'INSERT') AS allowed`,
745
- [role, `"${schemaName}".transaction_completion`],
746
- );
747
- expect(rows).toEqual([{ allowed: true }]);
748
- } finally {
749
- await userDB.query(`DROP SCHEMA IF EXISTS "${schemaName}" CASCADE`);
750
- await userDB.query(`DROP OWNED BY "${role}"`);
751
- await userDB.query(`DROP ROLE "${role}"`);
752
- }
753
- });
754
- });
@@ -1,8 +0,0 @@
1
- // Typechecked by esmtypes.test.ts, never executed: .mts makes it ESM regardless of the package type.
2
- import { DrizzleDataSource } from '@dbos-inc/drizzle-datasource';
3
- import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
4
-
5
- declare const dataSource: DrizzleDataSource;
6
-
7
- export const instanceClient: NodePgDatabase<{ [key: string]: object }> = dataSource.client;
8
- export const staticClient: NodePgDatabase<{ [key: string]: object }> = DrizzleDataSource.client;
@@ -1,27 +0,0 @@
1
- import { execFileSync } from 'child_process';
2
- import { readFileSync } from 'fs';
3
- import path from 'path';
4
-
5
- const packageRoot = path.join(__dirname, '..');
6
-
7
- describe('ESM type declarations', () => {
8
- test('an ESM consumer under NodeNext typechecks against the built declarations', () => {
9
- const tsc = require.resolve('typescript/bin/tsc');
10
- const project = path.join(__dirname, 'tsconfig.esmcheck.json');
11
- try {
12
- execFileSync(process.execPath, [tsc, '--project', project], { cwd: packageRoot, stdio: 'pipe' });
13
- } catch (error) {
14
- const { stdout } = error as { stdout?: Buffer };
15
- throw new Error(`ESM consumer failed to typecheck. Run 'npm run build' first.\n${stdout?.toString() ?? ''}`);
16
- }
17
- }, 120000);
18
-
19
- test('the ESM declarations carry no reference to the CommonJS declaration map', () => {
20
- const declarations = readFileSync(path.join(packageRoot, 'dist', 'index.d.mts'), 'utf8');
21
- expect(declarations).not.toContain('sourceMappingURL');
22
- });
23
-
24
- test('the exports map still resolves the package manifest', () => {
25
- expect(() => require.resolve('@dbos-inc/drizzle-datasource/package.json')).not.toThrow();
26
- });
27
- });
@@ -1,13 +0,0 @@
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
- }
@@ -1,11 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "module": "nodenext",
4
- "moduleResolution": "nodenext",
5
- "target": "esnext",
6
- "strict": true,
7
- "skipLibCheck": true,
8
- "noEmit": true
9
- },
10
- "files": ["esm-consumer.mts"]
11
- }
package/tsconfig.json DELETED
@@ -1,9 +0,0 @@
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
- }