@dudousxd/nestjs-durable-store-mikro-orm 0.29.0 → 0.31.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.cjs CHANGED
@@ -379,6 +379,19 @@ function durableEntities(options = {}) {
379
379
  class: StepCheckpointEntity,
380
380
  tableName: "durable_step_checkpoints",
381
381
  repository: /* @__PURE__ */ __name(() => StepCheckpointRepository, "repository"),
382
+ // `(runId, parallelGroup, seq)` serves the run detail's per-group reads — `checkpointStats` with a
383
+ // group and `queryCheckpoints` paging one fan in `seq` order — without scanning the whole run.
384
+ // Mirrors the Prisma adapter's index of the same name.
385
+ indexes: [
386
+ {
387
+ name: "durable_step_checkpoints_group_idx",
388
+ properties: [
389
+ "runId",
390
+ "parallelGroup",
391
+ "seq"
392
+ ]
393
+ }
394
+ ],
382
395
  properties: {
383
396
  runId: {
384
397
  type: "string",
@@ -998,6 +1011,30 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
998
1011
  await em.flush();
999
1012
  if ("searchAttributes" in patch) await this.reindexAttributes(runId, patch.searchAttributes);
1000
1013
  }
1014
+ async updateRunIf(runId, expectedStatuses, inputPatch) {
1015
+ if (expectedStatuses.length === 0) return false;
1016
+ const patch = inputPatch.status && import_nestjs_durable_core.TERMINAL_RUN_STATUSES.includes(inputPatch.status) ? await (0, import_nestjs_durable_core.clearTerminalSingletonAdmission)(runId, inputPatch, (id) => this.withScope({}).getRun(id)) : inputPatch;
1017
+ const data = runUpdateData(patch);
1018
+ const em = this.fork();
1019
+ let applied;
1020
+ if (Object.keys(data).length) {
1021
+ applied = await em.nativeUpdate(WorkflowRunEntity, {
1022
+ id: runId,
1023
+ status: {
1024
+ $in: expectedStatuses
1025
+ }
1026
+ }, data) > 0;
1027
+ } else {
1028
+ applied = await em.count(WorkflowRunEntity, {
1029
+ id: runId,
1030
+ status: {
1031
+ $in: expectedStatuses
1032
+ }
1033
+ }) > 0;
1034
+ }
1035
+ if (applied && "searchAttributes" in patch) await this.reindexAttributes(runId, patch.searchAttributes);
1036
+ return applied;
1037
+ }
1001
1038
  /** Rewrite a run's normalized attribute rows: delete the old set, insert the current one. Mirrors
1002
1039
  * the in-memory store's reindex so the side-table always reflects the run's live searchAttributes. */
1003
1040
  async reindexAttributes(runId, attributes, forked) {
@@ -1120,6 +1157,25 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1120
1157
  });
1121
1158
  return affected > 0;
1122
1159
  }
