@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.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, SignalWaiter } from '@dudousxd/nestjs-durable-core';
3
+ import { StepKind, RunStatus, StateStore, WorkflowRun, RetentionPolicy, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, ScheduleRecord, ScheduleQuery, SignalWaiter } 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
@@ -29,6 +29,8 @@ declare class BufferedSignalRepository extends EntityRepository<BufferedSignalEn
29
29
  }
30
30
  declare class BufferedEventRepository extends EntityRepository<BufferedEventEntity> {
31
31
  }
32
+ declare class ScheduleRepository extends EntityRepository<ScheduleEntity> {
33
+ }
32
34
 
33
35
  /**
34
36
  * How property names map to physical column names. The durable tables are adapter-agnostic: a run
@@ -121,6 +123,22 @@ declare class BufferedEventEntity {
121
123
  payload?: unknown;
122
124
  publishedAt: Date;
123
125
  }
126
+ /** A persisted schedule (`engine.schedules`). `spec`/`state` are engine-owned JSON documents. */
127
+ declare class ScheduleEntity {
128
+ [EntityRepositoryType]?: ScheduleRepository;
129
+ id: string;
130
+ namespace: string;
131
+ workflow: string;
132
+ paused: boolean;
133
+ /** Epoch ms as a `bigint` column — exact, so `updateSchedule`'s compare-and-set can't be defeated
134
+ * by a DATETIME's truncated milliseconds. Drivers may hand it back as a string. */
135
+ nextFireAt?: number | string | bigint | null;
136
+ tags?: string[] | null;
137
+ spec: Record<string, unknown>;
138
+ state: Record<string, unknown>;
139
+ createdAt: Date;
140
+ updatedAt: Date;
141
+ }
124
142
  declare function durableEntities(options?: {
125
143
  naming?: DurableColumnNaming;
126
144
  }): EntitySchema[];
