@dudousxd/nestjs-durable-store-mikro-orm 0.28.1 → 0.30.0

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.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _mikro_orm_core from '@mikro-orm/core';
2
2
  import { EntityRepository, EntityRepositoryType, EntitySchema, MikroORM } from '@mikro-orm/core';
3
- import { StepKind, RunStatus, StateStore, WorkflowRun, RetentionPolicy, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, ScheduleRecord, ScheduleQuery, SignalWaiter, ChildWait } from '@dudousxd/nestjs-durable-core';
3
+ import { StepKind, RunStatus, StateStore, WorkflowRun, RetentionPolicy, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, ScheduleRecord, ScheduleQuery, CheckpointStat, CheckpointQuery, SignalWaiter, ChildWait } from '@dudousxd/nestjs-durable-core';
4
4
 
5
5
  /**
6
6
  * Custom repositories for the durable entities, so a host app can `@InjectRepository`/inject a
@@ -198,13 +198,14 @@ declare class MikroOrmStateStore implements StateStore {
198
198
  getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
199
199
  saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
200
200
  saveCheckpointIfPending(checkpoint: StepCheckpoint): Promise<boolean>;
201
+ saveCheckpointsIfPending(checkpoints: StepCheckpoint[]): Promise<boolean[]>;
201
202
  transaction<T>(work: (tx: {
202
203
  raw: unknown;
203
204
  saveCheckpoint: (cp: StepCheckpoint) => Promise<void>;
204
205
  }) => Promise<T>): Promise<T>;
205
206
  listIncompleteRuns(namespace?: string): Promise<WorkflowRun[]>;
206
207
  listPendingRuns(limit: number, namespace?: string): Promise<WorkflowRun[]>;
207
- listDueTimers(nowMs: number, namespace?: string): Promise<WorkflowRun[]>;
208
+ listDueTimers(nowMs: number, namespace?: string, limit?: number): Promise<WorkflowRun[]>;
208
209
  tryLockRun(runId: string, owner: string, leaseUntilMs: number, nowMs: number): Promise<boolean>;
209
210
  releaseRunLock(runId: string): Promise<void>;
210
211
  renewRunLock(runId: string, owner: string, leaseUntilMs: number): Promise<boolean>;
@@ -288,7 +289,21 @@ declare class MikroOrmStateStore implements StateStore {
288
289
  listSchedules(query: ScheduleQuery): Promise<ScheduleRecord[]>;
289
290
  listCheckpoints(runId: string): Promise<StepCheckpoint[]>;
290
291
  getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
292
+ countCheckpoints(runId: string, fromSeq: number, toSeq: number, statuses: StepCheckpoint['status'][]): Promise<number>;
291
293
  listCheckpointsByNamePrefix(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
294
+ /** One `GROUP BY parallel_group, status, <name kind>` over the run's checkpoints — the buckets a
295
+ * run detail summarizes a fan from, without loading its rows. The name-kind `CASE` is the SQL of
296
+ * `checkpointNameKind`, and `min(coalesce(enqueued_at, started_at))` reads `enqueuedAt` the way
297
+ * {@link listCheckpoints} does. Raw SQL on metadata-resolved names, so a custom column naming
298
+ * strategy is honoured. */
299
+ checkpointStats(runId: string, opts?: {
300
+ group?: string | undefined;
301
+ }): Promise<CheckpointStat[]>;
302
+ /** The run's checkpoints matching `query`, in `seq` order and paged in the database — a fan's
303
+ * failures or one page of its members without the rest of the run. An excluded group keeps the
304
+ * ungrouped rows (`parallel_group is null or not in (...)`); an empty `statuses`/`namePrefixes`
305
+ * matches nothing, so it returns before querying. */
306
+ queryCheckpoints(runId: string, query: CheckpointQuery): Promise<StepCheckpoint[]>;
292
307
  putSignalWaiter(waiter: SignalWaiter): Promise<void>;
293
308
  takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
294
309
  listSignalWaiters(prefix: string): Promise<SignalWaiter[]>;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _mikro_orm_core from '@mikro-orm/core';
2
2
  import { EntityRepository, EntityRepositoryType, EntitySchema, MikroORM } from '@mikro-orm/core';
3
- import { StepKind, RunStatus, StateStore, WorkflowRun, RetentionPolicy, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, ScheduleRecord, ScheduleQuery, SignalWaiter, ChildWait } from '@dudousxd/nestjs-durable-core';
3
+ import { StepKind, RunStatus, StateStore, WorkflowRun, RetentionPolicy, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, ScheduleRecord, ScheduleQuery, CheckpointStat, CheckpointQuery, SignalWaiter, ChildWait } from '@dudousxd/nestjs-durable-core';
4
4
 
