@dudousxd/nestjs-durable-store-typeorm 0.18.0 → 0.19.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
@@ -26,6 +26,7 @@ __export(index_exports, {
26
26
  ENTITIES: () => ENTITIES,
27
27
  JSON_BLOB_COLUMNS: () => JSON_BLOB_COLUMNS,
28
28
  RunAttributeEntity: () => RunAttributeEntity,
29
+ ScheduleEntity: () => ScheduleEntity,
29
30
  SignalWaiterEntity: () => SignalWaiterEntity,
30
31
  StepCheckpointEntity: () => StepCheckpointEntity,
31
32
  TypeOrmStateStore: () => TypeOrmStateStore,
@@ -152,6 +153,23 @@ var BufferedEventEntity = class {
152
153
  payload;
153
154
  publishedAt;
154
155
  };
156
+ var ScheduleEntity = class {
157
+ static {
158
+ __name(this, "ScheduleEntity");
159
+ }
160
+ id;
161
+ namespace;
162
+ workflow;
163
+ paused;
164
+ /** Epoch ms as a `bigint` (drivers hand it back as a string) — an exact integer, so the
165
+ * compare-and-set in `updateSchedule` can't be defeated by a DATETIME's truncated milliseconds. */
166
+ nextFireAt;
167
+ tags;
168
+ spec;
169
+ state;
170
+ createdAt;
171
+ updatedAt;
172
+ };
155
173
  function durableEntities(options = {}) {
156
174
  const col = columnNamer(options.naming ?? "snake_case");
157
175
  const workflowRuns = new import_typeorm.EntitySchema({
@@ -479,13 +497,69 @@ function durableEntities(options = {}) {
479
497
  }
480
498
  }
481
499
  });
500
+ const schedules = new import_typeorm.EntitySchema({
501
+ name: "ScheduleEntity",
502
+ target: ScheduleEntity,
503
+ tableName: "durable_schedules",
504
+ columns: {
505
+ id: {
506
+ type: "text",
507
+ primary: true,
508
+ name: col("id")
509
+ },
510
+ namespace: {
511
+ type: "text",
512
+ name: col("namespace"),
513
+ default: "default"
514
+ },
515
+ workflow: {
516
+ type: "text",
517
+ name: col("workflow")
518
+ },
519
+ paused: {
520
+ type: Boolean,
521
+ name: col("paused"),
522
+ default: false
523
+ },
524
+ nextFireAt: {
525
+ type: "bigint",
526
+ nullable: true,
527
+ name: col("nextFireAt")
528
+ },
529
+ tags: {
530
+ type: "text",
531
+ nullable: true,
532
+ name: col("tags"),
533
+ transformer: jsonColumnTransformer("schedules.tags")
534
+ },
535
+ spec: {
536
+ type: "text",
537
+ name: col("spec"),
538
+ transformer: jsonColumnTransformer("schedules.spec")
539
+ },
540
+ state: {
541
+ type: "text",
542
+ name: col("state"),
543
+ transformer: jsonColumnTransformer("schedules.state")
544
+ },
545
+ createdAt: {
546
+ type: Date,
547
+ name: col("createdAt")
548
+ },
549
+ updatedAt: {
550
+ type: Date,
551
+ name: col("updatedAt")
552
+ }
553
+ }
554
+ });
482
555
  return [
483
556
  workflowRuns,
484
557
  stepCheckpoints,
485
558
  runAttributes,
486
559
  signalWaiters,
487
560
  bufferedSignals,
488
- bufferedEvents
561
+ bufferedEvents,
562
+ schedules
489
563
  ];
490
564
  }
491
565
  __name(durableEntities, "durableEntities");
@@ -513,7 +587,8 @@ var TABLE_TO_ENTITY = {
513
587
  durable_run_attributes: RunAttributeEntity,
514
588
  durable_signal_waiters: SignalWaiterEntity,
515
589
  durable_buffered_signals: BufferedSignalEntity,
516
- durable_buffered_events: BufferedEventEntity
590
+ durable_buffered_events: BufferedEventEntity,
591
+ durable_schedules: ScheduleEntity
517
592
  };
518
593
  function durableManagedTables() {
519
594
  return Object.keys(TABLE_TO_ENTITY);
@@ -571,6 +646,8 @@ async function ensureTypeOrmDurableSchema(dataSource) {
571
646
  const waiterCol = /* @__PURE__ */ __name((property) => q(resolve("durable_signal_waiters", property)), "waiterCol");
572
647
  const bufCol = /* @__PURE__ */ __name((property) => q(resolve("durable_buffered_signals", property)), "bufCol");
573
648
  const bevCol = /* @__PURE__ */ __name((property) => q(resolve("durable_buffered_events", property)), "bevCol");
649
+ const schedules = q("durable_schedules");
650
+ const schCol = /* @__PURE__ */ __name((property) => q(resolve("durable_schedules", property)), "schCol");
574
651
  const num = isPg ? "double precision" : isMysql ? "double" : "real";
575
652
  const bufferedId = isSqlite ? `${bufCol("id")} integer PRIMARY KEY AUTOINCREMENT` : isMysql ? `${bufCol("id")} bigint NOT NULL AUTO_INCREMENT PRIMARY KEY` : isPg ? `${bufCol("id")} bigserial PRIMARY KEY` : `${bufCol("id")} bigint NOT NULL AUTO_INCREMENT PRIMARY KEY`;
576
653
  const tables = [
@@ -614,6 +691,16 @@ async function ensureTypeOrmDurableSchema(dataSource) {
614
691
  `CREATE TABLE IF NOT EXISTS ${bufferedEvents} (
615
692
  ${bevCol("id")} ${str} PRIMARY KEY, ${bevCol("name")} ${str} NOT NULL,
616
693
  ${bevCol("payload")} ${txt}, ${bevCol("publishedAt")} ${ts} NOT NULL
694
+ )`,
695
+ // Persisted schedules. `next_fire_at` is epoch ms as a bigint (exact, so the compare-and-set on
696
+ // it survives MySQL's DATETIME millisecond truncation); `spec`/`state` are engine-owned JSON.
697
+ `CREATE TABLE IF NOT EXISTS ${schedules} (
698
+ ${schCol("id")} ${str} PRIMARY KEY, ${schCol("namespace")} ${nsCol},
699
+ ${schCol("workflow")} ${str} NOT NULL,
700
+ ${schCol("paused")} boolean NOT NULL DEFAULT ${isPg ? "false" : "0"},
701
+ ${schCol("nextFireAt")} bigint, ${schCol("tags")} ${txt},
702
+ ${schCol("spec")} ${txt} NOT NULL, ${schCol("state")} ${txt} NOT NULL,
703
+ ${schCol("createdAt")} ${ts} NOT NULL, ${schCol("updatedAt")} ${ts} NOT NULL
617
704
  )`
618
705
  ];
619
706
  const additive = {
@@ -757,6 +844,11 @@ async function ensureTypeOrmDurableSchema(dataSource) {
757
844
  ],
758
845
  // Search-attribute pushdown: equality + range predicates probe by (key, value). Two composite
759
846
  // indexes — one per typed column — so a numeric range or a string equality is an index scan.
847
+ // Persisted schedules: the poll's "due by T" scan (`paused = false AND next_fire_at <= T`).
848
+ [
849
+ "durable_schedules_due_idx",
850
+ `${schedules} (${schCol("paused")}, ${schCol("nextFireAt")})`
851
+ ],
760
852
  [
761
853
  "durable_run_attributes_num_idx",
762
854
  `${runAttributes} (${attrCol("key")}, ${attrCol("numValue")})`
@@ -823,6 +915,9 @@ var TypeOrmStateStore = class {
823
915
  attributes() {
824
916
  return this.dataSource.getRepository(RunAttributeEntity);
825
917
  }
918
+ schedules() {
919
+ return this.dataSource.getRepository(ScheduleEntity);
920
+ }
826
921
  /** Rewrite a run's normalized attribute rows: delete the old set, insert the current one. Mirrors
827
922
  * the in-memory store's reindex so the side-table always reflects the run's live searchAttributes. */
828
923
  async reindexAttributes(runId, attributes, em) {
@@ -883,6 +978,52 @@ var TypeOrmStateStore = class {
883
978
  id: runId
884
979
  });
885
980
  }
981
+ async deleteRuns(runIds) {
982
+ if (runIds.length === 0) return;
983
+ await this.dataSource.transaction(async (em) => {
984
+ await em.getRepository(StepCheckpointEntity).delete({
985
+ runId: (0, import_typeorm2.In)(runIds)
986
+ });
987
+ await em.getRepository(SignalWaiterEntity).delete({
988
+ runId: (0, import_typeorm2.In)(runIds)
989
+ });
990
+ await em.getRepository(RunAttributeEntity).delete({
991
+ runId: (0, import_typeorm2.In)(runIds)
992
+ });
993
+ await em.getRepository(WorkflowRunEntity).delete({
994
+ id: (0, import_typeorm2.In)(runIds)
995
+ });
996
+ });
997
+ }
998
+ async pruneTerminalRuns(policy, nowMs, limit) {
999
+ const statuses = policy.statuses.filter((s) => import_nestjs_durable_core.TERMINAL_RUN_STATUSES.includes(s));
1000
+ if (statuses.length === 0 || limit <= 0) return 0;
1001
+ if (policy.maxAge == null && policy.maxCount == null) return 0;
1002
+ const scoped = /* @__PURE__ */ __name(() => this.runQueryBuilder({
1003
+ ...policy.scope ?? {},
1004
+ statuses
1005
+ }).select("r.id", "id"), "scoped");
1006
+ const ids = /* @__PURE__ */ new Set();
1007
+ if (policy.maxAge != null) {
1008
+ const cutoff = new Date(nowMs - (0, import_nestjs_durable_core.parseDuration)(policy.maxAge));
1009
+ const rows = await scoped().andWhere("r.updatedAt < :cutoff", {
1010
+ cutoff
1011
+ }).orderBy("r.updatedAt", "ASC").limit(limit).getRawMany();
1012
+ for (const r of rows) ids.add(r.id);
1013
+ }
1014
+ if (policy.maxCount != null && ids.size < limit) {
1015
+ const rows = await scoped().orderBy("r.updatedAt", "DESC").addOrderBy("r.id", "DESC").limit(limit).offset(policy.maxCount).getRawMany();
1016
+ for (const r of rows) {
1017
+ if (ids.size >= limit) break;
1018
+ ids.add(r.id);
1019
+ }
1020
+ }
1021
+ const doomed = [
1022
+ ...ids
1023
+ ].slice(0, limit);
1024
+ await this.deleteRuns(doomed);
1025
+ return doomed.length;
1026
+ }
886
1027
  async getCheckpoint(runId, seq) {
887
1028
  const e = await this.checkpoints().findOneBy({
888
1029
  runId,
@@ -1038,6 +1179,9 @@ var TypeOrmStateStore = class {
1038
1179
  const rows = await qb.getMany();
1039
1180
  return rows.map(fromRunEntity);
1040
1181
  }
1182
+ async countRuns(query) {
1183
+ return this.runQueryBuilder(query).getCount();
1184
+ }
1041
1185
  /** `GROUP BY status, origin` over the same predicates {@link listRuns} pages — one aggregate, so a
1042
1186
  * console can show whole-set counts next to a bounded page instead of downloading every run to
1043
1187
  * count them in the browser. */
@@ -1100,6 +1244,69 @@ var TypeOrmStateStore = class {
1100
1244
  const type = String(this.dataSource.options.type);
1101
1245
  return type === "mysql" || type === "mariadb" || type === "aurora-mysql" ? "`" : '"';
1102
1246
  }
1247
+ // ---- persisted schedules ----------------------------------------------------------------------
1248
+ async saveSchedule(record) {
1249
+ await this.schedules().save(toScheduleEntity(record));
1250
+ }
1251
+ async getSchedule(id) {
1252
+ const e = await this.schedules().findOneBy({
1253
+ id
1254
+ });
1255
+ return e ? fromScheduleEntity(e) : null;
1256
+ }
1257
+ /** One conditional UPDATE: the compare-and-set on `next_fire_at` (an exact bigint) is atomic. */
1258
+ async updateSchedule(id, patch, expectedNextFireAt) {
1259
+ const set = {};
1260
+ if (patch.namespace !== void 0) set.namespace = patch.namespace;
1261
+ if (patch.workflow !== void 0) set.workflow = patch.workflow;
1262
+ if (patch.paused !== void 0) set.paused = patch.paused;
1263
+ if ("nextFireAt" in patch) set.nextFireAt = patch.nextFireAt ?? null;
1264
+ if ("tags" in patch) set.tags = patch.tags ?? null;
1265
+ if (patch.spec !== void 0) set.spec = patch.spec;
1266
+ if (patch.state !== void 0) set.state = patch.state;
1267
+ if (patch.updatedAt !== void 0) set.updatedAt = patch.updatedAt;
1268
+ if (!Object.keys(set).length) return await this.getSchedule(id) !== null;
1269
+ const qb = this.schedules().createQueryBuilder().update().set(set).where({
1270
+ id
1271
+ });
1272
+ if (expectedNextFireAt === null) qb.andWhere({
1273
+ nextFireAt: (0, import_typeorm2.IsNull)()
1274
+ });
1275
+ else if (expectedNextFireAt !== void 0) qb.andWhere({
1276
+ nextFireAt: expectedNextFireAt
1277
+ });
1278
+ const result = await qb.execute();
1279
+ return (result.affected ?? 0) > 0;
1280
+ }
1281
+ async deleteSchedule(id) {
1282
+ const result = await this.schedules().delete({
1283
+ id
1284
+ });
1285
+ return (result.affected ?? 0) > 0;
1286
+ }
1287
+ async listSchedules(query) {
1288
+ const qb = this.schedules().createQueryBuilder("s");
1289
+ if (query.namespace !== void 0) qb.andWhere("s.namespace = :namespace", {
1290
+ namespace: query.namespace
1291
+ });
1292
+ if (query.workflow !== void 0) qb.andWhere("s.workflow = :workflow", {
1293
+ workflow: query.workflow
1294
+ });
1295
+ if (query.paused !== void 0) qb.andWhere("s.paused = :paused", {
1296
+ paused: query.paused
1297
+ });
1298
+ if (query.tag !== void 0) qb.andWhere("s.tags LIKE :tag", {
1299
+ tag: `%"${query.tag}"%`
1300
+ });
1301
+ if (query.dueBy !== void 0) qb.andWhere("s.paused = :notPaused AND s.nextFireAt <= :dueBy", {
1302
+ notPaused: false,
1303
+ dueBy: query.dueBy
1304
+ });
1305
+ qb.orderBy("CASE WHEN s.nextFireAt IS NULL THEN 1 ELSE 0 END", "ASC").addOrderBy("s.nextFireAt", "ASC").addOrderBy("s.id", "ASC");
1306
+ if (query.limit != null) qb.take(query.limit);
1307
+ if (query.offset != null) qb.skip(query.offset);
1308
+ return (await qb.getMany()).map(fromScheduleEntity);
1309
+ }
1103
1310
  async listCheckpoints(runId) {
1104
1311
  const rows = await this.checkpoints().find({
1105
1312
  where: {
@@ -1343,6 +1550,39 @@ function fromCheckpointEntity(e) {
1343
1550
  };
1344
1551
  }
1345
1552
  __name(fromCheckpointEntity, "fromCheckpointEntity");
1553
+ function toScheduleEntity(r) {
1554
+ const e = new ScheduleEntity();
1555
+ e.id = r.id;
1556
+ e.namespace = r.namespace;
1557
+ e.workflow = r.workflow;
1558
+ e.paused = r.paused;
1559
+ e.nextFireAt = r.nextFireAt;
1560
+ e.tags = r.tags ?? null;
1561
+ e.spec = r.spec;
1562
+ e.state = r.state;
1563
+ e.createdAt = r.createdAt;
1564
+ e.updatedAt = r.updatedAt;
1565
+ return e;
1566
+ }
1567
+ __name(toScheduleEntity, "toScheduleEntity");
1568
+ function fromScheduleEntity(e) {
1569
+ return {
1570
+ id: e.id,
1571
+ namespace: e.namespace,
1572
+ workflow: e.workflow,
1573
+ // SQLite/MySQL hand a boolean column back as 0/1.
1574
+ paused: Boolean(e.paused),
1575
+ nextFireAt: e.nextFireAt == null ? null : Number(e.nextFireAt),
1576
+ ...e.tags ? {
1577
+ tags: e.tags
1578
+ } : {},
1579
+ spec: e.spec ?? {},
1580
+ state: e.state ?? {},
1581
+ createdAt: e.createdAt,
1582
+ updatedAt: e.updatedAt
1583
+ };
1584
+ }
1585
+ __name(fromScheduleEntity, "fromScheduleEntity");
1346
1586
  // Annotate the CommonJS export names for ESM import in node:
1347
1587
  0 && (module.exports = {
1348
1588
  BufferedEventEntity,
@@ -1350,6 +1590,7 @@ __name(fromCheckpointEntity, "fromCheckpointEntity");
1350
1590
  ENTITIES,
1351
1591
  JSON_BLOB_COLUMNS,
1352
1592
  RunAttributeEntity,
1593
+ ScheduleEntity,
1353
1594
  SignalWaiterEntity,
1354
1595
  StepCheckpointEntity,
1355
1596
  TypeOrmStateStore,