@@ -167,13 +185,15 @@ declare class MikroOrmStateStore implements StateStore {
167
185
  */
168
186
  private fork;
169
187
  ensureSchema(): Promise<void>;
188
+ tryAdmitSingleton(runId: string, tag: string, workflow: string, limit: number, retryWakeAt?: number): Promise<boolean>;
170
189
  createRun(run: WorkflowRun): Promise<void>;
171
- updateRun(runId: string, patch: Partial<WorkflowRun>): Promise<void>;
190
+ updateRun(runId: string, inputPatch: Partial<WorkflowRun>): Promise<void>;
172
191
  /** Rewrite a run's normalized attribute rows: delete the old set, insert the current one. Mirrors
173
192
  * the in-memory store's reindex so the side-table always reflects the run's live searchAttributes. */
174
193
  private reindexAttributes;
175
194
  getRun(runId: string): Promise<WorkflowRun | null>;
176
195
  deleteRun(runId: string): Promise<void>;
196
+ deleteRuns(runIds: string[]): Promise<void>;
177
197
  pruneTerminalRuns(policy: RetentionPolicy, nowMs: number, limit: number): Promise<number>;
178
198
  getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
179
199
  saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
@@ -199,6 +219,7 @@ declare class MikroOrmStateStore implements StateStore {
199
219
  /** `tags LIKE '%"etl"%'` — the quoted token, so `etl` doesn't match `etl-foo`. See the note in
200
220
  * {@link listRuns} for why this is raw SQL rather than a MikroORM condition. */
201
221
  private tagLikeSql;
222
+ countRuns(query: Omit<RunQuery, 'limit' | 'offset'>): Promise<number>;
202
223
  /** `GROUP BY status, origin` over the same predicates {@link listRuns} pages — one aggregate, so a
203
224
  * console can show whole-set counts next to a bounded page instead of downloading every run to
204
225
  * count them in the browser. */
@@ -257,6 +278,13 @@ declare class MikroOrmStateStore implements StateStore {
257
278
  private jsonAsText;
258
279
  /** Quote an identifier per the active SQL driver (MySQL/MariaDB backtick, others double-quote). */
259
280
  private idQuote;
281
+ saveSchedule(record: ScheduleRecord): Promise<void>;
282
+ getSchedule(id: string): Promise<ScheduleRecord | null>;
283
+ /** One conditional `nativeUpdate`: the compare-and-set on `next_fire_at` (an exact bigint) is a
284
+ * single statement. */
285
+ updateSchedule(id: string, patch: Partial<Omit<ScheduleRecord, 'id' | 'createdAt'>>, expectedNextFireAt?: number | null): Promise<boolean>;
286
+ deleteSchedule(id: string): Promise<boolean>;
287
+ listSchedules(query: ScheduleQuery): Promise<ScheduleRecord[]>;
260
288
  listCheckpoints(runId: string): Promise<StepCheckpoint[]>;
261
289
  getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
262
290
  listCheckpointsByNamePrefix(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
@@ -327,4 +355,4 @@ declare function durableManagedTables(): string[];
327
355
  */
328
356
  declare function ensureMikroOrmDurableSchema(orm: MikroORM): Promise<void>;
329
357
 
330
- export { BufferedEventEntity, BufferedEventRepository, BufferedSignalEntity, BufferedSignalRepository, type DurableColumnNaming, ENTITIES, MikroOrmStateStore, type MikroOrmStateStoreOptions, RunAttributeEntity, RunAttributeRepository, SignalWaiterEntity, SignalWaiterRepository, StepCheckpointEntity, StepCheckpointRepository, WorkflowRunEntity, WorkflowRunRepository, durableEntities, durableManagedTables, ensureMikroOrmDurableSchema };
358
+ export { BufferedEventEntity, BufferedEventRepository, BufferedSignalEntity, BufferedSignalRepository, type DurableColumnNaming, ENTITIES, MikroOrmStateStore, type MikroOrmStateStoreOptions, RunAttributeEntity, RunAttributeRepository, ScheduleEntity, ScheduleRepository, SignalWaiterEntity, SignalWaiterRepository, StepCheckpointEntity, StepCheckpointRepository, WorkflowRunEntity, WorkflowRunRepository, durableEntities, durableManagedTables, ensureMikroOrmDurableSchema };
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, SignalWaiter } from '@dudousxd/nestjs-durable-core';
3
+ import { StepKind, RunStatus, StateStore, WorkflowRun, RetentionPolicy, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, ScheduleRecord, ScheduleQuery, SignalWaiter } 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
@@ -29,6 +29,8 @@ declare class BufferedSignalRepository extends EntityRepository<BufferedSignalEn
29
29
  }
30
30
  declare class BufferedEventRepository extends EntityRepository<BufferedEventEntity> {
31
31
  }
32
+ declare class ScheduleRepository extends EntityRepository<ScheduleEntity> {
33
+ }
32
34
 
33
35
  /**
34
36
  * How property names map to physical column names. The durable tables are adapter-agnostic: a run
@@ -121,6 +123,22 @@ declare class BufferedEventEntity {
121
123
  payload?: unknown;
122
124
  publishedAt: Date;
123
125
  }
126
+ /** A persisted schedule (`engine.schedules`). `spec`/`state` are engine-owned JSON documents. */
127
+ declare class ScheduleEntity {
128
+ [EntityRepositoryType]?: ScheduleRepository;
129
+ id: string;
130
+ namespace: string;
131
+ workflow: string;
132
+ paused: boolean;
133
+ /** Epoch ms as a `bigint` column — exact, so `updateSchedule`'s compare-and-set can't be defeated
134
+ * by a DATETIME's truncated milliseconds. Drivers may hand it back as a string. */
135
+ nextFireAt?: number | string | bigint | null;
136
+ tags?: string[] | null;
137
+ spec: Record<string, unknown>;
138
+ state: Record<string, unknown>;
139
+ createdAt: Date;
140
+ updatedAt: Date;
141
+ }
124
142
  declare function durableEntities(options?: {
125
143
  naming?: DurableColumnNaming;
126
144
  }): EntitySchema[];
@@ -167,13 +185,15 @@ declare class MikroOrmStateStore implements StateStore {
167
185
  */
168
186
  private fork;
169
187
  ensureSchema(): Promise<void>;
188
+ tryAdmitSingleton(runId: string, tag: string, workflow: string, limit: number, retryWakeAt?: number): Promise<boolean>;
170
189
  createRun(run: WorkflowRun): Promise<void>;
171
- updateRun(runId: string, patch: Partial<WorkflowRun>): Promise<void>;
190
+ updateRun(runId: string, inputPatch: Partial<WorkflowRun>): Promise<void>;
172
191
  /** Rewrite a run's normalized attribute rows: delete the old set, insert the current one. Mirrors
173
192
  * the in-memory store's reindex so the side-table always reflects the run's live searchAttributes. */
174
193
  private reindexAttributes;
175
194
  getRun(runId: string): Promise<WorkflowRun | null>;
176
195
  deleteRun(runId: string): Promise<void>;
196
+ deleteRuns(runIds: string[]): Promise<void>;
177
197
  pruneTerminalRuns(policy: RetentionPolicy, nowMs: number, limit: number): Promise<number>;
178
198
  getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
179
199
  saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
@@ -199,6 +219,7 @@ declare class MikroOrmStateStore implements StateStore {
199
219
  /** `tags LIKE '%"etl"%'` — the quoted token, so `etl` doesn't match `etl-foo`. See the note in
200
220
  * {@link listRuns} for why this is raw SQL rather than a MikroORM condition. */
201
221
  private tagLikeSql;
222
+ countRuns(query: Omit<RunQuery, 'limit' | 'offset'>): Promise<number>;
202
223
  /** `GROUP BY status, origin` over the same predicates {@link listRuns} pages — one aggregate, so a
203
224
  * console can show whole-set counts next to a bounded page instead of downloading every run to
204
225
  * count them in the browser. */
@@ -257,6 +278,13 @@ declare class MikroOrmStateStore implements StateStore {
257
278
  private jsonAsText;
258
279
  /** Quote an identifier per the active SQL driver (MySQL/MariaDB backtick, others double-quote). */
259
280
  private idQuote;
281
+ saveSchedule(record: ScheduleRecord): Promise<void>;
282
+ getSchedule(id: string): Promise<ScheduleRecord | null>;
283
+ /** One conditional `nativeUpdate`: the compare-and-set on `next_fire_at` (an exact bigint) is a
284
+ * single statement. */
285
+ updateSchedule(id: string, patch: Partial<Omit<ScheduleRecord, 'id' | 'createdAt'>>, expectedNextFireAt?: number | null): Promise<boolean>;
286
+ deleteSchedule(id: string): Promise<boolean>;
287
+ listSchedules(query: ScheduleQuery): Promise<ScheduleRecord[]>;
260
288
  listCheckpoints(runId: string): Promise<StepCheckpoint[]>;
261
289
  getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
262
290
  listCheckpointsByNamePrefix(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
@@ -327,4 +355,4 @@ declare function durableManagedTables(): string[];
327
355
  */
328
356
  declare function ensureMikroOrmDurableSchema(orm: MikroORM): Promise<void>;
329
357
 
330
- export { BufferedEventEntity, BufferedEventRepository, BufferedSignalEntity, BufferedSignalRepository, type DurableColumnNaming, ENTITIES, MikroOrmStateStore, type MikroOrmStateStoreOptions, RunAttributeEntity, RunAttributeRepository, SignalWaiterEntity, SignalWaiterRepository, StepCheckpointEntity, StepCheckpointRepository, WorkflowRunEntity, WorkflowRunRepository, durableEntities, durableManagedTables, ensureMikroOrmDurableSchema };
358
+ export { BufferedEventEntity, BufferedEventRepository, BufferedSignalEntity, BufferedSignalRepository, type DurableColumnNaming, ENTITIES, MikroOrmStateStore, type MikroOrmStateStoreOptions, RunAttributeEntity, RunAttributeRepository, ScheduleEntity, ScheduleRepository, SignalWaiterEntity, SignalWaiterRepository, StepCheckpointEntity, StepCheckpointRepository, WorkflowRunEntity, WorkflowRunRepository, durableEntities, durableManagedTables, ensureMikroOrmDurableSchema };
package/dist/index.js CHANGED
@@ -36,6 +36,11 @@ var BufferedEventRepository = class extends EntityRepository {
36
36
  __name(this, "BufferedEventRepository");
37
37
  }
38
38
  };
39
+ var ScheduleRepository = class extends EntityRepository {
40
+ static {
41
+ __name(this, "ScheduleRepository");
42
+ }
43
+ };
39
44
 
40
45
  // src/entities.ts
41
46
  function snakeCase(property) {
@@ -135,6 +140,24 @@ var BufferedEventEntity = class {
135
140
  payload;
136
141
  publishedAt;
137
142
  };
143
+ var ScheduleEntity = class {
144
+ static {
145
+ __name(this, "ScheduleEntity");
146
+ }
147
+ [EntityRepositoryType];
148
+ id;
149
+ namespace;
150
+ workflow;
151
+ paused;
152
+ /** Epoch ms as a `bigint` column — exact, so `updateSchedule`'s compare-and-set can't be defeated
153
+ * by a DATETIME's truncated milliseconds. Drivers may hand it back as a string. */
154
+ nextFireAt;
155
+ tags;
156
+ spec;
157
+ state;
158
+ createdAt;
159
+ updatedAt;
160
+ };
138
161
  function namespaceFilterCond(args) {
139
162
  if (args?.namespace === void 0) return {};
140
163
  return {
@@ -534,20 +557,83 @@ function durableEntities(options = {}) {
534
557
  }
535
558
  }
536
559
  });
560
+ const schedules = new EntitySchema({
561
+ class: ScheduleEntity,
562
+ tableName: "durable_schedules",
563
+ repository: /* @__PURE__ */ __name(() => ScheduleRepository, "repository"),
564
+ // The poll's "due by T" scan: `paused = false AND next_fire_at <= T ORDER BY next_fire_at`.
565
+ indexes: [
566
+ {
567
+ name: "durable_schedules_due_idx",
568
+ properties: [
569
+ "paused",
570
+ "nextFireAt"
571
+ ]
572
+ }
573
+ ],
574
+ properties: {
575
+ id: {
576
+ type: "string",
577
+ primary: true,
578
+ fieldName: col("id")
579
+ },
580
+ namespace: {
581
+ type: "string",
582
+ default: "default",
583
+ fieldName: col("namespace")
584
+ },
585
+ workflow: {
586
+ type: "string",
587
+ fieldName: col("workflow")
588
+ },
589
+ paused: {
590
+ type: "boolean",
591
+ default: false,
592
+ fieldName: col("paused")
593
+ },
594
+ nextFireAt: {
595
+ type: "bigint",
596
+ nullable: true,
597
+ fieldName: col("nextFireAt")
598
+ },
599
+ tags: {
600
+ type: "json",
601
+ nullable: true,
602
+ fieldName: col("tags")
603
+ },
604
+ spec: {
605
+ type: "json",
606
+ fieldName: col("spec")
607
+ },
608
+ state: {
609
+ type: "json",
610
+ fieldName: col("state")
611
+ },
612
+ createdAt: {
613
+ type: "Date",
614
+ fieldName: col("createdAt")
615
+ },
616
+ updatedAt: {
617
+ type: "Date",
618
+ fieldName: col("updatedAt")
619
+ }
620
+ }
621
+ });
537
622
  return [
538
623
  workflowRuns,
539
624
  stepCheckpoints,
540
625
  runAttributes,
541
626
  signalWaiters,
542
627
  bufferedSignals,
543
- bufferedEvents
628
+ bufferedEvents,
629
+ schedules
544
630
  ];
545
631
  }
546
632
  __name(durableEntities, "durableEntities");
547
633
  var ENTITIES = durableEntities();
548
634
 
549
635
  // src/mikro-orm-state-store.ts
550
- import { ENGINE_MINTED_TAG_PREFIXES, RUN_VALUE_FACET_LIMIT, attributePredicateOperands, axisIsRunColumn, facetOrigin, mergeRunFacetRows, normalizeAttributeRows, parseDuration } from "@dudousxd/nestjs-durable-core";
636
+ import { ENGINE_MINTED_TAG_PREFIXES, RUN_VALUE_FACET_LIMIT, SINGLETON_ACTIVE_STATUSES, TERMINAL_RUN_STATUSES, attributePredicateOperands, axisIsRunColumn, clearTerminalSingletonAdmission, facetOrigin, mergeRunFacetRows, normalizeAttributeRows, parseDuration, singletonAdmissionDenialPatch, singletonAdmissionTags } from "@dudousxd/nestjs-durable-core";
551
637
  import { raw } from "@mikro-orm/core";
552
638
 
553
639
  // src/schema.ts
@@ -558,7 +644,8 @@ var DURABLE_TABLE_NAMES = /* @__PURE__ */ new Set([
558
644
  "durable_run_attributes",
559
645
  "durable_signal_waiters",
560
646
  "durable_buffered_signals",
561
- "durable_buffered_events"
647
+ "durable_buffered_events",
648
+ "durable_schedules"
562
649
  ]);
563
650
  function durableManagedTables() {
564
651
  return [
@@ -805,13 +892,50 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
805
892
  async ensureSchema() {
806
893
  await ensureMikroOrmDurableSchema(this.orm);
807
894
  }
895
+ async tryAdmitSingleton(runId, tag, workflow, limit, retryWakeAt) {
896
+ return this.fork().transactional(async (em) => {
897
+ const where = {
898
+ workflow,
899
+ status: {
900
+ $in: SINGLETON_ACTIVE_STATUSES
901
+ },
902
+ ...this.scopeNamespace !== void 0 ? {
903
+ namespace: this.scopeNamespace
904
+ } : {}
905
+ };
906
+ await em.nativeUpdate(WorkflowRunEntity, where, {
907
+ workflow
908
+ });
909
+ const rows = await em.find(WorkflowRunEntity, where);
910
+ const runs = rows.map(fromRunEntity);
911
+ const tags = singletonAdmissionTags(runs, runId, tag, workflow, limit);
912
+ if (!tags) {
913
+ const patch = singletonAdmissionDenialPatch(runs, runId, tag, workflow, retryWakeAt);
914
+ if (patch) await em.nativeUpdate(WorkflowRunEntity, {
915
+ id: runId
916
+ }, {
917
+ status: "suspended",
918
+ wakeAt: new Date(patch.wakeAt),
919
+ updatedAt: patch.updatedAt
920
+ });
921
+ return false;
922
+ }
923
+ await em.nativeUpdate(WorkflowRunEntity, {
924
+ id: runId
925
+ }, {
926
+ tags
927
+ });
928
+ return true;
929
+ });
930
+ }
808
931
  async createRun(run) {
809
932
  const em = this.fork();
810
933
  em.create(WorkflowRunEntity, toRunEntity(run));
811
934
  await em.flush();
812
935
  await this.reindexAttributes(run.id, run.searchAttributes);
813
936
  }
814
- async updateRun(runId, patch) {
937
+ async updateRun(runId, inputPatch) {
938
+ const patch = inputPatch.status && TERMINAL_RUN_STATUSES.includes(inputPatch.status) ? await clearTerminalSingletonAdmission(runId, inputPatch, (id) => this.withScope({}).getRun(id)) : inputPatch;
815
939
  const em = this.fork();
816
940
  const entity = await em.findOneOrFail(WorkflowRunEntity, {
817
941
  id: runId
@@ -855,71 +979,69 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
855
979
  id: runId
856
980
  });
857
981
  }
982
+ async deleteRuns(runIds) {
983
+ if (runIds.length === 0) return;
984
+ const runId = {
985
+ $in: runIds
986
+ };
987
+ await this.fork().transactional(async (tem) => {
988
+ await tem.nativeDelete(StepCheckpointEntity, {
989
+ runId
990
+ });
991
+ await tem.nativeDelete(SignalWaiterEntity, {
992
+ runId
993
+ });
994
+ await tem.nativeDelete(RunAttributeEntity, {
995
+ runId
996
+ });
997
+ await tem.nativeDelete(WorkflowRunEntity, {
998
+ id: runId
999
+ });
1000
+ });
1001
+ }
858
1002
  async pruneTerminalRuns(policy, nowMs, limit) {
859
- if (policy.statuses.length === 0 || limit <= 0) return 0;
1003
+ const statuses = policy.statuses.filter((s) => TERMINAL_RUN_STATUSES.includes(s));
1004
+ if (statuses.length === 0 || limit <= 0) return 0;
1005
+ if (policy.maxAge == null && policy.maxCount == null) return 0;
860
1006
  const em = this.fork();
861
- const status = {
862
- $in: policy.statuses
1007
+ const meta = em.getMetadata().get(WorkflowRunEntity);
1008
+ const idCol = meta.properties.id?.fieldNames?.[0] ?? "id";
1009
+ const query = {
1010
+ ...policy.scope ?? {},
1011
+ statuses
863
1012
  };
1013
+ const scoped = /* @__PURE__ */ __name((extra) => this.runQueryBuilder(em, query, {
1014
+ ...this.runWhere(query),
1015
+ ...extra ?? {}
1016
+ }).select([
1017
+ `r.${idCol}`
1018
+ ]), "scoped");
864
1019
  const ids = /* @__PURE__ */ new Set();
865
1020
  if (policy.maxAge != null) {
866
1021
  const cutoff = new Date(nowMs - parseDuration(policy.maxAge));
867
- const rows = await em.find(WorkflowRunEntity, {
868
- status,
1022
+ const rows = await scoped({
869
1023
  updatedAt: {
870
1024
  $lt: cutoff
871
1025
  }
872
- }, {
873
- fields: [
874
- "id"
875
- ],
876
- orderBy: {
877
- updatedAt: "asc"
878
- },
879
- limit
880
- });
881
- for (const r of rows) ids.add(r.id);
1026
+ }).orderBy({
1027
+ "r.updatedAt": "asc"
1028
+ }).limit(limit).execute();
1029
+ for (const r of rows) ids.add(String(r[idCol] ?? r.id));
882
1030
  }
883
1031
  if (policy.maxCount != null && ids.size < limit) {
884
- const rows = await em.find(WorkflowRunEntity, {
885
- status
886
- }, {
887
- fields: [
888
- "id"
889
- ],
890
- orderBy: {
891
- updatedAt: "desc",
892
- id: "desc"
893
- },
894
- limit,
895
- offset: policy.maxCount
896
- });
1032
+ const rows = await scoped().orderBy({
1033
+ "r.updatedAt": "desc",
1034
+ "r.id": "desc"
1035
+ }).limit(limit).offset(policy.maxCount).execute();
897
1036
  for (const r of rows) {
898
- ids.add(r.id);
899
1037
  if (ids.size >= limit) break;
1038
+ ids.add(String(r[idCol] ?? r.id));
900
1039
  }
901
1040
  }
902
- if (ids.size === 0) return 0;
903
1041
  const idList = [
904
1042
  ...ids
905
1043
  ].slice(0, limit);
906
- const runId = {
907
- $in: idList
908
- };
909
- await em.transactional(async (tem) => {
910
- await tem.nativeDelete(StepCheckpointEntity, {
911
- runId
912
- });
913
- await tem.nativeDelete(SignalWaiterEntity, {
914
- runId
915
- });
916
- await tem.nativeDelete(RunAttributeEntity, {
917
- runId
918
- });
919
- await tem.nativeDelete(WorkflowRunEntity, {
920
- id: runId
921
- });
922
- });
1044
+ await this.deleteRuns(idList);
923
1045
  return idList.length;
924
1046
  }
925
1047
  async getCheckpoint(runId, seq) {
@@ -1131,6 +1253,11 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1131
1253
  const colExpr = this.jsonAsText(em, `${quote("r")}.${quote(this.tagsColumn(em))}`);
1132
1254
  return `${colExpr} LIKE '${pattern}'`;
1133
1255
  }
1256
+ async countRuns(query) {
1257
+ const em = this.fork();
1258
+ const rows = await this.runQueryBuilder(em, query, this.runWhere(query)).select([]).addSelect(raw("count(*) as count")).execute();
1259
+ return Number(rows[0]?.count ?? 0);
1260
+ }
1134
1261
  /** `GROUP BY status, origin` over the same predicates {@link listRuns} pages — one aggregate, so a
1135
1262
  * console can show whole-set counts next to a bounded page instead of downloading every run to
1136
1263
  * count them in the browser. */
@@ -1322,6 +1449,76 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1322
1449
  const ch = isMysql ? "`" : '"';
1323
1450
  return (id) => `${ch}${id}${ch}`;
1324
1451
  }
1452
+ // ---- persisted schedules ----------------------------------------------------------------------
1453
+ async saveSchedule(record) {
1454
+ const em = this.fork();
1455
+ await em.upsert(ScheduleEntity, toScheduleEntity(record));
1456
+ }
1457
+ async getSchedule(id) {
1458
+ const entity = await this.fork().findOne(ScheduleEntity, {
1459
+ id
1460
+ });
1461
+ return entity ? fromScheduleEntity(entity) : null;
1462
+ }
1463
+ /** One conditional `nativeUpdate`: the compare-and-set on `next_fire_at` (an exact bigint) is a
1464
+ * single statement. */
1465
+ async updateSchedule(id, patch, expectedNextFireAt) {
1466
+ const data = {};
1467
+ if (patch.namespace !== void 0) data.namespace = patch.namespace;
1468
+ if (patch.workflow !== void 0) data.workflow = patch.workflow;
1469
+ if (patch.paused !== void 0) data.paused = patch.paused;
1470
+ if ("nextFireAt" in patch) data.nextFireAt = patch.nextFireAt ?? null;
1471
+ if ("tags" in patch) data.tags = patch.tags ?? null;
1472
+ if (patch.spec !== void 0) data.spec = patch.spec;
1473
+ if (patch.state !== void 0) data.state = patch.state;
1474
+ if (patch.updatedAt !== void 0) data.updatedAt = patch.updatedAt;
1475
+ if (!Object.keys(data).length) return await this.getSchedule(id) !== null;
1476
+ const where = {
1477
+ id
1478
+ };
1479
+ if (expectedNextFireAt !== void 0) where.nextFireAt = expectedNextFireAt;
1480
+ const affected = await this.fork().nativeUpdate(ScheduleEntity, where, data);
1481
+ return affected > 0;
1482
+ }
1483
+ async deleteSchedule(id) {
1484
+ return await this.fork().nativeDelete(ScheduleEntity, {
1485
+ id
1486
+ }) > 0;
1487
+ }
1488
+ async listSchedules(query) {
1489
+ const em = this.fork();
1490
+ const where = {};
1491
+ if (query.namespace !== void 0) where.namespace = query.namespace;
1492
+ if (query.workflow !== void 0) where.workflow = query.workflow;
1493
+ if (query.paused !== void 0) where.paused = query.paused;
1494
+ if (query.dueBy !== void 0) {
1495
+ where.$and = [
1496
+ {
1497
+ paused: false
1498
+ },
1499
+ {
1500
+ nextFireAt: {
1501
+ $lte: query.dueBy
1502
+ }
1503
+ }
1504
+ ];
1505
+ }
1506
+ const quote = this.idQuote(em);
1507
+ const meta = em.getMetadata().get(ScheduleEntity);
1508
+ const tagsCol = meta.properties.tags?.fieldNames?.[0] ?? "tags";
1509
+ const qb = em.createQueryBuilder(ScheduleEntity, "r").where(where);
1510
+ if (query.tag !== void 0) {
1511
+ const pattern = `%"${query.tag.replace(/'/g, "''")}"%`;
1512
+ qb.andWhere(`${this.jsonAsText(em, `${quote("r")}.${quote(tagsCol)}`)} LIKE '${pattern}'`);
1513
+ }
1514
+ qb.orderBy({
1515
+ "r.nextFireAt": "asc nulls last",
1516
+ "r.id": "asc"
1517
+ });
1518
+ if (query.limit != null) qb.limit(query.limit);
1519
+ if (query.offset != null) qb.offset(query.offset);
1520
+ return (await qb.getResultList()).map(fromScheduleEntity);
1521
+ }
1325
1522
  async listCheckpoints(runId) {
1326
1523
  const em = this.fork();
1327
1524
  const rows = await em.find(StepCheckpointEntity, {
@@ -1560,6 +1757,38 @@ function fromCheckpointEntity(e) {
1560
1757
  };
1561
1758
  }
1562
1759
  __name(fromCheckpointEntity, "fromCheckpointEntity");
1760
+ function toScheduleEntity(r) {
1761
+ const e = new ScheduleEntity();
1762
+ e.id = r.id;
1763
+ e.namespace = r.namespace;
1764
+ e.workflow = r.workflow;
1765
+ e.paused = r.paused;
1766
+ e.nextFireAt = r.nextFireAt;
1767
+ e.tags = r.tags ?? null;
1768
+ e.spec = r.spec;
1769
+ e.state = r.state;
1770
+ e.createdAt = r.createdAt;
1771
+ e.updatedAt = r.updatedAt;
1772
+ return e;
1773
+ }
1774
+ __name(toScheduleEntity, "toScheduleEntity");
1775
+ function fromScheduleEntity(e) {
1776
+ return {
1777
+ id: e.id,
1778
+ namespace: e.namespace,
1779
+ workflow: e.workflow,
1780
+ paused: Boolean(e.paused),
1781
+ nextFireAt: e.nextFireAt == null ? null : Number(e.nextFireAt),
1782
+ ...e.tags?.length ? {
1783
+ tags: e.tags
1784
+ } : {},
1785
+ spec: e.spec ?? {},
1786
+ state: e.state ?? {},
1787
+ createdAt: e.createdAt,
1788
+ updatedAt: e.updatedAt
1789
+ };
1790
+ }
1791
+ __name(fromScheduleEntity, "fromScheduleEntity");
1563
1792
  export {
1564
1793
  BufferedEventEntity,
1565
1794
  BufferedEventRepository,
@@ -1569,6 +1798,8 @@ export {
1569
1798
  MikroOrmStateStore,
1570
1799
  RunAttributeEntity,
1571
1800
  RunAttributeRepository,
1801
+ ScheduleEntity,
1802
+ ScheduleRepository,
1572
1803
  SignalWaiterEntity,
1573
1804
  SignalWaiterRepository,
1574
1805
  StepCheckpointEntity,