5
5
  /**
6
6
  * Custom repositories for the durable entities, so a host app can `@InjectRepository`/inject a
@@ -198,13 +198,14 @@ declare class MikroOrmStateStore implements StateStore {
198
198
  getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
199
199
  saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
200
200
  saveCheckpointIfPending(checkpoint: StepCheckpoint): Promise<boolean>;
201
+ saveCheckpointsIfPending(checkpoints: StepCheckpoint[]): Promise<boolean[]>;
201
202
  transaction<T>(work: (tx: {
202
203
  raw: unknown;
203
204
  saveCheckpoint: (cp: StepCheckpoint) => Promise<void>;
204
205
  }) => Promise<T>): Promise<T>;
205
206
  listIncompleteRuns(namespace?: string): Promise<WorkflowRun[]>;
206
207
  listPendingRuns(limit: number, namespace?: string): Promise<WorkflowRun[]>;
207
- listDueTimers(nowMs: number, namespace?: string): Promise<WorkflowRun[]>;
208
+ listDueTimers(nowMs: number, namespace?: string, limit?: number): Promise<WorkflowRun[]>;
208
209
  tryLockRun(runId: string, owner: string, leaseUntilMs: number, nowMs: number): Promise<boolean>;
209
210
  releaseRunLock(runId: string): Promise<void>;
210
211
  renewRunLock(runId: string, owner: string, leaseUntilMs: number): Promise<boolean>;
@@ -288,7 +289,21 @@ declare class MikroOrmStateStore implements StateStore {
288
289
  listSchedules(query: ScheduleQuery): Promise<ScheduleRecord[]>;
289
290
  listCheckpoints(runId: string): Promise<StepCheckpoint[]>;
290
291
  getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
292
+ countCheckpoints(runId: string, fromSeq: number, toSeq: number, statuses: StepCheckpoint['status'][]): Promise<number>;
291
293
  listCheckpointsByNamePrefix(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
294
+ /** One `GROUP BY parallel_group, status, <name kind>` over the run's checkpoints — the buckets a
295
+ * run detail summarizes a fan from, without loading its rows. The name-kind `CASE` is the SQL of
296
+ * `checkpointNameKind`, and `min(coalesce(enqueued_at, started_at))` reads `enqueuedAt` the way
297
+ * {@link listCheckpoints} does. Raw SQL on metadata-resolved names, so a custom column naming
298
+ * strategy is honoured. */
299
+ checkpointStats(runId: string, opts?: {
300
+ group?: string | undefined;
301
+ }): Promise<CheckpointStat[]>;
302
+ /** The run's checkpoints matching `query`, in `seq` order and paged in the database — a fan's
303
+ * failures or one page of its members without the rest of the run. An excluded group keeps the
304
+ * ungrouped rows (`parallel_group is null or not in (...)`); an empty `statuses`/`namePrefixes`
305
+ * matches nothing, so it returns before querying. */
306
+ queryCheckpoints(runId: string, query: CheckpointQuery): Promise<StepCheckpoint[]>;
292
307
  putSignalWaiter(waiter: SignalWaiter): Promise<void>;
293
308
  takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
294
309
  listSignalWaiters(prefix: string): Promise<SignalWaiter[]>;
package/dist/index.js CHANGED
@@ -337,6 +337,19 @@ function durableEntities(options = {}) {
337
337
  class: StepCheckpointEntity,
338
338
  tableName: "durable_step_checkpoints",
339
339
  repository: /* @__PURE__ */ __name(() => StepCheckpointRepository, "repository"),
340
+ // `(runId, parallelGroup, seq)` serves the run detail's per-group reads — `checkpointStats` with a
341
+ // group and `queryCheckpoints` paging one fan in `seq` order — without scanning the whole run.
342
+ // Mirrors the Prisma adapter's index of the same name.
343
+ indexes: [
344
+ {
345
+ name: "durable_step_checkpoints_group_idx",
346
+ properties: [
347
+ "runId",
348
+ "parallelGroup",
349
+ "seq"
350
+ ]
351
+ }
352
+ ],
340
353
  properties: {
341
354
  runId: {
342
355
  type: "string",
@@ -1078,6 +1091,25 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1078
1091
  });
1079
1092
  return affected > 0;
1080
1093
  }
