@dudousxd/nestjs-durable-store-mikro-orm 0.26.0 → 0.27.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
@@ -29,6 +29,8 @@ __export(index_exports, {
29
29
  MikroOrmStateStore: () => MikroOrmStateStore,
30
30
  RunAttributeEntity: () => RunAttributeEntity,
31
31
  RunAttributeRepository: () => RunAttributeRepository,
32
+ ScheduleEntity: () => ScheduleEntity,
33
+ ScheduleRepository: () => ScheduleRepository,
32
34
  SignalWaiterEntity: () => SignalWaiterEntity,
33
35
  SignalWaiterRepository: () => SignalWaiterRepository,
34
36
  StepCheckpointEntity: () => StepCheckpointEntity,
@@ -76,6 +78,11 @@ var BufferedEventRepository = class extends import_core.EntityRepository {
76
78
  __name(this, "BufferedEventRepository");
77
79
  }
78
80
  };
81
+ var ScheduleRepository = class extends import_core.EntityRepository {
82
+ static {
83
+ __name(this, "ScheduleRepository");
84
+ }
85
+ };
79
86
 
80
87
  // src/entities.ts
81
88
  function snakeCase(property) {
@@ -175,6 +182,24 @@ var BufferedEventEntity = class {
175
182
  payload;
176
183
  publishedAt;
177
184
  };
185
+ var ScheduleEntity = class {
186
+ static {
187
+ __name(this, "ScheduleEntity");
188
+ }
189
+ [import_core2.EntityRepositoryType];
190
+ id;
191
+ namespace;
192
+ workflow;
193
+ paused;
194
+ /** Epoch ms as a `bigint` column — exact, so `updateSchedule`'s compare-and-set can't be defeated
195
+ * by a DATETIME's truncated milliseconds. Drivers may hand it back as a string. */
196
+ nextFireAt;
197
+ tags;
198
+ spec;
199
+ state;
200
+ createdAt;
201
+ updatedAt;
202
+ };
178
203
  function namespaceFilterCond(args) {
179
204
  if (args?.namespace === void 0) return {};
180
205
  return {
@@ -574,13 +599,76 @@ function durableEntities(options = {}) {
574
599
  }
575
600
  }
576
601
  });
602
+ const schedules = new import_core2.EntitySchema({
603
+ class: ScheduleEntity,
604
+ tableName: "durable_schedules",
605
+ repository: /* @__PURE__ */ __name(() => ScheduleRepository, "repository"),
606
+ // The poll's "due by T" scan: `paused = false AND next_fire_at <= T ORDER BY next_fire_at`.
607
+ indexes: [
608
+ {
609
+ name: "durable_schedules_due_idx",
610
+ properties: [
611
+ "paused",
612
+ "nextFireAt"
613
+ ]
614
+ }
615
+ ],
616
+ properties: {
617
+ id: {
618
+ type: "string",
619
+ primary: true,
620
+ fieldName: col("id")
621
+ },
622
+ namespace: {
623
+ type: "string",
624
+ default: "default",
625
+ fieldName: col("namespace")
626
+ },
627
+ workflow: {
628
+ type: "string",
629
+ fieldName: col("workflow")
630
+ },
631
+ paused: {
632
+ type: "boolean",
633
+ default: false,
634
+ fieldName: col("paused")
635
+ },
636
+ nextFireAt: {
637
+ type: "bigint",
638
+ nullable: true,
639
+ fieldName: col("nextFireAt")
640
+ },
641
+ tags: {
642
+ type: "json",
643
+ nullable: true,
644
+ fieldName: col("tags")
645
+ },
646
+ spec: {
647
+ type: "json",
648
+ fieldName: col("spec")
649
+ },
650
+ state: {
651
+ type: "json",
652
+ fieldName: col("state")
653
+ },
654
+ createdAt: {
655
+ type: "Date",
656
+ fieldName: col("createdAt")
657
+ },
658
+ updatedAt: {
659
+ type: "Date",
660
+ fieldName: col("updatedAt")
661
+ }
662
+ }
663
+ });
577
664
  return [
578
665
  workflowRuns,
579
666
  stepCheckpoints,
580
667
  runAttributes,
581
668
  signalWaiters,
582
669
  bufferedSignals,
583
- bufferedEvents
670
+ bufferedEvents,
671
+ schedules
584
672
  ];
585
673
  }
