@dudousxd/nestjs-durable-store-mikro-orm 0.25.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. */
@@ -1191,83 +1283,118 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1191
1283
  count: Number(r.count)
1192
1284
  })));
1193
1285
  }
1194
- /** Distinct values of one filter axis over the same predicates {@link listRuns} pages. A run-table
1195
- * column is a `GROUP BY` like {@link runFacets}, exact over the whole matching set; the tag and
1196
- * attribute axes live outside the row (json array / side table) and are counted from a bounded
1197
- * page of runs instead — see {@link RunValueFacetOptions.scan}. */
1286
+ /**
1287
+ * Distinct values of one filter axis over the same predicates {@link listRuns} pages, with counts —
1288
+ * what a console's value pickers list.
1289
+ *
1290
+ * All three axis families are answered by ONE grouped query against the filtered run set, so the
1291
+ * counts are exact over that whole set (not a sample), the ordering is stable, and a picker can
1292
+ * page and search server-side. That last part is what makes a picker usable on the axis that needs
1293
+ * it most: tag cardinality grows with the data, so the values an operator is looking for are
1294
+ * routinely outside any top slice, and a search that only filtered the fetched page could never
1295
+ * find them.
1296
+ *
1297
+ * The predicates come from {@link runQueryBuilder} as a subquery rather than being rewritten here,
1298
+ * so a picker and the list it filters can never disagree about which runs they are talking about.
1299
+ */
1198
1300
  async runValueFacets(axis, query, opts) {
1199
- if (!(0, import_nestjs_durable_core.axisIsRunColumn)(axis)) {
1200
- return this.scannedValueFacets(axis, query, opts);
1201
- }
1202
1301
  const em = this.fork();
1203
- const meta = em.getMetadata().get(WorkflowRunEntity);
1204
- const col = meta.properties[axis.field]?.fieldNames?.[0] ?? axis.field;
1205
- const rows = await this.runQueryBuilder(em, query, this.runWhere(query)).select([
1206
- `r.${col}`
1207
- ]).addSelect((0, import_core3.raw)("count(*) as count")).groupBy([
1208
- `r.${col}`
1209
- ]).execute();
1210
- return (0, import_nestjs_durable_core.mergeRunValueFacetRows)(rows.map((row) => ({
1211
- value: row[axis.field] ?? null,
1302
+ const quote = this.idQuote(em);
1303
+ const limit = Math.max(0, opts?.limit ?? import_nestjs_durable_core.RUN_VALUE_FACET_LIMIT);
1304
+ const offset = Math.max(0, opts?.offset ?? 0);
1305
+ if (limit === 0) return [];
1306
+ const runs = this.runSubquery(em, query, axis);
1307
+ const { source, valueExpr } = this.valueSource(em, axis, runs, quote);
1308
+ const conditions = [
1309
+ this.searchCondition(valueExpr, opts?.search)
1310
+ ].filter((part) => part !== void 0);
1311
+ const where = conditions.length ? ` where ${conditions.join(" and ")}` : "";
1312
+ const engineLast = axis.field === "tag" ? `case when ${valueExpr} like '${import_nestjs_durable_core.ENGINE_MINTED_TAG_PREFIXES[0]}%' then 1 else 0 end, ` : "";
1313
+ const sql = `select ${valueExpr} as value, count(*) as count from ${source}${where} group by ${valueExpr} order by ${engineLast}count(*) desc, ${valueExpr} asc limit ${limit} offset ${offset}`;
1314
+ const rows = await em.getConnection().execute(sql);
1315
+ return rows.map((row) => ({
1316
+ // Blank and absent are the same thing to an operator on the origin axis — see facetOrigin.
1317
+ value: axis.field === "origin" ? (0, import_nestjs_durable_core.facetOrigin)(row.value) : this.text(row.value),
1212
1318
  count: Number(row.count)
1213
- })), opts);
1319
+ }));
1320
+ }
1321
+ /** The filtered run set as a derived table, carrying only the columns the axis needs. One source
1322
+ * of truth with `listRuns`: the same builder, the same predicates. */
1323
+ runSubquery(em, query, axis) {
1324
+ const meta = em.getMetadata().get(WorkflowRunEntity);
1325
+ const idCol = meta.properties.id?.fieldNames?.[0] ?? "id";
1326
+ const columns = axis.field === "tag" ? [
1327
+ `r.${idCol}`,
1328
+ `r.${this.tagsColumn(em)}`
1329
+ ] : (0, import_nestjs_durable_core.axisIsRunColumn)(axis) ? [
1330
+ `r.${meta.properties[axis.field]?.fieldNames?.[0] ?? axis.field}`
1331
+ ] : [
1332
+ `r.${idCol}`
1333
+ ];
1334
+ return this.runQueryBuilder(em, query, this.runWhere(query)).select(columns).getFormattedQuery();
1335
+ }
1336
+ /** Where the values of an axis come from, and how to name one — a column of the filtered runs, an
1337
+ * element of their tags array, or a row of the attribute side table. */
1338
+ valueSource(em, axis, runs, quote) {
1339
+ const meta = em.getMetadata().get(WorkflowRunEntity);
1340
+ if ((0, import_nestjs_durable_core.axisIsRunColumn)(axis)) {
1341
+ const col = meta.properties[axis.field]?.fieldNames?.[0] ?? axis.field;
1342
+ return {
1343
+ source: `(${runs}) as ${quote("r")}`,
1344
+ valueExpr: `${quote("r")}.${quote(col)}`
1345
+ };
1346
+ }
1347
+ if (axis.field === "tag") {
1348
+ return {
1349
+ source: `(${runs}) as ${quote("r")} ${this.jsonArrayJoin(em, `${quote("r")}.${quote(this.tagsColumn(em))}`, quote)}`,
1350
+ valueExpr: `${quote("t")}.${quote("value")}`
1351
+ };
1352
+ }
1353
+ const cols = this.attributeColumns(em);
1354
+ const idCol = meta.properties.id?.fieldNames?.[0] ?? "id";
1355
+ const a = quote("a");
1356
+ const join = `${quote(cols.table)} as ${a} join (${runs}) as ${quote("r")} on ${a}.${quote(cols.runId)} = ${quote("r")}.${quote(idCol)}`;
1357
+ if (axis.field === "attributeKey") {
1358
+ return {
1359
+ source: join,
1360
+ valueExpr: `${a}.${quote(cols.key)}`
1361
+ };
1362
+ }
1363
+ const key = axis.key.replace(/'/g, "''");
1364
+ return {
1365
+ source: `${join} and ${a}.${quote(cols.key)} = '${key}'`,
1366
+ valueExpr: `coalesce(${a}.${quote(cols.strValue)}, cast(${a}.${quote(cols.numValue)} as char))`
1367
+ };
1214
1368
  }
1215
1369
  /**
1216
- * The axes whose values do not live in a column of the run row: `tag` (inside a JSON array) and the
1217
- * two search-attribute axes (a side table). Both read the newest `scan` matching runs.
1218
- *
1219
- * Neither reads a run's PAYLOAD. The obvious implementation — page `listRuns` and count in
1220
- * memory — pulls `input`, `output` and `error` for every run in the window, which on a control
1221
- * plane with real payloads is tens of megabytes fetched to produce a list of a hundred short
1222
- * strings, every time a picker opens. These project the one column each question needs instead:
1223
- * the tags column, or the run ids that then drive one grouped read of the side table.
1370
+ * The dialect's way of turning a JSON array column into one row per element — the only portable
1371
+ * thing about it being that all three have one. Without it a tag picker cannot be grouped in the
1372
+ * database at all, and falls back to deduping a bounded page of runs in memory: counts that
1373
+ * describe a sample, and an order that cannot be paged.
1224
1374
  */
1225
- async scannedValueFacets(axis, query, opts) {
1226
- const em = this.fork();
1227
- const scan = opts?.scan ?? import_nestjs_durable_core.RUN_VALUE_FACET_SCAN;
1228
- const where = this.runWhere(query);
1229
- const runMeta = em.getMetadata().get(WorkflowRunEntity);
1230
- const idCol = runMeta.properties.id?.fieldNames?.[0] ?? "id";
1231
- if (axis.field === "tag") {
1232
- const tagsCol = this.tagsColumn(em);
1233
- const rows = await this.runQueryBuilder(em, query, where).select([
1234
- `r.${tagsCol}`
1235
- ]).orderBy({
1236
- createdAt: "desc"
1237
- }).limit(scan).execute();
1238
- const values = [];
1239
- for (const row of rows) {
1240
- const raw2 = row.tags ?? row[tagsCol];
1241
- const tags = typeof raw2 === "string" ? JSON.parse(raw2) : raw2;
1242
- if (!Array.isArray(tags)) continue;
1243
- for (const tag of tags) values.push({
1244
- value: String(tag),
1245
- count: 1
1246
- });
1247
- }
1248
- return (0, import_nestjs_durable_core.mergeRunValueFacetRows)(values, opts);
1375
+ jsonArrayJoin(em, colRef, quote) {
1376
+ const platform = String(em.getPlatform().constructor.name).toLowerCase();
1377
+ const t = quote("t");
1378
+ if (platform.includes("postgre")) {
1379
+ return `cross join lateral jsonb_array_elements_text(${colRef}::jsonb) as ${t}(${quote("value")})`;
1249
1380
  }
1250
- const idRows = await this.runQueryBuilder(em, query, where).select([
1251
- `r.${idCol}`
1252
- ]).orderBy({
1253
- createdAt: "desc"
1254
- }).limit(scan).execute();
1255
- const runIds = idRows.map((row) => String(row.id ?? row[idCol]));
1256
- if (runIds.length === 0) return [];
1257
- const attributes = await em.find(RunAttributeEntity, axis.field === "attributeValue" ? {
1258
- runId: {
1259
- $in: runIds
1260
- },
1261
- key: axis.key
1262
- } : {
1263
- runId: {
1264
- $in: runIds
1265
- }
1266
- });
1267
- return (0, import_nestjs_durable_core.mergeRunValueFacetRows)(attributes.map((row) => ({
1268
- value: axis.field === "attributeKey" ? row.key : row.strValue ?? (row.numValue == null ? null : String(row.numValue)),
1269
- count: 1
1270
- })), opts);
1381
+ if (platform.includes("mysql") || platform.includes("mariadb")) {
1382
+ return `join json_table(cast(${colRef} as json), '$[*]' columns (${quote("value")} varchar(512) path '$')) as ${t}`;
1383
+ }
1384
+ return `join json_each(${colRef}) as ${t}`;
1385
+ }
1386
+ /** `lower(value) like '%needle%'` — the picker's search box, applied BEFORE the page is cut so a
1387
+ * match outside the top slice is still reachable. */
1388
+ searchCondition(valueExpr, search) {
1389
+ const needle = search?.trim().toLowerCase();
1390
+ if (!needle) return void 0;
1391
+ const escaped = needle.replace(/'/g, "''").replace(/[%_]/g, (c) => `\\${c}`);
1392
+ return `lower(${valueExpr}) like '%${escaped}%'`;
1393
+ }
1394
+ /** A grouped value as the string a picker shows. Drivers return a JSON element or a numeric
1395
+ * attribute as something other than a string; `null` stays null (the absent bucket). */
1396
+ text(value) {
1397
+ return value === null || value === void 0 ? null : String(value);
1271
1398
  }
1272
1399
  /** One attribute predicate as a raw EXISTS subquery on the side-table, correlated to the outer run
1273
1400
  * alias. `<>` (ne) also excludes runs where the attribute is absent (missing-key-never-matches):
@@ -1327,6 +1454,76 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1327
1454
  const ch = isMysql ? "`" : '"';
1328
1455
  return (id) => `${ch}${id}${ch}`;
1329
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
+ }
1330
1527
  async listCheckpoints(runId) {
1331
1528
  const em = this.fork();
1332
1529
  const rows = await em.find(StepCheckpointEntity, {
@@ -1565,6 +1762,38 @@ function fromCheckpointEntity(e) {
1565
1762
  };
1566
1763
  }
1567
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");
1568
1797
  // Annotate the CommonJS export names for ESM import in node:
1569
1798
  0 && (module.exports = {
1570
1799
  BufferedEventEntity,
@@ -1575,6 +1804,8 @@ __name(fromCheckpointEntity, "fromCheckpointEntity");
1575
1804
  MikroOrmStateStore,
1576
1805
  RunAttributeEntity,
1577
1806
  RunAttributeRepository,
1807
+ ScheduleEntity,
1808
+ ScheduleRepository,
1578
1809
  SignalWaiterEntity,
1579
1810
  SignalWaiterRepository,
1580
1811
  StepCheckpointEntity,