@dudousxd/nestjs-durable-store-typeorm 0.24.0 → 0.25.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
@@ -38,7 +38,8 @@ __export(index_exports, {
38
38
  durableManagedTables: () => durableManagedTables,
39
39
  ensureTypeOrmDurableSchema: () => ensureTypeOrmDurableSchema,
40
40
  jsonBlobColumnType: () => jsonBlobColumnType,
41
- parseJsonColumn: () => parseJsonColumn
41
+ parseJsonColumn: () => parseJsonColumn,
42
+ typeOrmSchemaRepairWarning: () => typeOrmSchemaRepairWarning
42
43
  });
43
44
  module.exports = __toCommonJS(index_exports);
44
45
 
@@ -642,7 +643,19 @@ function buildWidenStatements(isMysql, quote, col) {
642
643
  return out;
643
644
  }
644
645
  __name(buildWidenStatements, "buildWidenStatements");
645
- async function ensureTypeOrmDurableSchema(dataSource) {
646
+ function typeOrmSchemaRepairWarning(repairs) {
647
+ return [
648
+ `[nestjs-durable] store-typeorm repaired the durable schema at boot: ${repairs.join("; ")}.`,
649
+ "The database had drifted from the schema this library version expects (it was created by an",
650
+ "older version, or a migration left these out), so they were applied at runtime and nothing",
651
+ "recorded them in your migration history. To make them permanent, add a migration that calls",
652
+ "`ensureTypeOrmDurableSchema(queryRunner.connection)`. This warning appears only when a repair",
653
+ "was applied; a schema that is already current logs nothing."
654
+ ].join(" ");
655
+ }
656
+ __name(typeOrmSchemaRepairWarning, "typeOrmSchemaRepairWarning");
657
+ async function ensureTypeOrmDurableSchema(dataSource, options = {}) {
658
+ const logger = options.logger ?? console;
646
659
  const q = /* @__PURE__ */ __name((id) => dataSource.driver.escape(id), "q");
647
660
  const resolve = durableColumnResolver(dataSource);
648
661
  const type = String(dataSource.options.type);
@@ -671,7 +684,9 @@ async function ensureTypeOrmDurableSchema(dataSource) {
671
684
  const num = isPg ? "double precision" : isMysql ? "double" : "real";
672
685
  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`;
673
686
  const tables = [
674
- `CREATE TABLE IF NOT EXISTS ${runs} (
687
+ [
688
+ "durable_workflow_runs",
689
+ `CREATE TABLE IF NOT EXISTS ${runs} (
675
690
  ${runsCol("id")} ${str} PRIMARY KEY,
676
691
  ${runsCol("workflow")} ${str} NOT NULL,
677
692
  ${runsCol("workflowVersion")} ${str} NOT NULL,
@@ -682,8 +697,11 @@ async function ensureTypeOrmDurableSchema(dataSource) {
682
697
  ${runsCol("recoveryAttempts")} ${int}, ${runsCol("tags")} ${txt}, ${runsCol("searchAttributes")} ${txt},
683
698
  ${runsCol("priority")} ${int}, ${runsCol("namespace")} ${nsCol}, ${runsCol("origin")} ${str},
684
699
  ${runsCol("createdAt")} ${ts} NOT NULL, ${runsCol("updatedAt")} ${ts} NOT NULL
685
- )`,
686
- `CREATE TABLE IF NOT EXISTS ${checkpoints} (
700
+ )`
701
+ ],
702
+ [
703
+ "durable_step_checkpoints",
704
+ `CREATE TABLE IF NOT EXISTS ${checkpoints} (
687
705
  ${cpCol("runId")} ${str} NOT NULL, ${cpCol("seq")} ${int} NOT NULL,
688
706
  ${cpCol("name")} ${str} NOT NULL, ${cpCol("kind")} ${str} NOT NULL, ${cpCol("stepId")} ${str} NOT NULL,
689
707
  ${cpCol("status")} ${str} NOT NULL, ${cpCol("input")} ${txt}, ${cpCol("output")} ${txt}, ${cpCol("error")} ${txt}, ${cpCol("events")} ${txt},
@@ -691,30 +709,45 @@ async function ensureTypeOrmDurableSchema(dataSource) {
691
709
  ${cpCol("wakeAt")} ${ts}, ${cpCol("enqueuedAt")} ${ts},
692
710
  ${cpCol("startedAt")} ${ts} NOT NULL, ${cpCol("finishedAt")} ${ts} NOT NULL,
693
711
  PRIMARY KEY (${cpCol("runId")}, ${cpCol("seq")})
694
- )`,
712
+ )`
713
+ ],
695
714
  // Normalized search-attribute side-table: one row per (run, key) so attribute predicates push
696
715
  // DOWN into SQL via an EXISTS join (indexed below). `key` is keyed → must be varchar on MySQL.
697
- `CREATE TABLE IF NOT EXISTS ${runAttributes} (
716
+ [
717
+ "durable_run_attributes",
718
+ `CREATE TABLE IF NOT EXISTS ${runAttributes} (
698
719
  ${attrCol("runId")} ${str} NOT NULL, ${attrCol("key")} ${str} NOT NULL,
699
720
  ${attrCol("strValue")} ${str}, ${attrCol("numValue")} ${num},
700
721
  PRIMARY KEY (${attrCol("runId")}, ${attrCol("key")})
701
- )`,
702
- `CREATE TABLE IF NOT EXISTS ${waiters} (
722
+ )`
723
+ ],
724
+ [
725
+ "durable_signal_waiters",
726
+ `CREATE TABLE IF NOT EXISTS ${waiters} (
703
727
  ${waiterCol("token")} ${str} PRIMARY KEY, ${waiterCol("runId")} ${str} NOT NULL, ${waiterCol("seq")} ${int} NOT NULL,
704
728
  ${waiterCol("parallelGroup")} ${str}
705
- )`,
706
- `CREATE TABLE IF NOT EXISTS ${buffered} (
729
+ )`
730
+ ],
731
+ [
732
+ "durable_buffered_signals",
733
+ `CREATE TABLE IF NOT EXISTS ${buffered} (
707
734
  ${bufferedId}, ${bufCol("token")} ${str} NOT NULL, ${bufCol("payload")} ${txt}
708
- )`,
735
+ )`
736
+ ],
709
737
  // Caller-minted uuid PK (engine.publishEvent), not auto-increment — unlike durable_buffered_signals
710
738
  // above, removeBufferedEvent needs to target the EXACT id it was buffered with.
711
- `CREATE TABLE IF NOT EXISTS ${bufferedEvents} (
739
+ [
740
+ "durable_buffered_events",
741
+ `CREATE TABLE IF NOT EXISTS ${bufferedEvents} (
712
742
  ${bevCol("id")} ${str} PRIMARY KEY, ${bevCol("name")} ${str} NOT NULL,
713
743
  ${bevCol("payload")} ${txt}, ${bevCol("publishedAt")} ${ts} NOT NULL
714
- )`,
744
+ )`
745
+ ],
715
746
  // Persisted schedules. `next_fire_at` is epoch ms as a bigint (exact, so the compare-and-set on
716
747
  // it survives MySQL's DATETIME millisecond truncation); `spec`/`state` are engine-owned JSON.
717
- `CREATE TABLE IF NOT EXISTS ${schedules} (
748
+ [
749
+ "durable_schedules",
750
+ `CREATE TABLE IF NOT EXISTS ${schedules} (
718
751
  ${schCol("id")} ${str} PRIMARY KEY, ${schCol("namespace")} ${nsCol},
719
752
  ${schCol("workflow")} ${str} NOT NULL,
720
753
  ${schCol("paused")} boolean NOT NULL DEFAULT ${isPg ? "false" : "0"},
@@ -722,6 +755,7 @@ async function ensureTypeOrmDurableSchema(dataSource) {
722
755
  ${schCol("spec")} ${txt} NOT NULL, ${schCol("state")} ${txt} NOT NULL,
723
756
  ${schCol("createdAt")} ${ts} NOT NULL, ${schCol("updatedAt")} ${ts} NOT NULL
724
757
  )`
758
+ ]
725
759
  ];
726
760
  const additive = {
727
761
  durable_workflow_runs: [
@@ -813,9 +847,21 @@ async function ensureTypeOrmDurableSchema(dataSource) {
813
847
  ]
814
848
  ]
815
849
  };
850
+ const repairs = [];
851
+ const created = /* @__PURE__ */ new Set();
816
852
  const runner = dataSource.createQueryRunner();
817
853
  try {
818
- for (const sql of tables) await runner.query(sql);
854
+ const before = await existingTables(runner, {
855
+ isSqlite,
856
+ isMysql
857
+ });
858
+ for (const [table, sql] of tables) {
859
+ await runner.query(sql);
860
+ if (!before.has(table)) {
861
+ created.add(table);
862
+ repairs.push(`created table ${table}`);
863
+ }
864
+ }
819
865
  for (const [table, props] of Object.entries(additive)) {
820
866
  const have = await existingColumns(runner, table, {
821
867
  isSqlite,
@@ -825,23 +871,35 @@ async function ensureTypeOrmDurableSchema(dataSource) {
825
871
  const column = resolve(table, property);
826
872
  if (!have.has(column)) {
827
873
  await runner.query(`ALTER TABLE ${q(table)} ADD COLUMN ${q(column)} ${colType}`);
874
+ repairs.push(`added column ${table}.${column}`);
828
875
  }
829
876
  }
830
877
  }
831
- for (const sql of buildWidenStatements(isMysql, q, resolve)) {
832
- try {
833
- await runner.query(sql);
834
- } catch {
878
+ if (isMysql) {
879
+ for (const [table, props] of Object.entries(JSON_BLOB_COLUMNS)) {
880
+ const types = await mysqlColumnTypes(runner, table);
881
+ for (const prop of props) {
882
+ const column = resolve(table, prop);
883
+ const current = types.get(column);
884
+ if (current === void 0 || current === "longtext") continue;
885
+ try {
886
+ await runner.query(`ALTER TABLE ${q(table)} MODIFY COLUMN ${q(column)} ${jsonBlobColumnType(true)}`);
887
+ repairs.push(`widened ${table}.${column} from ${current} to longtext`);
888
+ } catch {
889
+ }
890
+ }
835
891
  }
836
892
  }
837
893
  const indexes = [
838
894
  [
839
895
  "durable_runs_status_idx",
840
- `${runs} (${runsCol("status")}, ${runsCol("wakeAt")})`
896
+ `${runs} (${runsCol("status")}, ${runsCol("wakeAt")})`,
897
+ "durable_workflow_runs"
841
898
  ],
842
899
  [
843
900
  "durable_runs_workflow_status_idx",
844
- `${runs} (${runsCol("workflow")}, ${runsCol("status")})`
901
+ `${runs} (${runsCol("workflow")}, ${runsCol("status")})`,
902
+ "durable_workflow_runs"
845
903
  ],
846
904
  // namespace/status/createdAt: every poll tick of a namespaced worker filters on `namespace`
847
905
  // ANDed with `status` (and `listPendingRuns` orders by `createdAt`), so without this the two
@@ -850,57 +908,89 @@ async function ensureTypeOrmDurableSchema(dataSource) {
850
908
  // already exists picks it up as soon as it runs this version.
851
909
  [
852
910
  "durable_runs_namespace_status_idx",
853
- `${runs} (${runsCol("namespace")}, ${runsCol("status")}, ${runsCol("createdAt")})`
911
+ `${runs} (${runsCol("namespace")}, ${runsCol("status")}, ${runsCol("createdAt")})`,
912
+ "durable_workflow_runs"
854
913
  ],
855
914
  // A run's checkpoints by fan, in seq order: the run detail's per-group aggregates and fan
856
915
  // pages. `parallel_group` is `varchar(191)` in this DDL (also when added by ALTER above), so
857
916
  // MySQL can key it without a prefix length.
858
917
  [
859
918
  "durable_step_checkpoints_group_idx",
860
- `${checkpoints} (${cpCol("runId")}, ${cpCol("parallelGroup")}, ${cpCol("seq")})`
919
+ `${checkpoints} (${cpCol("runId")}, ${cpCol("parallelGroup")}, ${cpCol("seq")})`,
920
+ "durable_step_checkpoints"
861
921
  ],
862
922
  // A run's waiters: the `ctx.all` join barrier probes them once per settled child.
863
923
  [
864
924
  "durable_signal_waiters_run_id_idx",
865
- `${waiters} (${waiterCol("runId")})`
925
+ `${waiters} (${waiterCol("runId")})`,
926
+ "durable_signal_waiters"
866
927
  ],
867
928
  // buffered signals are taken FIFO per token (smallest id) — index the token for the scan.
868
929
  [
869
930
  "durable_buffered_signals_token_idx",
870
- `${buffered} (${bufCol("token")})`
931
+ `${buffered} (${bufCol("token")})`,
932
+ "durable_buffered_signals"
871
933
  ],
872
934
  // buffered events are scanned oldest-first per name — one composite index serves both.
873
935
  [
874
936
  "durable_buffered_events_name_published_at_idx",
875
- `${bufferedEvents} (${bevCol("name")}, ${bevCol("publishedAt")})`
937
+ `${bufferedEvents} (${bevCol("name")}, ${bevCol("publishedAt")})`,
938
+ "durable_buffered_events"
876
939
  ],
877
940
  // Search-attribute pushdown: equality + range predicates probe by (key, value). Two composite
878
941
  // indexes — one per typed column — so a numeric range or a string equality is an index scan.
879
942
  // Persisted schedules: the poll's "due by T" scan (`paused = false AND next_fire_at <= T`).
880
943
  [
881
944
  "durable_schedules_due_idx",
882
- `${schedules} (${schCol("paused")}, ${schCol("nextFireAt")})`
945
+ `${schedules} (${schCol("paused")}, ${schCol("nextFireAt")})`,
946
+ "durable_schedules"
883
947
  ],
884
948
  [
885
949
  "durable_run_attributes_num_idx",
886
- `${runAttributes} (${attrCol("key")}, ${attrCol("numValue")})`
950
+ `${runAttributes} (${attrCol("key")}, ${attrCol("numValue")})`,
951
+ "durable_run_attributes"
887
952
  ],
888
953
  [
889
954
  "durable_run_attributes_str_idx",
890
- `${runAttributes} (${attrCol("key")}, ${attrCol("strValue")})`
955
+ `${runAttributes} (${attrCol("key")}, ${attrCol("strValue")})`,
956
+ "durable_run_attributes"
891
957
  ]
892
958
  ];
893
- for (const [name, target] of indexes) {
959
+ for (const [name, target, table] of indexes) {
894
960
  try {
895
961
  await runner.query(`CREATE INDEX ${q(name)} ON ${target}`);
962
+ if (!created.has(table)) repairs.push(`created index ${name} on ${table}`);
896
963
  } catch {
897
964
  }
898
965
  }
899
966
  } finally {
900
967
  await runner.release();
901
968
  }
969
+ const freshInstall = created.size === tables.length;
970
+ if (repairs.length > 0 && !freshInstall) logger.warn(typeOrmSchemaRepairWarning(repairs));
902
971
  }
903
972
  __name(ensureTypeOrmDurableSchema, "ensureTypeOrmDurableSchema");
973
+ async function existingTables(runner, d) {
974
+ const names = durableManagedTables();
975
+ if (d.isSqlite) {
976
+ const rows2 = await runner.query("SELECT name FROM sqlite_master WHERE type = 'table'");
977
+ return new Set(rows2.map((r) => r.name).filter((name) => names.includes(name)));
978
+ }
979
+ const scope = d.isMysql ? "TABLE_SCHEMA = DATABASE()" : "table_schema = current_schema()";
980
+ const rows = await runner.query(`SELECT table_name AS name FROM information_schema.tables WHERE ${scope}`);
981
+ return new Set(rows.map((r) => r.name ?? r.TABLE_NAME).filter((name) => names.includes(name)));
982
+ }
983
+ __name(existingTables, "existingTables");
984
+ async function mysqlColumnTypes(runner, table) {
985
+ const rows = await runner.query("SELECT column_name AS name, data_type AS type FROM information_schema.columns WHERE table_name = ? AND TABLE_SCHEMA = DATABASE()", [
986
+ table
987
+ ]);
988
+ return new Map(rows.map((r) => [
989
+ r.name ?? r.COLUMN_NAME,
990
+ String(r.type ?? r.DATA_TYPE).toLowerCase()
991
+ ]));
992
+ }
993
+ __name(mysqlColumnTypes, "mysqlColumnTypes");
904
994
  async function existingColumns(runner, table, d) {
905
995
  if (d.isSqlite) {
906
996
  const rows2 = await runner.query(`PRAGMA table_info("${table}")`);
@@ -925,11 +1015,15 @@ var TypeOrmStateStore = class {
925
1015
  __name(this, "TypeOrmStateStore");
926
1016
  }
927
1017
  dataSource;
928
- constructor(dataSource) {
1018
+ options;
1019
+ constructor(dataSource, options = {}) {
929
1020
  this.dataSource = dataSource;
1021
+ this.options = options;
930
1022
  }
931
1023
  async ensureSchema() {
932
- await ensureTypeOrmDurableSchema(this.dataSource);
1024
+ await ensureTypeOrmDurableSchema(this.dataSource, {
1025
+ logger: this.options.schemaLogger
1026
+ });
933
1027
  }
934
1028
  runs() {
935
1029
  return this.dataSource.getRepository(WorkflowRunEntity);
@@ -1252,9 +1346,12 @@ var TypeOrmStateStore = class {
1252
1346
  }))).execute();
1253
1347
  return result.affected === 1;
1254
1348
  }
1255
- async releaseRunLock(runId) {
1256
- await this.runs().update({
1349
+ async releaseRunLock(runId, owner) {
1350
+ await this.runs().update(owner === void 0 ? {
1257
1351
  id: runId
1352
+ } : {
1353
+ id: runId,
1354
+ lockedBy: owner
1258
1355
  }, {
1259
1356
  lockedBy: null,
1260
1357
  lockedUntil: /* @__PURE__ */ __name(() => "NULL", "lockedUntil")
@@ -1929,6 +2026,7 @@ __name(updateIfPending, "updateIfPending");
1929
2026
  durableManagedTables,
1930
2027
  ensureTypeOrmDurableSchema,
1931
2028
  jsonBlobColumnType,
1932
- parseJsonColumn
2029
+ parseJsonColumn,
2030
+ typeOrmSchemaRepairWarning
1933
2031
  });
1934
2032
  //# sourceMappingURL=index.cjs.map