586
674
  __name(durableEntities, "durableEntities");
@@ -598,7 +686,8 @@ var DURABLE_TABLE_NAMES = /* @__PURE__ */ new Set([
598
686
  "durable_run_attributes",
599
687
  "durable_signal_waiters",
600
688
  "durable_buffered_signals",
601
- "durable_buffered_events"
689
+ "durable_buffered_events",
690
+ "durable_schedules"
602
691
  ]);
603
692
  function durableManagedTables() {
604
693
  return [
@@ -895,71 +984,69 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
895
984
  id: runId
896
985
  });
897
986
  }
987
+ async deleteRuns(runIds) {
988
+ if (runIds.length === 0) return;
989
+ const runId = {
990
+ $in: runIds
991
+ };
992
+ await this.fork().transactional(async (tem) => {
993
+ await tem.nativeDelete(StepCheckpointEntity, {
994
+ runId
995
+ });
996
+ await tem.nativeDelete(SignalWaiterEntity, {
997
+ runId
998
+ });
999
+ await tem.nativeDelete(RunAttributeEntity, {
1000
+ runId
1001
+ });
1002
+ await tem.nativeDelete(WorkflowRunEntity, {
1003
+ id: runId
1004
+ });
1005
+ });
1006
+ }
898
1007
  async pruneTerminalRuns(policy, nowMs, limit) {
899
- if (policy.statuses.length === 0 || limit <= 0) return 0;
1008
+ const statuses = policy.statuses.filter((s) => import_nestjs_durable_core.TERMINAL_RUN_STATUSES.includes(s));
1009
+ if (statuses.length === 0 || limit <= 0) return 0;
1010
+ if (policy.maxAge == null && policy.maxCount == null) return 0;
900
1011
  const em = this.fork();
901
- const status = {
902
- $in: policy.statuses
1012
+ const meta = em.getMetadata().get(WorkflowRunEntity);
1013
+ const idCol = meta.properties.id?.fieldNames?.[0] ?? "id";
1014
+ const query = {
1015
+ ...policy.scope ?? {},
1016
+ statuses
903
1017
  };
1018
+ const scoped = /* @__PURE__ */ __name((extra) => this.runQueryBuilder(em, query, {
1019
+ ...this.runWhere(query),
1020
+ ...extra ?? {}
1021
+ }).select([
1022
+ `r.${idCol}`
1023
+ ]), "scoped");
904
1024
  const ids = /* @__PURE__ */ new Set();
905
1025
  if (policy.maxAge != null) {
906
1026
  const cutoff = new Date(nowMs - (0, import_nestjs_durable_core.parseDuration)(policy.maxAge));
907
- const rows = await em.find(WorkflowRunEntity, {
908
- status,
1027
+ const rows = await scoped({
909
1028
  updatedAt: {
910
1029
  $lt: cutoff
911
1030
  }
912
- }, {
913
- fields: [
914
- "id"
915
- ],
916
- orderBy: {
917
- updatedAt: "asc"
918
- },
919
- limit
920
- });
921
- for (const r of rows) ids.add(r.id);
1031
+ }).orderBy({
1032
+ "r.updatedAt": "asc"
1033
+ }).limit(limit).execute();
1034
+ for (const r of rows) ids.add(String(r[idCol] ?? r.id));
922
1035
  }
923
1036
  if (policy.maxCount != null && ids.size < limit) {
924
- const rows = await em.find(WorkflowRunEntity, {
925
- status
926
- }, {
927
- fields: [
928
- "id"
929
- ],
930
- orderBy: {
931
- updatedAt: "desc",
932
- id: "desc"
933
- },
934
- limit,
935
- offset: policy.maxCount
936
- });
1037
+ const rows = await scoped().orderBy({
1038
+ "r.updatedAt": "desc",
1039
+ "r.id": "desc"
1040
+ }).limit(limit).offset(policy.maxCount).execute();
937
1041
  for (const r of rows) {
938
- ids.add(r.id);
939
1042
  if (ids.size >= limit) break;
1043
+ ids.add(String(r[idCol] ?? r.id));
940
1044
  }
941
1045
  }
942
- if (ids.size === 0) return 0;
943
1046
  const idList = [
944
1047
  ...ids
945
1048
  ].slice(0, limit);
946
- const runId = {
947
- $in: idList
948
- };
949
- await em.transactional(async (tem) => {
950
- await tem.nativeDelete(StepCheckpointEntity, {
951
- runId
952
- });
953
- await tem.nativeDelete(SignalWaiterEntity, {
954
- runId
955
- });
956
- await tem.nativeDelete(RunAttributeEntity, {
957
- runId
958
- });
959
- await tem.nativeDelete(WorkflowRunEntity, {
960
- id: runId
961
- });
962
- });
1049
+ await this.deleteRuns(idList);
963
1050
  return idList.length;
964
1051
  }
965
1052
  async getCheckpoint(runId, seq) {
@@ -1171,6 +1258,11 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1171
1258
  const colExpr = this.jsonAsText(em, `${quote("r")}.${quote(this.tagsColumn(em))}`);
1172
1259
  return `${colExpr} LIKE '${pattern}'`;
1173
1260
  }
1261
+ async countRuns(query) {
1262
+ const em = this.fork();
1263
+ const rows = await this.runQueryBuilder(em, query, this.runWhere(query)).select([]).addSelect((0, import_core3.raw)("count(*) as count")).execute();
1264
+ return Number(rows[0]?.count ?? 0);
1265
+ }
1174
1266
  /** `GROUP BY status, origin` over the same predicates {@link listRuns} pages — one aggregate, so a
1175
1267
  * console can show whole-set counts next to a bounded page instead of downloading every run to
1176
1268
  * count them in the browser. */
@@ -1362,6 +1454,76 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1362
1454
  const ch = isMysql ? "`" : '"';
1363
1455
  return (id) => `${ch}${id}${ch}`;
1364
1456
  }
