@dudousxd/nestjs-durable-store-mikro-orm 0.26.0 → 0.27.1

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 [
@@ -845,13 +934,50 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
845
934
  async ensureSchema() {
846
935
  await ensureMikroOrmDurableSchema(this.orm);
847
936
  }
937
+ async tryAdmitSingleton(runId, tag, workflow, limit, retryWakeAt) {
938
+ return this.fork().transactional(async (em) => {
939
+ const where = {
940
+ workflow,
941
+ status: {
942
+ $in: import_nestjs_durable_core.SINGLETON_ACTIVE_STATUSES
943
+ },
944
+ ...this.scopeNamespace !== void 0 ? {
945
+ namespace: this.scopeNamespace
946
+ } : {}
947
+ };
948
+ await em.nativeUpdate(WorkflowRunEntity, where, {
949
+ workflow
950
+ });
951
+ const rows = await em.find(WorkflowRunEntity, where);
952
+ const runs = rows.map(fromRunEntity);
953
+ const tags = (0, import_nestjs_durable_core.singletonAdmissionTags)(runs, runId, tag, workflow, limit);
954
+ if (!tags) {
955
+ const patch = (0, import_nestjs_durable_core.singletonAdmissionDenialPatch)(runs, runId, tag, workflow, retryWakeAt);
956
+ if (patch) await em.nativeUpdate(WorkflowRunEntity, {
957
+ id: runId
958
+ }, {
959
+ status: "suspended",
960
+ wakeAt: new Date(patch.wakeAt),
961
+ updatedAt: patch.updatedAt
962
+ });
963
+ return false;
964
+ }
965
+ await em.nativeUpdate(WorkflowRunEntity, {
966
+ id: runId
967
+ }, {
968
+ tags
969
+ });
970
+ return true;
971
+ });
972
+ }
848
973
  async createRun(run) {
849
974
  const em = this.fork();
850
975
  em.create(WorkflowRunEntity, toRunEntity(run));
851
976
  await em.flush();
852
977
  await this.reindexAttributes(run.id, run.searchAttributes);
853
978
  }
854
- async updateRun(runId, patch) {
979
+ async updateRun(runId, inputPatch) {
980
+ 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;
855
981
  const em = this.fork();
856
982
  const entity = await em.findOneOrFail(WorkflowRunEntity, {
857
983
  id: runId
@@ -895,71 +1021,69 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
895
1021
  id: runId
896
1022
  });
897
1023
  }
1024
+ async deleteRuns(runIds) {
1025
+ if (runIds.length === 0) return;
1026
+ const runId = {
1027
+ $in: runIds
1028
+ };
1029
+ await this.fork().transactional(async (tem) => {
1030
+ await tem.nativeDelete(StepCheckpointEntity, {
1031
+ runId
1032
+ });
1033
+ await tem.nativeDelete(SignalWaiterEntity, {
1034
+ runId
1035
+ });
1036
+ await tem.nativeDelete(RunAttributeEntity, {
1037
+ runId
1038
+ });
1039
+ await tem.nativeDelete(WorkflowRunEntity, {
1040
+ id: runId
1041
+ });
1042
+ });
1043
+ }
898
1044
  async pruneTerminalRuns(policy, nowMs, limit) {
899
- if (policy.statuses.length === 0 || limit <= 0) return 0;
1045
+ const statuses = policy.statuses.filter((s) => import_nestjs_durable_core.TERMINAL_RUN_STATUSES.includes(s));
1046
+ if (statuses.length === 0 || limit <= 0) return 0;
1047
+ if (policy.maxAge == null && policy.maxCount == null) return 0;
900
1048
  const em = this.fork();
901
- const status = {
902
- $in: policy.statuses
1049
+ const meta = em.getMetadata().get(WorkflowRunEntity);
1050
+ const idCol = meta.properties.id?.fieldNames?.[0] ?? "id";
1051
+ const query = {
1052
+ ...policy.scope ?? {},
1053
+ statuses
903
1054
  };
1055
+ const scoped = /* @__PURE__ */ __name((extra) => this.runQueryBuilder(em, query, {
1056
+ ...this.runWhere(query),
1057
+ ...extra ?? {}
1058
+ }).select([
1059
+ `r.${idCol}`
1060
+ ]), "scoped");
904
1061
  const ids = /* @__PURE__ */ new Set();
905
1062
  if (policy.maxAge != null) {
906
1063
  const cutoff = new Date(nowMs - (0, import_nestjs_durable_core.parseDuration)(policy.maxAge));
907
- const rows = await em.find(WorkflowRunEntity, {
908
- status,
1064
+ const rows = await scoped({
909
1065
  updatedAt: {
910
1066
  $lt: cutoff
911
1067
  }
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);
1068
+ }).orderBy({
1069
+ "r.updatedAt": "asc"
1070
+ }).limit(limit).execute();
1071
+ for (const r of rows) ids.add(String(r[idCol] ?? r.id));
922
1072
  }
923
1073
  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
- });
1074
+ const rows = await scoped().orderBy({
1075
+ "r.updatedAt": "desc",
1076
+ "r.id": "desc"
1077
+ }).limit(limit).offset(policy.maxCount).execute();
937
1078
  for (const r of rows) {
938
- ids.add(r.id);
939
1079
  if (ids.size >= limit) break;
1080
+ ids.add(String(r[idCol] ?? r.id));
940
1081
  }
941
1082
  }
942
- if (ids.size === 0) return 0;
943
1083
  const idList = [
944
1084
  ...ids
945
1085
  ].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
- });
1086
+ await this.deleteRuns(idList);
963
1087
  return idList.length;