1160
+ async saveCheckpointsIfPending(checkpoints) {
1161
+ if (checkpoints.length === 0) return [];
1162
+ return this.fork().transactional(async (em) => {
1163
+ const wrote = [];
1164
+ for (const checkpoint of checkpoints) {
1165
+ const { runId: _runId, seq: _seq, ...data } = toCheckpointEntity(checkpoint);
1166
+ const affected = await em.nativeUpdate(StepCheckpointEntity, {
1167
+ runId: checkpoint.runId,
1168
+ seq: checkpoint.seq,
1169
+ status: "pending"
1170
+ }, {
1171
+ ...data,
1172
+ wakeAt: checkpoint.wakeAt != null ? new Date(checkpoint.wakeAt) : null
1173
+ });
1174
+ wrote.push(affected > 0);
1175
+ }
1176
+ return wrote;
1177
+ });
1178
+ }
1123
1179
  async transaction(work) {
1124
1180
  return this.fork().transactional(async (em) => work({
1125
1181
  raw: em,
@@ -1143,6 +1199,37 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1143
1199
  });
1144
1200
  return rows.map(fromRunEntity);
1145
1201
  }
1202
+ async listOrphanedRuns(nowMs, limit, namespace) {
1203
+ if (limit <= 0) return [];
1204
+ const em = this.fork();
1205
+ const rows = await em.find(WorkflowRunEntity, {
1206
+ status: {
1207
+ $in: [
1208
+ "running",
1209
+ "cancelling"
1210
+ ]
1211
+ },
1212
+ $or: [
1213
+ {
1214
+ lockedUntil: null
1215
+ },
1216
+ {
1217
+ lockedUntil: {
1218
+ $lte: new Date(nowMs)
1219
+ }
1220
+ }
1221
+ ],
1222
+ ...namespace !== void 0 ? {
1223
+ namespace
1224
+ } : {}
1225
+ }, {
1226
+ orderBy: {
1227
+ createdAt: "ASC"
1228
+ },
1229
+ limit
1230
+ });
1231
+ return rows.map(fromRunEntity);
1232
+ }
1146
1233
  async listPendingRuns(limit, namespace) {
1147
1234
  const em = this.fork();
1148
1235
  const rows = await em.find(WorkflowRunEntity, {
@@ -1640,6 +1727,103 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1640
1727
  seq: "asc"
1641
1728
  }
1642
1729
  });
1730
+ return rows.filter((r) => prefixes.some((p) => r.name.startsWith(p))).map(fromCheckpointEntity);
1731
+ }
1732
+ /** One `GROUP BY parallel_group, status, <name kind>` over the run's checkpoints — the buckets a
1733
+ * run detail summarizes a fan from, without loading its rows. The name-kind `CASE` is the SQL of
1734
+ * `checkpointNameKind`, and `min(coalesce(enqueued_at, started_at))` reads `enqueuedAt` the way
1735
+ * {@link listCheckpoints} does. Raw SQL on metadata-resolved names, so a custom column naming
1736
+ * strategy is honoured. */
1737
+ async checkpointStats(runId, opts) {
1738
+ const em = this.fork();
1739
+ const quote = this.idQuote(em);
1740
+ const meta = em.getMetadata().get(StepCheckpointEntity);
1741
+ const c = /* @__PURE__ */ __name((prop) => quote(meta.properties[prop]?.fieldNames?.[0] ?? prop), "c");
1742
+ 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`;
1743
+ const params = [
1744
+ runId
1745
+ ];
1746
+ let where = `${c("runId")} = ?`;
1747
+ if (opts?.group !== void 0) {
1748
+ where += ` and ${c("parallelGroup")} = ?`;
1749
+ params.push(opts.group);
1750
+ }
1751
+ 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}`;
1752
+ const rows = await em.getConnection().execute(sql, params);
1753
+ const date = aggregateDateReader(em);
1754
+ return rows.map((row) => ({
1755
+ group: row.grp ?? null,
1756
+ status: row.status,
1757
+ nameKind: row.name_kind,
1758
+ count: Number(row.n),
1759
+ minSeq: Number(row.min_seq),
1760
+ maxSeq: Number(row.max_seq),
1761
+ minName: String(row.min_name),
1762
+ maxName: String(row.max_name),
1763
+ minEnqueuedAt: date(row.min_enqueued_at),
1764
+ minStartedAt: date(row.min_started_at),
1765
+ maxStartedAt: date(row.max_started_at),
1766
+ maxFinishedAt: date(row.max_finished_at),
1767
+ sumAttempts: Number(row.sum_attempts)
1768
+ }));
1769
+ }
1770
+ /** The run's checkpoints matching `query`, in `seq` order and paged in the database — a fan's
1771
+ * failures or one page of its members without the rest of the run. An excluded group keeps the
1772
+ * ungrouped rows (`parallel_group is null or not in (...)`); an empty `statuses`/`namePrefixes`
1773
+ * matches nothing, so it returns before querying. */
1774
+ async queryCheckpoints(runId, query) {
1775
+ if (query.statuses?.length === 0 || query.namePrefixes?.length === 0) return [];
1776
+ const and = [
1777
+ {
1778
+ runId
1779
+ }
1780
+ ];
1781
+ if (query.group !== void 0) and.push({
1782
+ parallelGroup: query.group
1783
+ });
1784
+ if (query.excludeGroups?.length) {
1785
+ and.push({
1786
+ $or: [
1787
+ {
1788
+ parallelGroup: null
1789
+ },
1790
+ {
1791
+ parallelGroup: {
1792
+ $nin: query.excludeGroups
1793
+ }
1794
+ }
1795
+ ]
1796
+ });
1797
+ }
1798
+ if (query.statuses) and.push({
1799
+ status: {
1800
+ $in: query.statuses
1801
+ }
1802
+ });
1803
+ if (query.namePrefixes) {
1804
+ and.push({
1805
+ $or: query.namePrefixes.map((p) => ({
1806
+ name: {
1807
+ $like: `${p}%`
1808
+ }
1809
+ }))
1810
+ });
1811
+ }
1812
+ const rows = await this.fork().find(StepCheckpointEntity, {
1813
+ $and: and
1814
+ }, {
1815
+ orderBy: {
1816
+ seq: "asc"
1817
+ },
1818
+ // An offset alone is not valid SQL on SQLite/MySQL, so an unbounded page gets a limit no
1819
+ // run reaches.
1820
+ ...query.limit !== void 0 || query.offset ? {
1821
+ limit: query.limit ?? Number.MAX_SAFE_INTEGER
1822
+ } : {},
1823
+ ...query.offset ? {
1824
+ offset: query.offset
1825
+ } : {}
1826
+ });
1643
1827
  return rows.map(fromCheckpointEntity);
1644
1828
  }