1457
+ // ---- persisted schedules ----------------------------------------------------------------------
1458
+ async saveSchedule(record) {
1459
+ const em = this.fork();
1460
+ await em.upsert(ScheduleEntity, toScheduleEntity(record));
1461
+ }
1462
+ async getSchedule(id) {
1463
+ const entity = await this.fork().findOne(ScheduleEntity, {
1464
+ id
1465
+ });
1466
+ return entity ? fromScheduleEntity(entity) : null;
1467
+ }
1468
+ /** One conditional `nativeUpdate`: the compare-and-set on `next_fire_at` (an exact bigint) is a
1469
+ * single statement. */
1470
+ async updateSchedule(id, patch, expectedNextFireAt) {
1471
+ const data = {};
1472
+ if (patch.namespace !== void 0) data.namespace = patch.namespace;
1473
+ if (patch.workflow !== void 0) data.workflow = patch.workflow;
1474
+ if (patch.paused !== void 0) data.paused = patch.paused;
1475
+ if ("nextFireAt" in patch) data.nextFireAt = patch.nextFireAt ?? null;
1476
+ if ("tags" in patch) data.tags = patch.tags ?? null;
1477
+ if (patch.spec !== void 0) data.spec = patch.spec;
1478
+ if (patch.state !== void 0) data.state = patch.state;
1479
+ if (patch.updatedAt !== void 0) data.updatedAt = patch.updatedAt;
1480
+ if (!Object.keys(data).length) return await this.getSchedule(id) !== null;
1481
+ const where = {
1482
+ id
1483
+ };
1484
+ if (expectedNextFireAt !== void 0) where.nextFireAt = expectedNextFireAt;
1485
+ const affected = await this.fork().nativeUpdate(ScheduleEntity, where, data);
1486
+ return affected > 0;
1487
+ }
1488
+ async deleteSchedule(id) {
1489
+ return await this.fork().nativeDelete(ScheduleEntity, {
1490
+ id
1491
+ }) > 0;
1492
+ }
1493
+ async listSchedules(query) {
1494
+ const em = this.fork();
1495
+ const where = {};
1496
+ if (query.namespace !== void 0) where.namespace = query.namespace;
1497
+ if (query.workflow !== void 0) where.workflow = query.workflow;
1498
+ if (query.paused !== void 0) where.paused = query.paused;
1499
+ if (query.dueBy !== void 0) {
1500
+ where.$and = [
1501
+ {
1502
+ paused: false
1503
+ },
1504
+ {
1505
+ nextFireAt: {
1506
+ $lte: query.dueBy
1507
+ }
1508
+ }
1509
+ ];
1510
+ }
1511
+ const quote = this.idQuote(em);
1512
+ const meta = em.getMetadata().get(ScheduleEntity);
1513
+ const tagsCol = meta.properties.tags?.fieldNames?.[0] ?? "tags";
1514
+ const qb = em.createQueryBuilder(ScheduleEntity, "r").where(where);
1515
+ if (query.tag !== void 0) {
1516
+ const pattern = `%"${query.tag.replace(/'/g, "''")}"%`;
1517
+ qb.andWhere(`${this.jsonAsText(em, `${quote("r")}.${quote(tagsCol)}`)} LIKE '${pattern}'`);
1518
+ }
1519
+ qb.orderBy({
1520
+ "r.nextFireAt": "asc nulls last",
1521
+ "r.id": "asc"
1522
+ });
1523
+ if (query.limit != null) qb.limit(query.limit);
1524
+ if (query.offset != null) qb.offset(query.offset);
1525
+ return (await qb.getResultList()).map(fromScheduleEntity);
1526
+ }
1365
1527
  async listCheckpoints(runId) {
1366
1528
  const em = this.fork();
1367
1529
  const rows = await em.find(StepCheckpointEntity, {
@@ -1600,6 +1762,38 @@ function fromCheckpointEntity(e) {
1600
1762
  };
1601
1763
  }
1602
1764
  __name(fromCheckpointEntity, "fromCheckpointEntity");
1765
+ function toScheduleEntity(r) {
1766
+ const e = new ScheduleEntity();
1767
+ e.id = r.id;
1768
+ e.namespace = r.namespace;
1769
+ e.workflow = r.workflow;
1770
+ e.paused = r.paused;
1771
+ e.nextFireAt = r.nextFireAt;
1772
+ e.tags = r.tags ?? null;
1773
+ e.spec = r.spec;
1774
+ e.state = r.state;
1775
+ e.createdAt = r.createdAt;
1776
+ e.updatedAt = r.updatedAt;
1777
+ return e;
1778
+ }
1779
+ __name(toScheduleEntity, "toScheduleEntity");
1780
+ function fromScheduleEntity(e) {
1781
+ return {
1782
+ id: e.id,
1783
+ namespace: e.namespace,
1784
+ workflow: e.workflow,
1785
+ paused: Boolean(e.paused),
1786
+ nextFireAt: e.nextFireAt == null ? null : Number(e.nextFireAt),
1787
+ ...e.tags?.length ? {
1788
+ tags: e.tags
1789
+ } : {},
1790
+ spec: e.spec ?? {},
1791
+ state: e.state ?? {},
1792
+ createdAt: e.createdAt,
1793
+ updatedAt: e.updatedAt
1794
+ };
1795
+ }
1796
+ __name(fromScheduleEntity, "fromScheduleEntity");
1603
1797
  // Annotate the CommonJS export names for ESM import in node:
1604
1798
  0 && (module.exports = {
1605
1799
  BufferedEventEntity,
@@ -1610,6 +1804,8 @@ __name(fromCheckpointEntity, "fromCheckpointEntity");
1610
1804
  MikroOrmStateStore,
1611
1805
  RunAttributeEntity,
1612
1806
  RunAttributeRepository,
1807
+ ScheduleEntity,
1808
+ ScheduleRepository,
1613
1809
  SignalWaiterEntity,
1614
1810
  SignalWaiterRepository,
1615
1811
  StepCheckpointEntity,