1094
+ async saveCheckpointsIfPending(checkpoints) {
1095
+ if (checkpoints.length === 0) return [];
1096
+ return this.fork().transactional(async (em) => {
1097
+ const wrote = [];
1098
+ for (const checkpoint of checkpoints) {
1099
+ const { runId: _runId, seq: _seq, ...data } = toCheckpointEntity(checkpoint);
1100
+ const affected = await em.nativeUpdate(StepCheckpointEntity, {
1101
+ runId: checkpoint.runId,
1102
+ seq: checkpoint.seq,
1103
+ status: "pending"
1104
+ }, {
1105
+ ...data,
1106
+ wakeAt: checkpoint.wakeAt != null ? new Date(checkpoint.wakeAt) : null
1107
+ });
1108
+ wrote.push(affected > 0);
1109
+ }
1110
+ return wrote;
1111
+ });
1112
+ }
1081
1113
  async transaction(work) {
1082
1114
  return this.fork().transactional(async (em) => work({
1083
1115
  raw: em,
@@ -1116,7 +1148,7 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1116
1148
  });
1117
1149
  return rows.map(fromRunEntity);
1118
1150
  }
1119
- async listDueTimers(nowMs, namespace) {
1151
+ async listDueTimers(nowMs, namespace, limit) {
1120
1152
  const em = this.fork();
1121
1153
  const rows = await em.find(WorkflowRunEntity, {
1122
1154
  status: "suspended",
@@ -1127,6 +1159,13 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1127
1159
  ...namespace !== void 0 ? {
1128
1160
  namespace
1129
1161
  } : {}
1162
+ }, {
1163
+ orderBy: {
1164
+ wakeAt: "asc"
1165
+ },
1166
+ ...limit !== void 0 ? {
1167
+ limit
1168
+ } : {}
1130
1169
  });
1131
1170
  return rows.map(fromRunEntity);
1132
1171
  }
@@ -1563,6 +1602,19 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1563
1602
  });
1564
1603
  return entity ? fromCheckpointEntity(entity) : void 0;
1565
1604
  }
1605
+ async countCheckpoints(runId, fromSeq, toSeq, statuses) {
1606
+ if (statuses.length === 0) return 0;
1607
+ return this.fork().count(StepCheckpointEntity, {
1608
+ runId,
1609
+ seq: {
1610
+ $gte: fromSeq,
1611
+ $lte: toSeq
1612
+ },
1613
+ status: {
1614
+ $in: statuses
1615
+ }
1616
+ });
1617
+ }
1566
1618
  async listCheckpointsByNamePrefix(runId, prefixes) {
1567
1619
  if (prefixes.length === 0) return [];
1568
1620
  const em = this.fork();
@@ -1578,6 +1630,103 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1578
1630
  seq: "asc"
1579
1631
  }
1580
1632
  });
