@dbos-inc/postgres-datasource 3.0.7-preview → 3.0.8-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/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +38 -36
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/index.ts +48 -48
- package/package.json +6 -5
- package/tests/datasource.test.ts +57 -19
package/index.ts
CHANGED
|
@@ -21,40 +21,50 @@ import { SuperJSON } from 'superjson';
|
|
|
21
21
|
export { IsolationLevel, PostgresTransactionOptions };
|
|
22
22
|
|
|
23
23
|
interface PostgresDataSourceContext {
|
|
24
|
-
|
|
25
|
-
client: postgres.TransactionSql<{}>;
|
|
24
|
+
client: postgres.TransactionSql;
|
|
26
25
|
}
|
|
27
26
|
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
28
|
+
type Options = postgres.Options<{}>;
|
|
29
|
+
|
|
28
30
|
const asyncLocalCtx = new AsyncLocalStorage<PostgresDataSourceContext>();
|
|
29
31
|
|
|
30
32
|
class PostgresTransactionHandler implements DataSourceTransactionHandler {
|
|
31
33
|
readonly dsType = 'PostgresDataSource';
|
|
32
|
-
|
|
34
|
+
#dbField: Sql | undefined;
|
|
33
35
|
|
|
34
36
|
constructor(
|
|
35
37
|
readonly name: string,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
private readonly options: Options = {},
|
|
39
|
+
) {}
|
|
40
|
+
|
|
41
|
+
async initialize(): Promise<void> {
|
|
42
|
+
const db = this.#dbField;
|
|
43
|
+
this.#dbField = postgres(this.options);
|
|
44
|
+
await db?.end();
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
async destroy(): Promise<void> {
|
|
48
|
+
const db = this.#dbField;
|
|
49
|
+
this.#dbField = undefined;
|
|
50
|
+
await db?.end();
|
|
44
51
|
}
|
|
45
52
|
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
get #db(): Sql {
|
|
54
|
+
if (!this.#dbField) {
|
|
55
|
+
throw new Error(`DataSource ${this.name} is not initialized.`);
|
|
56
|
+
}
|
|
57
|
+
return this.#dbField;
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
async #checkExecution(
|
|
51
61
|
workflowID: string,
|
|
52
|
-
|
|
62
|
+
stepID: number,
|
|
53
63
|
): Promise<{ output: string | null } | { error: string } | undefined> {
|
|
54
64
|
type Result = { output: string | null; error: string | null };
|
|
55
65
|
const result = await this.#db<Result[]>/*sql*/ `
|
|
56
66
|
SELECT output, error FROM dbos.transaction_completion
|
|
57
|
-
WHERE workflow_id = ${workflowID} AND function_num = ${
|
|
67
|
+
WHERE workflow_id = ${workflowID} AND function_num = ${stepID}`;
|
|
58
68
|
if (result.length === 0) {
|
|
59
69
|
return undefined;
|
|
60
70
|
}
|
|
@@ -63,16 +73,15 @@ class PostgresTransactionHandler implements DataSourceTransactionHandler {
|
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
static async #recordOutput(
|
|
66
|
-
|
|
67
|
-
client: postgres.TransactionSql<{}>,
|
|
76
|
+
client: postgres.TransactionSql,
|
|
68
77
|
workflowID: string,
|
|
69
|
-
|
|
78
|
+
stepID: number,
|
|
70
79
|
output: string | null,
|
|
71
80
|
): Promise<void> {
|
|
72
81
|
try {
|
|
73
82
|
await client/*sql*/ `
|
|
74
83
|
INSERT INTO dbos.transaction_completion (workflow_id, function_num, output)
|
|
75
|
-
VALUES (${workflowID}, ${
|
|
84
|
+
VALUES (${workflowID}, ${stepID}, ${output})`;
|
|
76
85
|
} catch (error) {
|
|
77
86
|
if (isPGKeyConflictError(error)) {
|
|
78
87
|
throw new DBOSWorkflowConflictError(workflowID);
|
|
@@ -82,11 +91,11 @@ class PostgresTransactionHandler implements DataSourceTransactionHandler {
|
|
|
82
91
|
}
|
|
83
92
|
}
|
|
84
93
|
|
|
85
|
-
async #recordError(workflowID: string,
|
|
94
|
+
async #recordError(workflowID: string, stepID: number, error: string): Promise<void> {
|
|
86
95
|
try {
|
|
87
96
|
await this.#db/*sql*/ `
|
|
88
97
|
INSERT INTO dbos.transaction_completion (workflow_id, function_num, error)
|
|
89
|
-
VALUES (${workflowID}, ${
|
|
98
|
+
VALUES (${workflowID}, ${stepID}, ${error})`;
|
|
90
99
|
} catch (error) {
|
|
91
100
|
if (isPGKeyConflictError(error)) {
|
|
92
101
|
throw new DBOSWorkflowConflictError(workflowID);
|
|
@@ -103,26 +112,24 @@ class PostgresTransactionHandler implements DataSourceTransactionHandler {
|
|
|
103
112
|
...args: Args
|
|
104
113
|
): Promise<Return> {
|
|
105
114
|
const workflowID = DBOS.workflowID;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const functionNum = DBOS.stepID;
|
|
110
|
-
if (functionNum === undefined) {
|
|
111
|
-
throw new Error('Function Number is not set.');
|
|
115
|
+
const stepID = DBOS.stepID;
|
|
116
|
+
if (workflowID !== undefined && stepID === undefined) {
|
|
117
|
+
throw new Error('DBOS.stepID is undefined inside a workflow.');
|
|
112
118
|
}
|
|
113
119
|
|
|
114
120
|
const isolationLevel = config?.isolationLevel ? `ISOLATION LEVEL ${config.isolationLevel}` : '';
|
|
115
121
|
const readOnly = config?.readOnly ?? false;
|
|
116
122
|
const accessMode = config?.readOnly === undefined ? '' : readOnly ? 'READ ONLY' : 'READ WRITE';
|
|
117
|
-
const saveResults = !readOnly && workflowID;
|
|
123
|
+
const saveResults = !readOnly && workflowID !== undefined;
|
|
118
124
|
|
|
125
|
+
// Retry loop if appropriate
|
|
119
126
|
let retryWaitMS = 1;
|
|
120
127
|
const backoffFactor = 1.5;
|
|
121
|
-
const maxRetryWaitMS = 2000;
|
|
128
|
+
const maxRetryWaitMS = 2000; // Maximum wait 2 seconds.
|
|
122
129
|
|
|
123
130
|
while (true) {
|
|
124
131
|
// Check to see if this tx has already been executed
|
|
125
|
-
const previousResult = saveResults ? await this.#checkExecution(workflowID,
|
|
132
|
+
const previousResult = saveResults ? await this.#checkExecution(workflowID, stepID!) : undefined;
|
|
126
133
|
if (previousResult) {
|
|
127
134
|
DBOS.span?.setAttribute('cached', true);
|
|
128
135
|
|
|
@@ -141,12 +148,7 @@ class PostgresTransactionHandler implements DataSourceTransactionHandler {
|
|
|
141
148
|
|
|
142
149
|
// save the output of read/write transactions
|
|
143
150
|
if (saveResults) {
|
|
144
|
-
await PostgresTransactionHandler.#recordOutput(
|
|
145
|
-
client,
|
|
146
|
-
workflowID,
|
|
147
|
-
functionNum,
|
|
148
|
-
SuperJSON.stringify(result),
|
|
149
|
-
);
|
|
151
|
+
await PostgresTransactionHandler.#recordOutput(client, workflowID, stepID!, SuperJSON.stringify(result));
|
|
150
152
|
}
|
|
151
153
|
|
|
152
154
|
return result;
|
|
@@ -162,7 +164,7 @@ class PostgresTransactionHandler implements DataSourceTransactionHandler {
|
|
|
162
164
|
} else {
|
|
163
165
|
if (saveResults) {
|
|
164
166
|
const message = SuperJSON.stringify(error);
|
|
165
|
-
await this.#recordError(workflowID,
|
|
167
|
+
await this.#recordError(workflowID, stepID!, message);
|
|
166
168
|
}
|
|
167
169
|
|
|
168
170
|
throw error;
|
|
@@ -173,20 +175,18 @@ class PostgresTransactionHandler implements DataSourceTransactionHandler {
|
|
|
173
175
|
}
|
|
174
176
|
|
|
175
177
|
export class PostgresDataSource implements DBOSDataSource<PostgresTransactionOptions> {
|
|
176
|
-
|
|
177
|
-
static get client(): postgres.TransactionSql<{}> {
|
|
178
|
+
static get client(): postgres.TransactionSql {
|
|
178
179
|
if (!DBOS.isInTransaction()) {
|
|
179
180
|
throw new Error('invalid use of PostgresDataSource.client outside of a DBOS transaction.');
|
|
180
181
|
}
|
|
181
182
|
const ctx = asyncLocalCtx.getStore();
|
|
182
183
|
if (!ctx) {
|
|
183
|
-
throw new Error('
|
|
184
|
+
throw new Error('invalid use of PostgresDataSource.client outside of a DBOS transaction.');
|
|
184
185
|
}
|
|
185
186
|
return ctx.client;
|
|
186
187
|
}
|
|
187
188
|
|
|
188
|
-
|
|
189
|
-
static async initializeInternalSchema(options: postgres.Options<{}> = {}): Promise<void> {
|
|
189
|
+
static async initializeInternalSchema(options: Options = {}): Promise<void> {
|
|
190
190
|
const pg = postgres({ ...options, onnotice: () => {} });
|
|
191
191
|
try {
|
|
192
192
|
await pg.unsafe(createTransactionCompletionSchemaPG);
|
|
@@ -196,12 +196,12 @@ export class PostgresDataSource implements DBOSDataSource<PostgresTransactionOpt
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
readonly name: string;
|
|
200
199
|
#provider: PostgresTransactionHandler;
|
|
201
200
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
201
|
+
constructor(
|
|
202
|
+
readonly name: string,
|
|
203
|
+
options: Options = {},
|
|
204
|
+
) {
|
|
205
205
|
this.#provider = new PostgresTransactionHandler(name, options);
|
|
206
206
|
registerDataSource(this.#provider);
|
|
207
207
|
}
|
|
@@ -212,10 +212,10 @@ export class PostgresDataSource implements DBOSDataSource<PostgresTransactionOpt
|
|
|
212
212
|
|
|
213
213
|
registerTransaction<This, Args extends unknown[], Return>(
|
|
214
214
|
func: (this: This, ...args: Args) => Promise<Return>,
|
|
215
|
-
name: string,
|
|
216
215
|
config?: PostgresTransactionOptions,
|
|
216
|
+
name?: string,
|
|
217
217
|
): (this: This, ...args: Args) => Promise<Return> {
|
|
218
|
-
return registerTransaction(this.name, func, { name }, config);
|
|
218
|
+
return registerTransaction(this.name, func, { name: name ?? func.name }, config);
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
transaction(config?: PostgresTransactionOptions) {
|
|
@@ -223,14 +223,14 @@ export class PostgresDataSource implements DBOSDataSource<PostgresTransactionOpt
|
|
|
223
223
|
const ds = this;
|
|
224
224
|
return function decorator<This, Args extends unknown[], Return>(
|
|
225
225
|
_target: object,
|
|
226
|
-
propertyKey:
|
|
226
|
+
propertyKey: PropertyKey,
|
|
227
227
|
descriptor: TypedPropertyDescriptor<(this: This, ...args: Args) => Promise<Return>>,
|
|
228
228
|
) {
|
|
229
229
|
if (!descriptor.value) {
|
|
230
230
|
throw Error('Use of decorator when original method is undefined');
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
descriptor.value = ds.registerTransaction(descriptor.value,
|
|
233
|
+
descriptor.value = ds.registerTransaction(descriptor.value, config, String(propertyKey));
|
|
234
234
|
|
|
235
235
|
return descriptor;
|
|
236
236
|
};
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dbos-inc/postgres-datasource",
|
|
3
|
-
"version": "3.0.
|
|
4
|
-
"description": "",
|
|
3
|
+
"version": "3.0.8-preview",
|
|
4
|
+
"description": "DBOS DataSource library for Postgres database client",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"homepage": "https://docs.dbos.dev/",
|
|
6
9
|
"repository": {
|
|
7
10
|
"type": "git",
|
|
8
11
|
"url": "https://github.com/dbos-inc/dbos-transact-ts",
|
|
9
|
-
"directory": "packages/
|
|
12
|
+
"directory": "packages/postgres-datasource"
|
|
10
13
|
},
|
|
11
|
-
"homepage": "https://docs.dbos.dev/",
|
|
12
|
-
"main": "index.js",
|
|
13
14
|
"scripts": {
|
|
14
15
|
"build": "tsc --project tsconfig.json",
|
|
15
16
|
"test": "jest --detectOpenHandles"
|
package/tests/datasource.test.ts
CHANGED
|
@@ -44,13 +44,19 @@ describe('PostgresDataSource', () => {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
await PostgresDataSource.initializeInternalSchema(config);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterAll(async () => {
|
|
50
|
+
await userDB.end();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
beforeEach(async () => {
|
|
47
54
|
DBOS.setConfig({ name: 'pg-ds-test' });
|
|
48
55
|
await DBOS.launch();
|
|
49
56
|
});
|
|
50
57
|
|
|
51
|
-
|
|
58
|
+
afterEach(async () => {
|
|
52
59
|
await DBOS.shutdown();
|
|
53
|
-
await userDB.end();
|
|
54
60
|
});
|
|
55
61
|
|
|
56
62
|
test('insert dataSource.register function', async () => {
|
|
@@ -59,7 +65,7 @@ describe('PostgresDataSource', () => {
|
|
|
59
65
|
await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
|
|
60
66
|
const workflowID = randomUUID();
|
|
61
67
|
|
|
62
|
-
await expect(DBOS.withNextWorkflowID(workflowID, () =>
|
|
68
|
+
await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowReg(user))).resolves.toMatchObject({
|
|
63
69
|
user,
|
|
64
70
|
greet_count: 1,
|
|
65
71
|
});
|
|
@@ -81,10 +87,10 @@ describe('PostgresDataSource', () => {
|
|
|
81
87
|
await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
|
|
82
88
|
const workflowID = randomUUID();
|
|
83
89
|
|
|
84
|
-
const result = await DBOS.withNextWorkflowID(workflowID, () =>
|
|
90
|
+
const result = await DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowReg(user));
|
|
85
91
|
expect(result).toMatchObject({ user, greet_count: 1 });
|
|
86
92
|
|
|
87
|
-
await expect(DBOS.withNextWorkflowID(workflowID, () =>
|
|
93
|
+
await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowReg(user))).resolves.toMatchObject(result);
|
|
88
94
|
});
|
|
89
95
|
|
|
90
96
|
test('insert dataSource.runAsTx function', async () => {
|
|
@@ -93,7 +99,7 @@ describe('PostgresDataSource', () => {
|
|
|
93
99
|
await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
|
|
94
100
|
const workflowID = randomUUID();
|
|
95
101
|
|
|
96
|
-
await expect(DBOS.withNextWorkflowID(workflowID, () =>
|
|
102
|
+
await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowRunTx(user))).resolves.toMatchObject({
|
|
97
103
|
user,
|
|
98
104
|
greet_count: 1,
|
|
99
105
|
});
|
|
@@ -115,10 +121,10 @@ describe('PostgresDataSource', () => {
|
|
|
115
121
|
await userDB.query('DELETE FROM greetings WHERE name = $1', [user]);
|
|
116
122
|
const workflowID = randomUUID();
|
|
117
123
|
|
|
118
|
-
const result = await DBOS.withNextWorkflowID(workflowID, () =>
|
|
124
|
+
const result = await DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowRunTx(user));
|
|
119
125
|
expect(result).toMatchObject({ user, greet_count: 1 });
|
|
120
126
|
|
|
121
|
-
await expect(DBOS.withNextWorkflowID(workflowID, () =>
|
|
127
|
+
await expect(DBOS.withNextWorkflowID(workflowID, () => regInsertWorkflowRunTx(user))).resolves.toMatchObject(
|
|
122
128
|
result,
|
|
123
129
|
);
|
|
124
130
|
});
|
|
@@ -281,6 +287,40 @@ describe('PostgresDataSource', () => {
|
|
|
281
287
|
{ user, greet_count: 1 },
|
|
282
288
|
]);
|
|
283
289
|
});
|
|
290
|
+
|
|
291
|
+
test('invoke-reg-tx-fun-outside-wf', async () => {
|
|
292
|
+
const user = 'outsideWfUser' + Date.now();
|
|
293
|
+
const result = await regInsertFunction(user);
|
|
294
|
+
expect(result).toMatchObject({ user, greet_count: 1 });
|
|
295
|
+
|
|
296
|
+
const txResults = await userDB.query('SELECT * FROM dbos.transaction_completion WHERE output LIKE $1', [
|
|
297
|
+
`%${user}%`,
|
|
298
|
+
]);
|
|
299
|
+
expect(txResults.rows.length).toBe(0);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('invoke-reg-tx-static-method-outside-wf', async () => {
|
|
303
|
+
const user = 'outsideWfUser' + Date.now();
|
|
304
|
+
const result = await StaticClass.insertFunction(user);
|
|
305
|
+
expect(result).toMatchObject({ user, greet_count: 1 });
|
|
306
|
+
|
|
307
|
+
const txResults = await userDB.query('SELECT * FROM dbos.transaction_completion WHERE output LIKE $1', [
|
|
308
|
+
`%${user}%`,
|
|
309
|
+
]);
|
|
310
|
+
expect(txResults.rows.length).toBe(0);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('invoke-reg-tx-inst-method-outside-wf', async () => {
|
|
314
|
+
const user = 'outsideWfUser' + Date.now();
|
|
315
|
+
const instance = new InstanceClass();
|
|
316
|
+
const result = await instance.insertFunction(user);
|
|
317
|
+
expect(result).toMatchObject({ user, greet_count: 1 });
|
|
318
|
+
|
|
319
|
+
const txResults = await userDB.query('SELECT * FROM dbos.transaction_completion WHERE output LIKE $1', [
|
|
320
|
+
`%${user}%`,
|
|
321
|
+
]);
|
|
322
|
+
expect(txResults.rows.length).toBe(0);
|
|
323
|
+
});
|
|
284
324
|
});
|
|
285
325
|
|
|
286
326
|
export interface greetings {
|
|
@@ -300,9 +340,8 @@ async function insertFunction(user: string) {
|
|
|
300
340
|
}
|
|
301
341
|
|
|
302
342
|
async function errorFunction(user: string) {
|
|
303
|
-
const
|
|
343
|
+
const _result = await insertFunction(user);
|
|
304
344
|
throw new Error(`test error ${Date.now()}`);
|
|
305
|
-
return result;
|
|
306
345
|
}
|
|
307
346
|
|
|
308
347
|
async function readFunction(user: string) {
|
|
@@ -314,9 +353,9 @@ async function readFunction(user: string) {
|
|
|
314
353
|
return { user, greet_count: row?.greet_count, now: Date.now() };
|
|
315
354
|
}
|
|
316
355
|
|
|
317
|
-
const regInsertFunction = dataSource.registerTransaction(insertFunction
|
|
318
|
-
const regErrorFunction = dataSource.registerTransaction(errorFunction
|
|
319
|
-
const regReadFunction = dataSource.registerTransaction(readFunction,
|
|
356
|
+
const regInsertFunction = dataSource.registerTransaction(insertFunction);
|
|
357
|
+
const regErrorFunction = dataSource.registerTransaction(errorFunction);
|
|
358
|
+
const regReadFunction = dataSource.registerTransaction(readFunction, { readOnly: true });
|
|
320
359
|
|
|
321
360
|
class StaticClass {
|
|
322
361
|
static async insertFunction(user: string) {
|
|
@@ -328,8 +367,8 @@ class StaticClass {
|
|
|
328
367
|
}
|
|
329
368
|
}
|
|
330
369
|
|
|
331
|
-
StaticClass.insertFunction = dataSource.registerTransaction(StaticClass.insertFunction
|
|
332
|
-
StaticClass.readFunction = dataSource.registerTransaction(StaticClass.readFunction,
|
|
370
|
+
StaticClass.insertFunction = dataSource.registerTransaction(StaticClass.insertFunction);
|
|
371
|
+
StaticClass.readFunction = dataSource.registerTransaction(StaticClass.readFunction, { readOnly: true });
|
|
333
372
|
|
|
334
373
|
class InstanceClass {
|
|
335
374
|
async insertFunction(user: string) {
|
|
@@ -344,12 +383,11 @@ class InstanceClass {
|
|
|
344
383
|
InstanceClass.prototype.insertFunction = dataSource.registerTransaction(
|
|
345
384
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
346
385
|
InstanceClass.prototype.insertFunction,
|
|
347
|
-
'insertFunction',
|
|
348
386
|
);
|
|
349
387
|
InstanceClass.prototype.readFunction = dataSource.registerTransaction(
|
|
350
388
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
351
389
|
InstanceClass.prototype.readFunction,
|
|
352
|
-
|
|
390
|
+
{ readOnly: true },
|
|
353
391
|
);
|
|
354
392
|
|
|
355
393
|
async function insertWorkflowReg(user: string) {
|
|
@@ -389,8 +427,8 @@ async function instanceWorkflow(user: string) {
|
|
|
389
427
|
return [result, readResult];
|
|
390
428
|
}
|
|
391
429
|
|
|
392
|
-
const
|
|
393
|
-
const
|
|
430
|
+
const regInsertWorkflowReg = DBOS.registerWorkflow(insertWorkflowReg, 'insertWorkflowReg');
|
|
431
|
+
const regInsertWorkflowRunTx = DBOS.registerWorkflow(insertWorkflowRunTx, 'insertWorkflowRunTx');
|
|
394
432
|
const regErrorWorkflowReg = DBOS.registerWorkflow(errorWorkflowReg, 'errorWorkflowReg');
|
|
395
433
|
const regErrorWorkflowRunTx = DBOS.registerWorkflow(errorWorkflowRunTx, 'errorWorkflowRunTx');
|
|
396
434
|
const regReadWorkflowReg = DBOS.registerWorkflow(readWorkflowReg, 'readWorkflowReg');
|