1645
1829
  async putSignalWaiter(waiter) {
@@ -1674,7 +1858,7 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1674
1858
  $like: `${prefix}%`
1675
1859
  }
1676
1860
  });
1677
- return rows.map((e) => ({
1861
+ return rows.filter((e) => e.token.startsWith(prefix)).map((e) => ({
1678
1862
  token: e.token,
1679
1863
  runId: e.runId,
1680
1864
  seq: e.seq,
@@ -1807,6 +1991,29 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1807
1991
  return affected > 0;
1808
1992
  }
1809
1993
  };
1994
+ function runUpdateData(patch) {
1995
+ const data = {};
1996
+ if ("workflow" in patch) data.workflow = patch.workflow;
1997
+ if ("workflowVersion" in patch) data.workflowVersion = patch.workflowVersion;
1998
+ if ("status" in patch) data.status = patch.status;
1999
+ if ("input" in patch) data.input = patch.input ?? null;
2000
+ if ("output" in patch) data.output = patch.output ?? null;
2001
+ if ("error" in patch) data.error = patch.error ?? null;
2002
+ if ("wakeAt" in patch) data.wakeAt = patch.wakeAt == null ? null : new Date(patch.wakeAt);
2003
+ if ("lockedBy" in patch) data.lockedBy = patch.lockedBy ?? null;
2004
+ if ("lockedUntil" in patch) data.lockedUntil = patch.lockedUntil == null ? null : new Date(patch.lockedUntil);
2005
+ if ("awaitingDecisionTaskId" in patch) data.awaitingDecisionTaskId = patch.awaitingDecisionTaskId ?? null;
2006
+ if ("recoveryAttempts" in patch) data.recoveryAttempts = patch.recoveryAttempts ?? null;
2007
+ if ("tags" in patch) data.tags = patch.tags ?? null;
2008
+ if ("searchAttributes" in patch) data.searchAttributes = patch.searchAttributes ?? null;
2009
+ if ("priority" in patch) data.priority = patch.priority ?? null;
2010
+ if ("namespace" in patch) data.namespace = patch.namespace ?? "default";
2011
+ if ("origin" in patch) data.origin = patch.origin ?? null;
2012
+ if (patch.createdAt != null) data.createdAt = patch.createdAt;
2013
+ if (patch.updatedAt != null) data.updatedAt = patch.updatedAt;
2014
+ return data;
2015
+ }
2016
+ __name(runUpdateData, "runUpdateData");
1810
2017
  function toRunEntity(run) {
1811
2018
  const e = new WorkflowRunEntity();
1812
2019
  e.id = run.id;
@@ -1856,6 +2063,21 @@ function fromRunEntity(e) {
1856
2063
  };
1857
2064
  }
1858
2065
  __name(fromRunEntity, "fromRunEntity");
2066
+ function aggregateDateReader(em) {
2067
+ const platform = em.getPlatform();
2068
+ const tz = platform.getTimezone();
2069
+ return (value) => {
2070
+ if (value instanceof Date) return value;
2071
+ if (typeof value === "bigint") return platform.parseDate(Number(value));
2072
+ if (typeof value === "number" || !tz || tz === "local") {
2073
+ return platform.parseDate(value);
2074
+ }
2075
+ const text = String(value);
2076
+ const zoned = text.includes("+") || text.lastIndexOf("-") > 10 || text.endsWith("Z");
2077
+ return platform.parseDate(zoned ? text : `${text}${tz}`);
2078
+ };
2079
+ }
2080
+ __name(aggregateDateReader, "aggregateDateReader");
1859
2081
  function toCheckpointEntity(cp) {
1860
2082
  const e = new StepCheckpointEntity();
1861
2083
  e.runId = cp.runId;