1633
+ return rows.filter((r) => prefixes.some((p) => r.name.startsWith(p))).map(fromCheckpointEntity);
1634
+ }
1635
+ /** One `GROUP BY parallel_group, status, <name kind>` over the run's checkpoints — the buckets a
1636
+ * run detail summarizes a fan from, without loading its rows. The name-kind `CASE` is the SQL of
1637
+ * `checkpointNameKind`, and `min(coalesce(enqueued_at, started_at))` reads `enqueuedAt` the way
1638
+ * {@link listCheckpoints} does. Raw SQL on metadata-resolved names, so a custom column naming
1639
+ * strategy is honoured. */
1640
+ async checkpointStats(runId, opts) {
1641
+ const em = this.fork();
1642
+ const quote = this.idQuote(em);
1643
+ const meta = em.getMetadata().get(StepCheckpointEntity);
1644
+ const c = /* @__PURE__ */ __name((prop) => quote(meta.properties[prop]?.fieldNames?.[0] ?? prop), "c");
1645
+ const nameKind = `case when ${c("name")} like 'signal:child:%' then 'child' when ${c("name")} like 'fan:%' then 'folded' when ${c("name")} like 'spawn:%' then 'spawn' else 'step' end`;
1646
+ const params = [
1647
+ runId
1648
+ ];
1649
+ let where = `${c("runId")} = ?`;
1650
+ if (opts?.group !== void 0) {
1651
+ where += ` and ${c("parallelGroup")} = ?`;
1652
+ params.push(opts.group);
1653
+ }
1654
+ const sql = `select ${c("parallelGroup")} as ${quote("grp")}, ${c("status")} as ${quote("status")}, ${nameKind} as ${quote("name_kind")}, count(*) as ${quote("n")}, min(${c("seq")}) as ${quote("min_seq")}, max(${c("seq")}) as ${quote("max_seq")}, min(${c("name")}) as ${quote("min_name")}, max(${c("name")}) as ${quote("max_name")}, min(coalesce(${c("enqueuedAt")}, ${c("startedAt")})) as ${quote("min_enqueued_at")}, min(${c("startedAt")}) as ${quote("min_started_at")}, max(${c("startedAt")}) as ${quote("max_started_at")}, max(${c("finishedAt")}) as ${quote("max_finished_at")}, sum(${c("attempts")}) as ${quote("sum_attempts")} from ${quote(meta.tableName)} where ${where} group by ${c("parallelGroup")}, ${c("status")}, ${nameKind}`;
1655
+ const rows = await em.getConnection().execute(sql, params);
1656
+ const date = aggregateDateReader(em);
1657
+ return rows.map((row) => ({
1658
+ group: row.grp ?? null,
1659
+ status: row.status,
1660
+ nameKind: row.name_kind,
1661
+ count: Number(row.n),
1662
+ minSeq: Number(row.min_seq),
1663
+ maxSeq: Number(row.max_seq),
1664
+ minName: String(row.min_name),
1665
+ maxName: String(row.max_name),
1666
+ minEnqueuedAt: date(row.min_enqueued_at),
1667
+ minStartedAt: date(row.min_started_at),
1668
+ maxStartedAt: date(row.max_started_at),
1669
+ maxFinishedAt: date(row.max_finished_at),
1670
+ sumAttempts: Number(row.sum_attempts)
1671
+ }));
1672
+ }
1673
+ /** The run's checkpoints matching `query`, in `seq` order and paged in the database — a fan's
1674
+ * failures or one page of its members without the rest of the run. An excluded group keeps the
1675
+ * ungrouped rows (`parallel_group is null or not in (...)`); an empty `statuses`/`namePrefixes`
1676
+ * matches nothing, so it returns before querying. */
1677
+ async queryCheckpoints(runId, query) {
1678
+ if (query.statuses?.length === 0 || query.namePrefixes?.length === 0) return [];
1679
+ const and = [
1680
+ {
1681
+ runId
1682
+ }
1683
+ ];
1684
+ if (query.group !== void 0) and.push({
1685
+ parallelGroup: query.group
1686
+ });
1687
+ if (query.excludeGroups?.length) {
1688
+ and.push({
1689
+ $or: [
1690
+ {
1691
+ parallelGroup: null
1692
+ },
1693
+ {
1694
+ parallelGroup: {
1695
+ $nin: query.excludeGroups
1696
+ }
1697
+ }
1698
+ ]
1699
+ });
1700
+ }
1701
+ if (query.statuses) and.push({
1702
+ status: {
1703
+ $in: query.statuses
1704
+ }
1705
+ });
1706
+ if (query.namePrefixes) {
1707
+ and.push({
1708
+ $or: query.namePrefixes.map((p) => ({
1709
+ name: {
1710
+ $like: `${p}%`
1711
+ }
1712
+ }))
1713
+ });
1714
+ }
1715
+ const rows = await this.fork().find(StepCheckpointEntity, {
1716
+ $and: and
1717
+ }, {
1718
+ orderBy: {
1719
+ seq: "asc"
1720
+ },
1721
+ // An offset alone is not valid SQL on SQLite/MySQL, so an unbounded page gets a limit no
1722
+ // run reaches.
1723
+ ...query.limit !== void 0 || query.offset ? {
1724
+ limit: query.limit ?? Number.MAX_SAFE_INTEGER
1725
+ } : {},
1726
+ ...query.offset ? {
1727
+ offset: query.offset
1728
+ } : {}
1729
+ });
1581
1730
  return rows.map(fromCheckpointEntity);
1582
1731
  }
1583
1732
  async putSignalWaiter(waiter) {
@@ -1612,7 +1761,7 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1612
1761
  $like: `${prefix}%`
1613
1762
  }
1614
1763
  });
1615
- return rows.map((e) => ({
1764
+ return rows.filter((e) => e.token.startsWith(prefix)).map((e) => ({
1616
1765
  token: e.token,
1617
1766
  runId: e.runId,
1618
1767
  seq: e.seq,
@@ -1794,6 +1943,21 @@ function fromRunEntity(e) {
1794
1943
  };
1795
1944
  }
1796
1945
  __name(fromRunEntity, "fromRunEntity");
1946
+ function aggregateDateReader(em) {
1947
+ const platform = em.getPlatform();
1948
+ const tz = platform.getTimezone();
1949
+ return (value) => {
1950
+ if (value instanceof Date) return value;
1951
+ if (typeof value === "bigint") return platform.parseDate(Number(value));
1952
+ if (typeof value === "number" || !tz || tz === "local") {
1953
+ return platform.parseDate(value);
1954
+ }
1955
+ const text = String(value);
1956
+ const zoned = text.includes("+") || text.lastIndexOf("-") > 10 || text.endsWith("Z");
1957
+ return platform.parseDate(zoned ? text : `${text}${tz}`);
1958
+ };
1959
+ }
1960
+ __name(aggregateDateReader, "aggregateDateReader");
1797
1961
  function toCheckpointEntity(cp) {
1798
1962
  const e = new StepCheckpointEntity();
1799
1963
  e.runId = cp.runId;