964
1088
  }
965
1089
  async getCheckpoint(runId, seq) {
@@ -1171,6 +1295,11 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1171
1295
  const colExpr = this.jsonAsText(em, `${quote("r")}.${quote(this.tagsColumn(em))}`);
1172
1296
  return `${colExpr} LIKE '${pattern}'`;
1173
1297
  }
1298
+ async countRuns(query) {
1299
+ const em = this.fork();
1300
+ const rows = await this.runQueryBuilder(em, query, this.runWhere(query)).select([]).addSelect((0, import_core3.raw)("count(*) as count")).execute();
1301
+ return Number(rows[0]?.count ?? 0);
1302
+ }
1174
1303
  /** `GROUP BY status, origin` over the same predicates {@link listRuns} pages — one aggregate, so a
1175
1304
  * console can show whole-set counts next to a bounded page instead of downloading every run to
1176
1305
  * count them in the browser. */
@@ -1362,6 +1491,76 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1362
1491
  const ch = isMysql ? "`" : '"';
1363
1492
  return (id) => `${ch}${id}${ch}`;
1364
1493
  }
1494
+ // ---- persisted schedules ----------------------------------------------------------------------
1495
+ async saveSchedule(record) {
1496
+ const em = this.fork();
1497
+ await em.upsert(ScheduleEntity, toScheduleEntity(record));
1498
+ }
1499
+ async getSchedule(id) {
1500
+ const entity = await this.fork().findOne(ScheduleEntity, {
1501
+ id
1502
+ });
1503
+ return entity ? fromScheduleEntity(entity) : null;
1504
+ }
1505
+ /** One conditional `nativeUpdate`: the compare-and-set on `next_fire_at` (an exact bigint) is a
1506
+ * single statement. */
1507
+ async updateSchedule(id, patch, expectedNextFireAt) {
1508
+ const data = {};
1509
+ if (patch.namespace !== void 0) data.namespace = patch.namespace;
1510
+ if (patch.workflow !== void 0) data.workflow = patch.workflow;
1511
+ if (patch.paused !== void 0) data.paused = patch.paused;
1512
+ if ("nextFireAt" in patch) data.nextFireAt = patch.nextFireAt ?? null;
1513
+ if ("tags" in patch) data.tags = patch.tags ?? null;
1514
+ if (patch.spec !== void 0) data.spec = patch.spec;
1515
+ if (patch.state !== void 0) data.state = patch.state;
1516
+ if (patch.updatedAt !== void 0) data.updatedAt = patch.updatedAt;
1517
+ if (!Object.keys(data).length) return await this.getSchedule(id) !== null;
1518
+ const where = {
1519
+ id
1520
+ };
1521
+ if (expectedNextFireAt !== void 0) where.nextFireAt = expectedNextFireAt;
1522
+ const affected = await this.fork().nativeUpdate(ScheduleEntity, where, data);
1523
+ return affected > 0;
1524
+ }
1525
+ async deleteSchedule(id) {
1526
+ return await this.fork().nativeDelete(ScheduleEntity, {
1527
+ id
1528
+ }) > 0;
1529
+ }
1530
+ async listSchedules(query) {
1531
+ const em = this.fork();
1532
+ const where = {};
1533
+ if (query.namespace !== void 0) where.namespace = query.namespace;
1534
+ if (query.workflow !== void 0) where.workflow = query.workflow;
1535
+ if (query.paused !== void 0) where.paused = query.paused;
1536
+ if (query.dueBy !== void 0) {
1537
+ where.$and = [
1538
+ {
1539
+ paused: false
1540
+ },
1541
+ {
1542
+ nextFireAt: {
1543
+ $lte: query.dueBy
1544
+ }
1545
+ }
1546
+ ];
1547
+ }
1548
+ const quote = this.idQuote(em);
1549
+ const meta = em.getMetadata().get(ScheduleEntity);
1550
+ const tagsCol = meta.properties.tags?.fieldNames?.[0] ?? "tags";
1551
+ const qb = em.createQueryBuilder(ScheduleEntity, "r").where(where);
1552
+ if (query.tag !== void 0) {
1553
+ const pattern = `%"${query.tag.replace(/'/g, "''")}"%`;
1554
+ qb.andWhere(`${this.jsonAsText(em, `${quote("r")}.${quote(tagsCol)}`)} LIKE '${pattern}'`);
1555
+ }
1556
+ qb.orderBy({
1557
+ "r.nextFireAt": "asc nulls last",
1558
+ "r.id": "asc"
1559
+ });
1560
+ if (query.limit != null) qb.limit(query.limit);
1561
+ if (query.offset != null) qb.offset(query.offset);
1562
+ return (await qb.getResultList()).map(fromScheduleEntity);
1563
+ }
1365
1564
  async listCheckpoints(runId) {
1366
1565
  const em = this.fork();
1367
1566
  const rows = await em.find(StepCheckpointEntity, {
@@ -1600,6 +1799,38 @@ function fromCheckpointEntity(e) {
1600
1799
  };
1601
1800
  }
1602
1801
  __name(fromCheckpointEntity, "fromCheckpointEntity");
1802
+ function toScheduleEntity(r) {
1803
+ const e = new ScheduleEntity();
1804
+ e.id = r.id;
1805
+ e.namespace = r.namespace;
1806
+ e.workflow = r.workflow;
1807
+ e.paused = r.paused;
1808
+ e.nextFireAt = r.nextFireAt;
1809
+ e.tags = r.tags ?? null;
1810
+ e.spec = r.spec;
1811
+ e.state = r.state;
1812
+ e.createdAt = r.createdAt;
1813
+ e.updatedAt = r.updatedAt;
1814
+ return e;
1815
+ }
1816
+ __name(toScheduleEntity, "toScheduleEntity");
1817
+ function fromScheduleEntity(e) {
1818
+ return {
1819
+ id: e.id,
1820
+ namespace: e.namespace,
1821
+ workflow: e.workflow,
1822
+ paused: Boolean(e.paused),
1823
+ nextFireAt: e.nextFireAt == null ? null : Number(e.nextFireAt),
1824
+ ...e.tags?.length ? {
1825
+ tags: e.tags
1826
+ } : {},
1827
+ spec: e.spec ?? {},
1828
+ state: e.state ?? {},
1829
+ createdAt: e.createdAt,
1830
+ updatedAt: e.updatedAt
1831
+ };
1832
+ }
1833
+ __name(fromScheduleEntity, "fromScheduleEntity");
1603
1834
  // Annotate the CommonJS export names for ESM import in node:
1604
1835
  0 && (module.exports = {
1605
1836
  BufferedEventEntity,
@@ -1610,6 +1841,8 @@ __name(fromCheckpointEntity, "fromCheckpointEntity");
1610
1841
  MikroOrmStateStore,
1611
1842
  RunAttributeEntity,
1612
1843
  RunAttributeRepository,
1844
+ ScheduleEntity,
1845
+ ScheduleRepository,
1613
1846
  SignalWaiterEntity,
1614
1847
  SignalWaiterRepository,
1615
1848
  StepCheckpointEntity,