@entity-access/entity-access 1.0.95 → 1.0.96

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.
Files changed (103) hide show
  1. package/.vscode/launch.json +3 -0
  2. package/dist/compiler/QueryCompiler.d.ts +2 -0
  3. package/dist/compiler/QueryCompiler.d.ts.map +1 -1
  4. package/dist/compiler/QueryCompiler.js +6 -0
  5. package/dist/compiler/QueryCompiler.js.map +1 -1
  6. package/dist/compiler/RawQuery.d.ts +10 -0
  7. package/dist/compiler/RawQuery.d.ts.map +1 -0
  8. package/dist/compiler/RawQuery.js +26 -0
  9. package/dist/compiler/RawQuery.js.map +1 -0
  10. package/dist/compiler/postgres/PostgreSqlMethodTransformer.d.ts.map +1 -1
  11. package/dist/compiler/postgres/PostgreSqlMethodTransformer.js +3 -0
  12. package/dist/compiler/postgres/PostgreSqlMethodTransformer.js.map +1 -1
  13. package/dist/compiler/sql-server/SqlServerSqlMethodTransformer.d.ts.map +1 -1
  14. package/dist/compiler/sql-server/SqlServerSqlMethodTransformer.js +9 -6
  15. package/dist/compiler/sql-server/SqlServerSqlMethodTransformer.js.map +1 -1
  16. package/dist/drivers/base/BaseDriver.d.ts +15 -8
  17. package/dist/drivers/base/BaseDriver.d.ts.map +1 -1
  18. package/dist/drivers/base/BaseDriver.js +13 -3
  19. package/dist/drivers/base/BaseDriver.js.map +1 -1
  20. package/dist/drivers/postgres/PostgreSqlDriver.d.ts +2 -12
  21. package/dist/drivers/postgres/PostgreSqlDriver.d.ts.map +1 -1
  22. package/dist/drivers/postgres/PostgreSqlDriver.js +11 -2
  23. package/dist/drivers/postgres/PostgreSqlDriver.js.map +1 -1
  24. package/dist/drivers/sql-server/SqlServerDriver.d.ts +8 -2
  25. package/dist/drivers/sql-server/SqlServerDriver.d.ts.map +1 -1
  26. package/dist/drivers/sql-server/SqlServerDriver.js +18 -10
  27. package/dist/drivers/sql-server/SqlServerDriver.js.map +1 -1
  28. package/dist/eternity/ActivitySuspendedError.d.ts +3 -0
  29. package/dist/eternity/ActivitySuspendedError.d.ts.map +1 -1
  30. package/dist/eternity/ActivitySuspendedError.js +6 -0
  31. package/dist/eternity/ActivitySuspendedError.js.map +1 -1
  32. package/dist/eternity/EternityContext.d.ts +3 -1
  33. package/dist/eternity/EternityContext.d.ts.map +1 -1
  34. package/dist/eternity/EternityContext.js +54 -6
  35. package/dist/eternity/EternityContext.js.map +1 -1
  36. package/dist/eternity/EternityStorage.d.ts +6 -0
  37. package/dist/eternity/EternityStorage.d.ts.map +1 -1
  38. package/dist/eternity/EternityStorage.js +60 -19
  39. package/dist/eternity/EternityStorage.js.map +1 -1
  40. package/dist/eternity/Workflow.d.ts +15 -4
  41. package/dist/eternity/Workflow.d.ts.map +1 -1
  42. package/dist/eternity/Workflow.js +35 -22
  43. package/dist/eternity/Workflow.js.map +1 -1
  44. package/dist/migrations/postgres/PostgresAutomaticMigrations.d.ts +3 -3
  45. package/dist/migrations/postgres/PostgresAutomaticMigrations.d.ts.map +1 -1
  46. package/dist/migrations/postgres/PostgresAutomaticMigrations.js +2 -2
  47. package/dist/migrations/postgres/PostgresAutomaticMigrations.js.map +1 -1
  48. package/dist/migrations/sql-server/SqlServerAutomaticMigrations.d.ts +3 -3
  49. package/dist/migrations/sql-server/SqlServerAutomaticMigrations.d.ts.map +1 -1
  50. package/dist/migrations/sql-server/SqlServerAutomaticMigrations.js +2 -2
  51. package/dist/migrations/sql-server/SqlServerAutomaticMigrations.js.map +1 -1
  52. package/dist/model/EntityContext.d.ts +3 -1
  53. package/dist/model/EntityContext.d.ts.map +1 -1
  54. package/dist/model/EntityContext.js +6 -2
  55. package/dist/model/EntityContext.js.map +1 -1
  56. package/dist/model/EntityQuery.js +3 -3
  57. package/dist/model/EntityQuery.js.map +1 -1
  58. package/dist/model/verification/VerificationSession.js +1 -1
  59. package/dist/model/verification/VerificationSession.js.map +1 -1
  60. package/dist/query/ast/ExpressionToSql.d.ts.map +1 -1
  61. package/dist/query/ast/ExpressionToSql.js +21 -19
  62. package/dist/query/ast/ExpressionToSql.js.map +1 -1
  63. package/dist/query/ast/Expressions.d.ts +1 -0
  64. package/dist/query/ast/Expressions.d.ts.map +1 -1
  65. package/dist/query/ast/Expressions.js +3 -0
  66. package/dist/query/ast/Expressions.js.map +1 -1
  67. package/dist/sql/ISql.d.ts +2 -0
  68. package/dist/sql/ISql.d.ts.map +1 -1
  69. package/dist/tests/drivers/postgres/connection-test.js +1 -1
  70. package/dist/tests/drivers/postgres/connection-test.js.map +1 -1
  71. package/dist/tests/eternity/child-tests.d.ts +3 -0
  72. package/dist/tests/eternity/child-tests.d.ts.map +1 -0
  73. package/dist/tests/eternity/child-tests.js +103 -0
  74. package/dist/tests/eternity/child-tests.js.map +1 -0
  75. package/dist/tests/eternity/eternity-tests.d.ts.map +1 -1
  76. package/dist/tests/eternity/eternity-tests.js.map +1 -1
  77. package/dist/tests/model/createContext.js +2 -2
  78. package/dist/tests/model/createContext.js.map +1 -1
  79. package/dist/tsconfig.tsbuildinfo +1 -1
  80. package/package.json +1 -1
  81. package/src/compiler/QueryCompiler.ts +7 -0
  82. package/src/compiler/RawQuery.ts +29 -0
  83. package/src/compiler/postgres/PostgreSqlMethodTransformer.ts +3 -0
  84. package/src/compiler/sql-server/SqlServerSqlMethodTransformer.ts +9 -6
  85. package/src/drivers/base/BaseDriver.ts +28 -10
  86. package/src/drivers/postgres/PostgreSqlDriver.ts +15 -2
  87. package/src/drivers/sql-server/SqlServerDriver.ts +21 -11
  88. package/src/eternity/ActivitySuspendedError.ts +8 -1
  89. package/src/eternity/EternityContext.ts +69 -6
  90. package/src/eternity/EternityStorage.ts +79 -19
  91. package/src/eternity/Workflow.ts +51 -11
  92. package/src/migrations/postgres/PostgresAutomaticMigrations.ts +5 -5
  93. package/src/migrations/sql-server/SqlServerAutomaticMigrations.ts +5 -5
  94. package/src/model/EntityContext.ts +9 -3
  95. package/src/model/EntityQuery.ts +3 -3
  96. package/src/model/verification/VerificationSession.ts +1 -1
  97. package/src/query/ast/ExpressionToSql.ts +22 -20
  98. package/src/query/ast/Expressions.ts +4 -0
  99. package/src/sql/ISql.ts +3 -0
  100. package/src/tests/drivers/postgres/connection-test.ts +1 -1
  101. package/src/tests/eternity/child-tests.ts +119 -0
  102. package/src/tests/eternity/eternity-tests.ts +1 -0
  103. package/src/tests/model/createContext.ts +2 -2
