@dudousxd/nestjs-durable-store-mikro-orm 0.29.0 → 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,6 +198,7 @@ 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>;
@@ -290,6 +291,19 @@ declare class MikroOrmStateStore implements StateStore {
290
291
  getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
291
292
  countCheckpoints(runId: string, fromSeq: number, toSeq: number, statuses: StepCheckpoint['status'][]): Promise<number>;
292
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[]>;
293
307
  putSignalWaiter(waiter: SignalWaiter): Promise<void>;
294
308
  takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
295
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,6 +198,7 @@ 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>;
@@ -290,6 +291,19 @@ declare class MikroOrmStateStore implements StateStore {
290
291
  getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
291
292
  countCheckpoints(runId: string, fromSeq: number, toSeq: number, statuses: StepCheckpoint['status'][]): Promise<number>;
292
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[]>;
293
307
  putSignalWaiter(waiter: SignalWaiter): Promise<void>;
294
308
  takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
295
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,
@@ -1598,6 +1630,103 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1598
1630
  seq: "asc"
1599
1631
  }
1600
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
+ });
1601
1730
  return rows.map(fromCheckpointEntity);
1602
1731
  }
1603
1732
  async putSignalWaiter(waiter) {
@@ -1632,7 +1761,7 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1632
1761
  $like: `${prefix}%`
1633
1762
  }
1634
1763
  });
1635
- return rows.map((e) => ({
1764
+ return rows.filter((e) => e.token.startsWith(prefix)).map((e) => ({
1636
1765
  token: e.token,
1637
1766
  runId: e.runId,
1638
1767
  seq: e.seq,
@@ -1814,6 +1943,21 @@ function fromRunEntity(e) {
1814
1943
  };
1815
1944
  }
1816
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");
1817
1961
  function toCheckpointEntity(cp) {
1818
1962
  const e = new StepCheckpointEntity();
1819
1963
  e.runId = cp.runId;