@@ -35,6 +35,9 @@ export const PostgreSqlHelper: ISqlHelpers = {
35
35
  },
36
36
  },
37
37
  date: {
38
+ now() {
39
+ return prepareAny `NOW()`;
40
+ },
38
41
  addDays(d, n) {
39
42
  return prepareAny `(${d} + (${n} * interval '1 day'))`;
40
43
  },
@@ -33,23 +33,26 @@ export const SqlServerSqlHelper: ISqlHelpers = {
33
33
  },
34
34
  },
35
35
  date: {
36
+ now() {
37
+ return prepareAny `GETUTCDATE()`;
38
+ },
36
39
  addDays(d, n) {
37
- return prepareAny `DateAdd(DAY, ${d}, ${n})`;
40
+ return prepareAny `DateAdd(DAY, ${n}, ${d})`;
38
41
  },
39
42
  addHours(d, n) {
40
- return prepareAny `DateAdd(HOUR, ${d}, ${n})`;
43
+ return prepareAny `DateAdd(HOUR, ${n}, ${d})`;
41
44
  },
42
45
  addMinutes(d, n) {
43
- return prepareAny `DateAdd(MINUTE, ${d}, ${n})`;
46
+ return prepareAny `DateAdd(MINUTE, ${n}, ${d})`;
44
47
  },
45
48
  addMonths(d, n) {
46
- return prepareAny `DateAdd(MONTH, ${d}, ${n})`;
49
+ return prepareAny `DateAdd(MONTH, ${n}, ${d})`;
47
50
  },
48
51
  addSeconds(d, n) {
49
- return prepareAny `DateAdd(SECOND, ${d}, ${n})`;
52
+ return prepareAny `DateAdd(SECOND, ${n}, ${d})`;
50
53
  },
51
54
  addYears(d, n) {
52
- return prepareAny `DateAdd(YEAR, ${d}, ${n})`;
55
+ return prepareAny `DateAdd(YEAR, ${n}, ${d})`;
53
56
  },
54
57
  dayOf(d) {
55
58
  return prepareAny `DATE_PART(day, ${d})`;
@@ -53,19 +53,33 @@ export interface IBaseTransaction {
53
53
  dispose(): Promise<any>;
54
54
  }
55
55
 
56
- export abstract class BaseDriver {
57
- abstract get compiler(): QueryCompiler;
56
+ export abstract class BaseConnection {
57
+
58
+ protected compiler: QueryCompiler;
59
+
60
+ protected connectionString: IDbConnectionString;
58
61
 
59
62
  private currentTransaction: IBaseTransaction;
60
63
 
61
- constructor(public readonly connectionString: IDbConnectionString) {}
64
+
65
+ constructor(public driver: BaseDriver) {
66
+ this.compiler = driver.compiler;
67
+ this.connectionString = driver.connectionString;
68
+ }
69
+
70
+ public abstract ensureDatabase(): Promise<any>;
71
+
72
+ /**
73
+ * This migrations only support creation of missing items.
74
+ * However, you can provide events to change existing items.
75
+ */
76
+ public abstract automaticMigrations(): Migrations;
77
+
62
78
 
63
79
  public abstract executeReader(command: IQuery, signal?: AbortSignal): Promise<IDbReader>;
64
80
 
65
81
  public abstract executeQuery(command: IQuery, signal?: AbortSignal): Promise<IQueryResult>;
66
82
 
67
- public abstract ensureDatabase(): Promise<any>;
68
-
69
83
  public abstract createTransaction(): Promise<IBaseTransaction>;
70
84
 
71
85
  public async runInTransaction<T = any>(fx?: () => Promise<T>) {
@@ -90,12 +104,16 @@ export abstract class BaseDriver {
90
104
  this.currentTransaction = null;
91
105
  }
92
106
  }
107
+ }
108
+
109
+ export abstract class BaseDriver {
110
+ abstract get compiler(): QueryCompiler;
111
+
112
+
113
+ constructor(public readonly connectionString: IDbConnectionString) {}
114
+
115
+ abstract newConnection(): BaseConnection;
93
116
 
94
- /**
95
- * This migrations only support creation of missing items.
96
- * However, you can provide events to change existing items.
97
- */
98
- public abstract automaticMigrations(): Migrations;
99
117
 
100
118
  createInsertExpression(type: EntityType, entity: any): InsertStatement {
101
119
  const returnFields = [] as Identifier[];
@@ -5,7 +5,7 @@ import TimedCache from "../../common/cache/TimedCache.js";
5
5
  import QueryCompiler from "../../compiler/QueryCompiler.js";
6
6
  import Migrations from "../../migrations/Migrations.js";
7
7
  import PostgresAutomaticMigrations from "../../migrations/postgres/PostgresAutomaticMigrations.js";
8
- import { BaseDriver, IBaseTransaction, IDbConnectionString, IDbReader, IQuery, IRecord, toQuery } from "../base/BaseDriver.js";
8
+ import { BaseConnection, BaseDriver, IBaseTransaction, IDbConnectionString, IDbReader, IQuery, IRecord, toQuery } from "../base/BaseDriver.js";
9
9
  import pg from "pg";
10
10
  import Cursor from "pg-cursor";
11
11
  export interface IPgSqlConnectionString extends IDbConnectionString {
@@ -66,13 +66,25 @@ export default class PostgreSqlDriver extends BaseDriver {
66
66
  return this.myCompiler;
67
67
  }
68
68
 
69
- private transaction: IPooledObject<pg.Client>;
70
69
  private myCompiler = new QueryCompiler();
71
70
 
72
71
  constructor(private readonly config: IPgSqlConnectionString) {
73
72
  super(config);
74
73
  }
75
74
 
75
+ newConnection(): BaseConnection {
76
+ return new PostgreSqlConnection(this);
77
+ }
78
+ }
79
+
80
+ class PostgreSqlConnection extends BaseConnection {
81
+
82
+ private transaction: IPooledObject<pg.Client>;
83
+
84
+ private get config() {
85
+ return this.connectionString;
86
+ }
87
+
76
88
  public async createTransaction(): Promise<IBaseTransaction> {
77
89
  const tx = await this.getConnection();
78
90
  await tx.query("BEGIN");
@@ -102,6 +114,7 @@ export default class PostgreSqlDriver extends BaseDriver {
102
114
  const q = toQuery(command);
103
115
  text = q.text;
104
116
  const result = await connection.query(q.text, q.values);
117
+ (result as any).updated = result.rowCount;
105
118
  return result;
106
119
  } catch (error) {
107
120
  throw new Error(`Failed executing ${text}\n${error}`);
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable no-console */
2
2
  import QueryCompiler from "../../compiler/QueryCompiler.js";
3
3
  import Migrations from "../../migrations/Migrations.js";
4
- import { BaseDriver, IBaseTransaction, IDbConnectionString, IDbReader, IQuery, IRecord, disposableSymbol, toQuery } from "../base/BaseDriver.js";
4
+ import { BaseConnection, BaseDriver, IBaseTransaction, IDbConnectionString, IDbReader, IQuery, IRecord, disposableSymbol, toQuery } from "../base/BaseDriver.js";
5
5
  import sql from "mssql";
6
6
  import SqlServerQueryCompiler from "./SqlServerQueryCompiler.js";
7
7
  import SqlServerAutomaticMigrations from "../../migrations/sql-server/SqlServerAutomaticMigrations.js";
@@ -19,13 +19,29 @@ export default class SqlServerDriver extends BaseDriver {
19
19
  }
20
20
 
21
21
  private sqlQueryCompiler = new SqlServerQueryCompiler();
22
- private transaction: sql.Transaction;
23
22
 
24
23
  constructor(private readonly config: ISqlServerConnectionString) {
25
24
  super(config);
26
25
  config.server = config.host;
27
26
  }
28
27
 
28
+ newConnection(): BaseConnection {
29
+ return new SqlServerConnection(this, this.config);
30
+ }
31
+ }
32
+
33
+ export class SqlServerConnection extends BaseConnection {
34
+
35
+ private transaction: sql.Transaction;
36
+
37
+ private get sqlQueryCompiler() {
38
+ return this.compiler as SqlServerQueryCompiler;
39
+ }
40
+
41
+ constructor(driver, private config: ISqlServerConnectionString) {
42
+ super(driver);
43
+ }
44
+
29
45
  public async executeReader(command: IQuery, signal?: AbortSignal): Promise<IDbReader> {
30
46
  command = toQuery(command);
31
47
  let rq = await this.newRequest();
@@ -64,15 +80,10 @@ export default class SqlServerDriver extends BaseDriver {
64
80
 
65
81
  public ensureDatabase() {
66
82
  const create = async () => {
67
- const defaultDb = "master";
68
-
83
+ const config = { ... this.config, database: "master" };
69
84
  const db = this.config.database;
70
- this.config.database = defaultDb;
71
85
 
72
- const connection = await this.newRequest();
73
- // @ts-expect-error readonly
74
- this.config = { ... this.config };
75
- this.config.database = db;
86
+ const connection = await this.newConnection(config);
76
87
 
77
88
  const createSql = `IF NOT EXISTS (SELECT name FROM master.dbo.sysdatabases WHERE name = ${SqlServerLiteral.escapeLiteral(db)}) BEGIN
78
89
  CREATE DATABASE ${db};
@@ -137,8 +148,7 @@ export default class SqlServerDriver extends BaseDriver {
137
148
  return (await this.newConnection()).request();
138
149
  }
139
150
 
140
- private newConnection() {
141
- const config = this.config;
151
+ private newConnection(config = this.config) {
142
152
  const key = config.server + "//" + config.database + "/" + config.user;
143
153
  return namedPool.getOrCreateAsync(config.server + "://" + config.database,
144
154
  () => {
@@ -1,3 +1,10 @@
1
+ import TimeSpan from "../types/TimeSpan.js";
1
2
 
2
3
 
3
- export class ActivitySuspendedError extends Error { }
4
+ export class ActivitySuspendedError extends Error {
5
+
6
+ constructor(public ttl: TimeSpan = TimeSpan.fromDays(1)) {
7
+ super("Activity Suspended");
8
+ }
9
+
10
+ }
@@ -40,6 +40,8 @@ function bindStep(context: EternityContext, store: WorkflowStorage, name: string
40
40
 
41
41
  store.lastID = id;
42
42
 
43
+ let ttl = TimeSpan.fromSeconds(0);
44
+
43
45
  const step: Partial<WorkflowStorage> = {
44
46
  id,
45
47
  parentID: this.id,
@@ -63,10 +65,13 @@ function bindStep(context: EternityContext, store: WorkflowStorage, name: string
63
65
  const eta = this.currentTime.add(maxTS);
64
66
  step.eta = eta;
65
67
 
68
+ ttl = maxTS;
69
+
66
70
  if (eta <= start) {
67
71
  // time is up...
68
72
  lastResult = "";
69
73
  step.state = "done";
74
+ ttl = TimeSpan.fromSeconds(0);
70
75
  }
71
76
 
72
77
  } else {
@@ -74,9 +79,11 @@ function bindStep(context: EternityContext, store: WorkflowStorage, name: string
74
79
  try {
75
80
 
76
81
  const types = old[injectServiceKeysSymbol] as any[];
77
- for (let index = a.length; index < types.length; index++) {
78
- const element = ServiceProvider.resolve(this, types[index]);
79
- a.push(element);
82
+ if (types) {
83
+ for (let index = a.length; index < types.length; index++) {
84
+ const element = ServiceProvider.resolve(this, types[index]);
85
+ a.push(element);
86
+ }
80
87
  }
81
88
  lastResult = (await old.apply(this, a)) ?? 0;
82
89
  step.output = JSON.stringify(lastResult);
@@ -101,7 +108,7 @@ function bindStep(context: EternityContext, store: WorkflowStorage, name: string
101
108
  throw lastError;
102
109
  }
103
110
  if (step.state !== "done") {
104
- throw new ActivitySuspendedError();
111
+ throw new ActivitySuspendedError(ttl);
105
112
  }
106
113
  return lastResult;
107
114
  };
@@ -163,7 +170,12 @@ export default class EternityContext {
163
170
  public async queue<T>(
164
171
  type: IClassOf<Workflow<T>>,
165
172
  input: Partial<T>,
166
- { id, throwIfExists, eta }: { id?: string, throwIfExists?: boolean, eta?: DateTime } = {}) {
173
+ { id, throwIfExists, eta, parentID }: {
174
+ id?: string,
175
+ throwIfExists?: boolean,
176
+ eta?: DateTime,
177
+ parentID?: string
178
+ } = {}) {
167
179
  const clock = this.storage.clock;
168
180
  if (id) {
169
181
  const r = await this.storage.get(id);
@@ -193,6 +205,7 @@ export default class EternityContext {
193
205
  isWorkflow: true,
194
206
  queued: now,
195
207
  updated: now,
208
+ parentID,
196
209
  eta
197
210
  });
198
211
 
@@ -216,6 +229,32 @@ export default class EternityContext {
216
229
 
217
230
  return pending.length;
218
231
  }
232
+
233
+ async runChild(w: Workflow, type, input) {
234
+
235
+ // there might still be some workflows pending
236
+ // this will ensure even empty workflow !!
237
+ const schema = WorkflowRegistry.register(type, void 0);
238
+
239
+ const id = w.id + `-child(${schema.name},${JSON.stringify(input)})`;
240
+
241
+
242
+ const result = await this.storage.get(id);
243
+ if (result) {
244
+ const { state } = result;
245
+ if (state === "done") {
246
+ return JSON.parse(result.output);
247
+ }
248
+ if (state === "failed") {
249
+ throw new Error(result.error);
250
+ }
251
+ throw new ActivitySuspendedError();
252
+ }
253
+
254
+ await this.queue(type, input, { id, parentID: w.id });
255
+ throw new ActivitySuspendedError();
256
+ }
257
+
219
258
  private async run(workflow: WorkflowStorage) {
220
259
 
221
260
  const clock = this.storage.clock;
@@ -235,7 +274,7 @@ export default class EternityContext {
235
274
  const schema = WorkflowRegistry.getByName(workflow.name);
236
275
  const { eta, id, updated } = workflow;
237
276
  const input = JSON.parse(workflow.input);
238
- const instance = new (schema.type)({ input, eta, id, currentTime: DateTime.from(updated) });
277
+ const instance = new (schema.type)({ input, eta, id, currentTime: DateTime.from(updated) }, this);
239
278
  for (const iterator of schema.activities) {
240
279
  instance[iterator] = bindStep(this, workflow, iterator, instance[iterator]);
241
280
  }
@@ -251,6 +290,9 @@ export default class EternityContext {
251
290
  } catch (error) {
252
291
  if (error instanceof ActivitySuspendedError) {
253
292
  // this will update last id...
293
+ workflow.eta = clock.utcNow.add(error.ttl);
294
+ workflow.lockedTTL = null;
295
+ workflow.lockToken = null;
254
296
  await this.storage.save(workflow);
255
297
  return;
256
298
  }
@@ -260,8 +302,29 @@ export default class EternityContext {
260
302
  workflow.eta = clock.utcNow.add(instance.failedPreserveTime);
261
303
  }
262
304
 
305
+ // in case of child workflow...
306
+ // eta will be set to one year...
307
+ if (workflow.parentID) {
308
+ workflow.eta = clock.utcNow.addYears(1);
309
+ // since we have finished.. we should
310
+ // make parent's eta approach now sooner..
311
+ }
312
+
313
+ workflow.lockedTTL = null;
314
+ workflow.lockToken = null;
263
315
  await this.storage.save(workflow);
264
316
 
317
+ if (workflow.parentID) {
318
+ const parent = await this.storage.get(workflow.parentID);
319
+ if (parent) {
320
+ parent.lockTTL = null;
321
+ parent.lockToken = null;
322
+ parent.eta = clock.utcNow;
323
+ await this.storage.save(parent);
324
+ }
325
+ }
326
+ // workflow finished successfully...
327
+
265
328
  } finally {
266
329
  scope.dispose();
267
330
  }
@@ -1,11 +1,14 @@
1
+ import { randomUUID } from "crypto";
1
2
  import Column from "../decorators/Column.js";
2
3
  import Index from "../decorators/Index.js";
3
4
  import Table from "../decorators/Table.js";
4
5
  import Inject, { RegisterScoped, RegisterSingleton, ServiceProvider } from "../di/di.js";
5
6
  import { BaseDriver } from "../drivers/base/BaseDriver.js";
6
7
  import EntityContext from "../model/EntityContext.js";
8
+ import { BinaryExpression, CallExpression, Expression, NullExpression, NumberLiteral, UpdateStatement } from "../query/ast/Expressions.js";
7
9
  import DateTime from "../types/DateTime.js";
8
10
  import WorkflowClock from "./WorkflowClock.js";
11
+ import RawQuery from "../compiler/RawQuery.js";
9
12
 
10
13
  @Table("Workflows")
11
14
  @Index({
@@ -53,6 +56,9 @@ export class WorkflowStorage {
53
56
  @Column({ nullable: true })
54
57
  public lockedTTL: DateTime;
55
58
 
59
+ @Column({ nullable: true })
60
+ public lockToken: string;
61
+
56
62
  @Column({ dataType: "AsciiChar", length: 10})
57
63
  public state: "queued" | "failed" | "done";
58
64
 
@@ -83,6 +89,8 @@ class WorkflowContext extends EntityContext {
83
89
  @RegisterSingleton
84
90
  export default class EternityStorage {
85
91
 
92
+ private lockQuery: RawQuery;
93
+
86
94
  constructor(
87
95
  @Inject
88
96
  private driver: BaseDriver,
@@ -97,6 +105,10 @@ export default class EternityStorage {
97
105
  const r = await db.workflows.where({ id }, (p) => (x) => x.id === p.id && x.isWorkflow === true).first();
98
106
  if (r !== null) {
99
107
  return {
108
+ id,
109
+ parentID: r.parentID,
110
+ lockTTL: r.lockedTTL,
111
+ lockToken: r.lockToken,
100
112
  updated: r.updated,
101
113
  eta: r.eta,
102
114
  queued: r.queued,
@@ -131,7 +143,8 @@ export default class EternityStorage {
131
143
 
132
144
  async save(state: Partial<WorkflowStorage>) {
133
145
  const db = new WorkflowContext(this.driver);
134
- await this.driver.runInTransaction(async () => {
146
+ const connection = db.connection;
147
+ await connection.runInTransaction(async () => {
135
148
  let w = await db.workflows.where(state, (p) => (x) => x.id === p.id).first();
136
149
  if (!w) {
137
150
  w = db.workflows.add(state);
@@ -145,6 +158,7 @@ export default class EternityStorage {
145
158
  }
146
159
 
147
160
  w.state ||= "queued";
161
+ w.updated = DateTime.utcNow;
148
162
  await db.saveChanges();
149
163
  });
150
164
  }
@@ -152,29 +166,75 @@ export default class EternityStorage {
152
166
  async dequeue(signal?: AbortSignal) {
153
167
  const db = new WorkflowContext(this.driver);
154
168
  const now = this.clock.utcNow;
155
- const lockedTTL = now.addMinutes(1);
156
- return db.driver.runInTransaction(async () => {
157
- const list = await db.workflows
158
- .where({now}, (p) => (x) => x.eta <= p.now
159
- && (x.lockedTTL === null || x.lockedTTL <= p.now)
160
- && x.isWorkflow === true)
161
- .orderBy({}, (p) => (x) => x.eta)
162
- .thenBy({}, (p) => (x) => x.priority)
163
- .limit(20)
164
- .withSignal(signal)
165
- .toArray();
166
- for (const iterator of list) {
167
- iterator.lockedTTL = lockedTTL;
169
+
170
+ if(!this.lockQuery) {
171
+
172
+ const type = db.model.getEntityType(WorkflowStorage);
173
+
174
+ const px = Expression.parameter("x");
175
+ const lockTokenField = type.getProperty("lockToken").field.columnName;
176
+ const lockTTLField = type.getProperty("lockedTTL").field.columnName;
177
+
178
+ const exp = UpdateStatement.create({
179
+ table: type.fullyQualifiedName,
180
+ set: [
181
+ Expression.assign(
182
+ Expression.identifier(lockTokenField),
183
+ Expression.member(px, "lockToken")
184
+ ),
185
+ Expression.assign(
186
+ Expression.identifier(lockTTLField),
187
+ CallExpression.create({
188
+ callee: Expression.member(Expression.member(Expression.identifier("Sql"), "date"), "addMinutes"),
189
+ arguments: [CallExpression.create({
190
+ callee: Expression.member(Expression.member(Expression.identifier("Sql"), "date"), "now")
191
+ }),
192
+ NumberLiteral.create({ value: 5 })
193
+ ]
194
+ })
195
+ )
196
+ ],
197
+ where: Expression.logicalAnd(Expression.equal(
198
+ Expression.identifier("id"),
199
+ Expression.member(px, "id")
200
+ ), Expression.equal(
201
+ Expression.identifier(lockTokenField),
202
+ NullExpression.create({})
203
+ ))
204
+ });
205
+
206
+ this.lockQuery = this.driver.compiler.compileToRawQuery(null, exp, px);
207
+ }
208
+
209
+ const q = this.lockQuery;
210
+
211
+ const items = await db.workflows
212
+ .where({now}, (p) => (x) => x.eta <= p.now
213
+ && (x.lockedTTL === null || x.lockedTTL <= p.now)
214
+ && x.lockToken === null
215
+ && x.isWorkflow === true)
216
+ .orderBy({}, (p) => (x) => x.eta)
217
+ .thenBy({}, (p) => (x) => x.priority)
218
+ .limit(20)
219
+ .withSignal(signal)
220
+ .toArray();
221
+ const list: WorkflowStorage[] = [];
222
+ const uuid = randomUUID();
223
+ for (const iterator of items) {
224
+ // try to acquire lock...
225
+ iterator.lockToken = uuid;
226
+ const r = await q.invoke(db.connection, iterator);
227
+ if (r.updated > 0) {
228
+ list.push(iterator);
168
229
  }
169
- await db.saveChanges(signal);
170
- return list;
171
- });
230
+ }
231
+ return list;
172
232
  }
173
233
 
174
234
  async seed() {
175
235
  const db = new WorkflowContext(this.driver);
176
- await db.driver.ensureDatabase();
177
- await db.driver.automaticMigrations().migrate(db);
236
+ await db.connection.ensureDatabase();
237
+ await db.connection.automaticMigrations().migrate(db);
178
238
  }
179
239
 
180
240
  }
@@ -1,6 +1,8 @@
1
+ import { IClassOf } from "../decorators/IClassOf.js";
1
2
  import Inject from "../di/di.js";
2
3
  import DateTime from "../types/DateTime.js";
3
4
  import TimeSpan from "../types/TimeSpan.js";
5
+ import { ActivitySuspendedError } from "./ActivitySuspendedError.js";
4
6
  import EternityContext from "./EternityContext.js";
5
7
  import { WorkflowRegistry } from "./WorkflowRegistry.js";
6
8
 
@@ -34,25 +36,63 @@ export default abstract class Workflow<TIn = any, TOut = any> {
34
36
 
35
37
  public failedPreserveTime: TimeSpan = TimeSpan.fromDays(1);
36
38
 
37
- @Inject
38
- protected context: EternityContext;
39
-
40
- constructor(p: Partial<Workflow>) {
41
- Object.setPrototypeOf(p, new.target.prototype);
42
- const w = p as Workflow;
43
- w.preserveTime = TimeSpan.fromMinutes(5);
44
- w.failedPreserveTime = TimeSpan.fromDays(1);
45
- return w;
39
+ constructor(
40
+ {
41
+ sequence,
42
+ input,
43
+ id,
44
+ eta,
45
+ currentTime
46
+ }: {
47
+ sequence: string,
48
+ input: TIn,
49
+ id: string,
50
+ eta: DateTime,
51
+ currentTime: DateTime
52
+ },
53
+ protected context: EternityContext
54
+ ) {
55
+ this.input = input;
56
+ this.id = id;
57
+ this.eta = eta;
58
+ this.currentTime = currentTime;
59
+ this.sequence = sequence;
46
60
  }
47
61
 
48
62
  public abstract run(): Promise<TOut>;
49
63
 
50
- public delay(ts: TimeSpan) {
64
+ protected delay(ts: TimeSpan) {
51
65
  return Promise.resolve("");
52
66
  }
53
67
 
54
- public waitForExternalEvent(ts: TimeSpan, ... names: string[]) {
68
+ protected waitForExternalEvent(ts: TimeSpan, ... names: string[]) {
55
69
  // do nothing...
56
70
  return Promise.resolve("");
57
71
  }
72
+
73
+ protected async runChild<TChildIn, TChildOut>(type: IClassOf<Workflow<TChildIn, TChildOut>>, input: TChildIn): Promise<TChildOut> {
74
+ return this.context.runChild(this, type, input);
75
+ }
76
+
77
+ protected async all<T extends readonly unknown[] | []>(values: T): Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }>{
78
+ let suspended: ActivitySuspendedError;
79
+ try {
80
+ const r = await Promise.all(values.map(async (x) => {
81
+ try {
82
+ return await x;
83
+ } catch (error) {
84
+ if (error instanceof ActivitySuspendedError) {
85
+ suspended = error;
86
+ }
87
+ throw error;
88
+ }
89
+ }));
90
+ return r as any;
91
+ } catch (error) {
92
+ if (suspended) {
93
+ throw suspended;
94
+ }
95
+ throw error;
96
+ }
97
+ }
58
98
  }
@@ -1,6 +1,6 @@
1
1
  import { IColumn } from "../../decorators/IColumn.js";
2
2
  import { IIndex } from "../../decorators/IIndex.js";
3
- import { BaseDriver } from "../../drivers/base/BaseDriver.js";
3
+ import { BaseConnection, BaseDriver } from "../../drivers/base/BaseDriver.js";
4
4
  import EntityType from "../../entity-query/EntityType.js";
5
5
  import EntityContext from "../../model/EntityContext.js";
6
6
  import Migrations from "../Migrations.js";
@@ -15,7 +15,7 @@ export default class PostgresAutomaticMigrations extends PostgresMigrations {
15
15
  const nonKeyColumns = type.nonKeys;
16
16
  const keys = type.keys;
17
17
 
18
- const driver = context.driver;
18
+ const driver = context.connection;
19
19
 
20
20
  await this.createTable(driver, type, keys);
21
21
 
@@ -42,7 +42,7 @@ export default class PostgresAutomaticMigrations extends PostgresMigrations {
42
42
  }
43
43
  }
44
44
 
45
- async createColumns(driver: BaseDriver, type: EntityType, nonKeyColumns: IColumn[]) {
45
+ async createColumns(driver: BaseConnection, type: EntityType, nonKeyColumns: IColumn[]) {
46
46
 
47
47
  const name = type.schema
48
48
  ? type.schema + "." + type.name
@@ -67,7 +67,7 @@ export default class PostgresAutomaticMigrations extends PostgresMigrations {
67
67
 
68
68
  }
69
69
 
70
- async createTable(driver: BaseDriver, type: EntityType, keys: IColumn[]) {
70
+ async createTable(driver: BaseConnection, type: EntityType, keys: IColumn[]) {
71
71
 
72
72
  const name = type.schema
73
73
  ? type.schema + "." + type.name
@@ -98,7 +98,7 @@ export default class PostgresAutomaticMigrations extends PostgresMigrations {
98
98
  }
99
99
 
100
100
  async migrateIndex(context: EntityContext, index: IIndex, type: EntityType) {
101
- const driver = context.driver;
101
+ const driver = context.connection;
102
102
  const name = type.schema
103
103
  ? type.schema + "." + type.name
104
104
  : type.name;