@dudousxd/nestjs-durable-store-mikro-orm 0.33.1 → 0.33.2

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
@@ -45,6 +45,13 @@ declare class ScheduleRepository extends EntityRepository<ScheduleEntity> {
45
45
  * swap against an existing table.
46
46
  */
47
47
  type DurableColumnNaming = 'snake_case' | 'preserve' | ((property: string) => string);
48
+ /**
49
+ * Fractional-second digits on every timestamp column. MySQL's `datetime` defaults to 0 and ROUNDS
50
+ * the milliseconds away, which moves leases, timers and event order by up to half a second. 6 rather
51
+ * than 3 because it is Postgres's default `timestamptz` precision, so pinning it changes no
52
+ * Postgres column; SQLite ignores it.
53
+ */
54
+ declare const DATETIME_PRECISION = 6;
48
55
  declare class WorkflowRunEntity {
49
56
  [EntityRepositoryType]?: WorkflowRunRepository;
50
57
  id: string;
@@ -171,7 +178,27 @@ interface DurableSchemaLogger {
171
178
  interface EnsureMikroOrmDurableSchemaOptions {
172
179
  /** Sink for the schema-repair warning. Defaults to `console`. */
173
180
  logger?: DurableSchemaLogger | undefined;
181
+ /**
182
+ * MySQL only: the largest estimated row count (`information_schema.tables.table_rows`) of an
183
+ * existing durable table on which the boot-time heal will run a column TYPE change. MySQL runs a
184
+ * type change (e.g. `datetime` -> `datetime(6)`) as a full table copy that blocks writes until it
185
+ * finishes, so on a bigger table the statement is skipped and logged for you to run out of band
186
+ * (a maintenance window, `gh-ost`, `pt-online-schema-change`); every boot logs it again until the
187
+ * column matches. Defaults to {@link DEFAULT_BLOCKING_ALTER_ROW_LIMIT}. `Infinity` always applies.
188
+ */
189
+ blockingAlterRowLimit?: number | undefined;
174
190
  }
191
+ /** Default {@link EnsureMikroOrmDurableSchemaOptions.blockingAlterRowLimit}: a copy of this many rows takes seconds. */
192
+ declare const DEFAULT_BLOCKING_ALTER_ROW_LIMIT = 100000;
193
+ /**
194
+ * The warning logged when the heal skipped a MySQL column type change because the table is too big
195
+ * to rebuild at boot (see {@link EnsureMikroOrmDurableSchemaOptions.blockingAlterRowLimit}).
196
+ */
197
+ declare function mikroOrmDeferredAlterWarning(deferred: ReadonlyArray<{
198
+ table: string;
199
+ rows: number;
200
+ statement: string;
201
+ }>, limit: number): string;
175
202
  /**
176
203
  * The warning logged when the boot-time heal changed an EXISTING durable schema: created a table
177
204
  * that was missing next to ones that exist, added a column or index, changed a column type, or
@@ -234,10 +261,16 @@ interface MikroOrmStateStoreOptions {
234
261
  * satisfies it.
235
262
  */
236
263
  schemaLogger?: DurableSchemaLogger | undefined;
264
+ /**
265
+ * MySQL: the largest existing durable table `ensureSchema()` will rebuild for a column type
266
+ * change at boot; bigger ones are logged for you to run out of band. See
267
+ * `blockingAlterRowLimit` on `ensureMikroOrmDurableSchema`.
268
+ */
269
+ schemaBlockingAlterRowLimit?: number | undefined;
237
270
  }
238
271
  /**
239
272
  * MikroORM-backed `StateStore`. Works on any MikroORM driver — Postgres, MySQL, SQLite (tested);
240
- * timestamps and `wakeAt` use native datetime columns. Each operation runs on a forked
273
+ * timestamps and `wakeAt` use native datetime columns with microsecond precision (MySQL `datetime(6)`). Each operation runs on a forked
241
274
  * EntityManager so it owns its unit of work.
242
275
  *
243
276
  * Pass `opts.scope.namespace` to confine all reads to a single tenant namespace. Omit for the
@@ -413,4 +446,4 @@ declare class MikroOrmStateStore implements StateStore {
413
446
  removeBufferedEvent(id: string): Promise<boolean>;
414
447
  }
415
448
 
416
- export { BufferedEventEntity, BufferedEventRepository, BufferedSignalEntity, BufferedSignalRepository, type DurableColumnNaming, type DurableSchemaLogger, ENTITIES, type EnsureMikroOrmDurableSchemaOptions, MikroOrmStateStore, type MikroOrmStateStoreOptions, RunAttributeEntity, RunAttributeRepository, ScheduleEntity, ScheduleRepository, SignalWaiterEntity, SignalWaiterRepository, StepCheckpointEntity, StepCheckpointRepository, WorkflowRunEntity, WorkflowRunRepository, durableEntities, durableManagedTables, ensureMikroOrmDurableSchema, mikroOrmSchemaRepairWarning };
449
+ export { BufferedEventEntity, BufferedEventRepository, BufferedSignalEntity, BufferedSignalRepository, DATETIME_PRECISION, DEFAULT_BLOCKING_ALTER_ROW_LIMIT, type DurableColumnNaming, type DurableSchemaLogger, ENTITIES, type EnsureMikroOrmDurableSchemaOptions, MikroOrmStateStore, type MikroOrmStateStoreOptions, RunAttributeEntity, RunAttributeRepository, ScheduleEntity, ScheduleRepository, SignalWaiterEntity, SignalWaiterRepository, StepCheckpointEntity, StepCheckpointRepository, WorkflowRunEntity, WorkflowRunRepository, durableEntities, durableManagedTables, ensureMikroOrmDurableSchema, mikroOrmDeferredAlterWarning, mikroOrmSchemaRepairWarning };
package/dist/index.d.ts CHANGED
@@ -45,6 +45,13 @@ declare class ScheduleRepository extends EntityRepository<ScheduleEntity> {
45
45
  * swap against an existing table.
46
46
  */
47
47
  type DurableColumnNaming = 'snake_case' | 'preserve' | ((property: string) => string);
48
+ /**
49
+ * Fractional-second digits on every timestamp column. MySQL's `datetime` defaults to 0 and ROUNDS
50
+ * the milliseconds away, which moves leases, timers and event order by up to half a second. 6 rather
51
+ * than 3 because it is Postgres's default `timestamptz` precision, so pinning it changes no
52
+ * Postgres column; SQLite ignores it.
53
+ */
54
+ declare const DATETIME_PRECISION = 6;
48
55
  declare class WorkflowRunEntity {
49
56
  [EntityRepositoryType]?: WorkflowRunRepository;
50
57
  id: string;
@@ -171,7 +178,27 @@ interface DurableSchemaLogger {
171
178
  interface EnsureMikroOrmDurableSchemaOptions {
172
179
  /** Sink for the schema-repair warning. Defaults to `console`. */
173
180
  logger?: DurableSchemaLogger | undefined;
181
+ /**
182
+ * MySQL only: the largest estimated row count (`information_schema.tables.table_rows`) of an
183
+ * existing durable table on which the boot-time heal will run a column TYPE change. MySQL runs a
184
+ * type change (e.g. `datetime` -> `datetime(6)`) as a full table copy that blocks writes until it
185
+ * finishes, so on a bigger table the statement is skipped and logged for you to run out of band
186
+ * (a maintenance window, `gh-ost`, `pt-online-schema-change`); every boot logs it again until the
187
+ * column matches. Defaults to {@link DEFAULT_BLOCKING_ALTER_ROW_LIMIT}. `Infinity` always applies.
188
+ */
189
+ blockingAlterRowLimit?: number | undefined;
174
190
  }
191
+ /** Default {@link EnsureMikroOrmDurableSchemaOptions.blockingAlterRowLimit}: a copy of this many rows takes seconds. */
192
+ declare const DEFAULT_BLOCKING_ALTER_ROW_LIMIT = 100000;
193
+ /**
194
+ * The warning logged when the heal skipped a MySQL column type change because the table is too big
195
+ * to rebuild at boot (see {@link EnsureMikroOrmDurableSchemaOptions.blockingAlterRowLimit}).
196
+ */
197
+ declare function mikroOrmDeferredAlterWarning(deferred: ReadonlyArray<{
198
+ table: string;
199
+ rows: number;
200
+ statement: string;
201
+ }>, limit: number): string;
175
202
  /**
176
203
  * The warning logged when the boot-time heal changed an EXISTING durable schema: created a table
177
204
  * that was missing next to ones that exist, added a column or index, changed a column type, or
@@ -234,10 +261,16 @@ interface MikroOrmStateStoreOptions {
234
261
  * satisfies it.
235
262
  */
236
263
  schemaLogger?: DurableSchemaLogger | undefined;
264
+ /**
265
+ * MySQL: the largest existing durable table `ensureSchema()` will rebuild for a column type
266
+ * change at boot; bigger ones are logged for you to run out of band. See
267
+ * `blockingAlterRowLimit` on `ensureMikroOrmDurableSchema`.
268
+ */
269
+ schemaBlockingAlterRowLimit?: number | undefined;
237
270
  }
238
271
  /**
239
272
  * MikroORM-backed `StateStore`. Works on any MikroORM driver — Postgres, MySQL, SQLite (tested);
240
- * timestamps and `wakeAt` use native datetime columns. Each operation runs on a forked
273
+ * timestamps and `wakeAt` use native datetime columns with microsecond precision (MySQL `datetime(6)`). Each operation runs on a forked
241
274
  * EntityManager so it owns its unit of work.
242
275
  *
243
276
  * Pass `opts.scope.namespace` to confine all reads to a single tenant namespace. Omit for the
@@ -413,4 +446,4 @@ declare class MikroOrmStateStore implements StateStore {
413
446
  removeBufferedEvent(id: string): Promise<boolean>;
414
447
  }
415
448
 
416
- export { BufferedEventEntity, BufferedEventRepository, BufferedSignalEntity, BufferedSignalRepository, type DurableColumnNaming, type DurableSchemaLogger, ENTITIES, type EnsureMikroOrmDurableSchemaOptions, MikroOrmStateStore, type MikroOrmStateStoreOptions, RunAttributeEntity, RunAttributeRepository, ScheduleEntity, ScheduleRepository, SignalWaiterEntity, SignalWaiterRepository, StepCheckpointEntity, StepCheckpointRepository, WorkflowRunEntity, WorkflowRunRepository, durableEntities, durableManagedTables, ensureMikroOrmDurableSchema, mikroOrmSchemaRepairWarning };
449
+ export { BufferedEventEntity, BufferedEventRepository, BufferedSignalEntity, BufferedSignalRepository, DATETIME_PRECISION, DEFAULT_BLOCKING_ALTER_ROW_LIMIT, type DurableColumnNaming, type DurableSchemaLogger, ENTITIES, type EnsureMikroOrmDurableSchemaOptions, MikroOrmStateStore, type MikroOrmStateStoreOptions, RunAttributeEntity, RunAttributeRepository, ScheduleEntity, ScheduleRepository, SignalWaiterEntity, SignalWaiterRepository, StepCheckpointEntity, StepCheckpointRepository, WorkflowRunEntity, WorkflowRunRepository, durableEntities, durableManagedTables, ensureMikroOrmDurableSchema, mikroOrmDeferredAlterWarning, mikroOrmSchemaRepairWarning };
package/dist/index.js CHANGED
@@ -47,6 +47,7 @@ function snakeCase(property) {
47
47
  return property.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
48
48
  }
49
49
  __name(snakeCase, "snakeCase");
50
+ var DATETIME_PRECISION = 6;
50
51
  function columnNamer(naming) {
51
52
  if (naming === "snake_case") return snakeCase;
52
53
  if (naming === "preserve") return (property) => property;
@@ -253,6 +254,7 @@ function durableEntities(options = {}) {
253
254
  },
254
255
  wakeAt: {
255
256
  type: "Date",
257
+ length: DATETIME_PRECISION,
256
258
  nullable: true,
257
259
  fieldName: col("wakeAt")
258
260
  },
@@ -263,6 +265,7 @@ function durableEntities(options = {}) {
263
265
  },
264
266
  lockedUntil: {
265
267
  type: "Date",
268
+ length: DATETIME_PRECISION,
266
269
  nullable: true,
267
270
  fieldName: col("lockedUntil")
268
271
  },
@@ -325,10 +328,12 @@ function durableEntities(options = {}) {
325
328
  },
326
329
  createdAt: {
327
330
  type: "Date",
331
+ length: DATETIME_PRECISION,
328
332
  fieldName: col("createdAt")
329
333
  },
330
334
  updatedAt: {
331
335
  type: "Date",
336
+ length: DATETIME_PRECISION,
332
337
  fieldName: col("updatedAt")
333
338
  }
334
339
  }
@@ -417,20 +422,24 @@ function durableEntities(options = {}) {
417
422
  },
418
423
  wakeAt: {
419
424
  type: "Date",
425
+ length: DATETIME_PRECISION,
420
426
  nullable: true,
421
427
  fieldName: col("wakeAt")
422
428
  },
423
429
  enqueuedAt: {
424
430
  type: "Date",
431
+ length: DATETIME_PRECISION,
425
432
  nullable: true,
426
433
  fieldName: col("enqueuedAt")
427
434
  },
428
435
  startedAt: {
429
436
  type: "Date",
437
+ length: DATETIME_PRECISION,
430
438
  fieldName: col("startedAt")
431
439
  },
432
440
  finishedAt: {
433
441
  type: "Date",
442
+ length: DATETIME_PRECISION,
434
443
  fieldName: col("finishedAt")
435
444
  }
436
445
  }
@@ -575,6 +584,7 @@ function durableEntities(options = {}) {
575
584
  },
576
585
  publishedAt: {
577
586
  type: "Date",
587
+ length: DATETIME_PRECISION,
578
588
  fieldName: col("publishedAt")
579
589
  }
580
590
  }
@@ -633,10 +643,12 @@ function durableEntities(options = {}) {
633
643
  },
634
644
  createdAt: {
635
645
  type: "Date",
646
+ length: DATETIME_PRECISION,
636
647
  fieldName: col("createdAt")
637
648
  },
638
649
  updatedAt: {
639
650
  type: "Date",
651
+ length: DATETIME_PRECISION,
640
652
  fieldName: col("updatedAt")
641
653
  }
642
654
  }
@@ -679,6 +691,21 @@ var MARKER_TABLE = "durable_schema_meta";
679
691
  var MARKER_ROW_ID = "durable";
680
692
  var SCHEMA_LOCK_NAME = "durable_schema";
681
693
  var SCHEMA_REVISION = 1;
694
+ var DEFAULT_BLOCKING_ALTER_ROW_LIMIT = 1e5;
695
+ function deferredAlterWarning(store, deferred, limit) {
696
+ return [
697
+ `[nestjs-durable] ${store} did NOT apply ${deferred.length} durable-schema column type change(s) at boot.`,
698
+ `MySQL rebuilds a table to change a column type and blocks writes to it until the copy ends, and these tables hold more than blockingAlterRowLimit (${limit}) rows:`,
699
+ ...deferred.map((d) => ` -- ${d.table}, ~${d.rows} rows
700
+ ${d.statement};`),
701
+ "Run them in a maintenance window or with an online schema change tool (gh-ost, pt-online-schema-change), or raise blockingAlterRowLimit to let a boot run them. Until then the store keeps working on the old column types, and this warning repeats on every boot."
702
+ ].join("\n");
703
+ }
704
+ __name(deferredAlterWarning, "deferredAlterWarning");
705
+ function mikroOrmDeferredAlterWarning(deferred, limit) {
706
+ return deferredAlterWarning("store-mikro-orm", deferred, limit);
707
+ }
708
+ __name(mikroOrmDeferredAlterWarning, "mikroOrmDeferredAlterWarning");
682
709
  function mikroOrmSchemaRepairWarning(repairs) {
683
710
  return [
684
711
  `[nestjs-durable] store-mikro-orm repaired the durable schema at boot: ${repairs.join("; ")}.`,
@@ -883,29 +910,43 @@ async function ensureMikroOrmDurableSchema(orm, options = {}) {
883
910
  if (await readStoredFingerprint(orm) === expected) {
884
911
  return;
885
912
  }
886
- const { repairs, freshInstall } = await healDurableSchema(orm);
887
- await writeFingerprint(orm, dialect, expected);
913
+ const limit = options.blockingAlterRowLimit ?? DEFAULT_BLOCKING_ALTER_ROW_LIMIT;
914
+ const { repairs, freshInstall, deferred } = await healDurableSchema(orm, dialect, limit);
915
+ if (deferred.length === 0) await writeFingerprint(orm, dialect, expected);
888
916
  if (repairs.length > 0 && !freshInstall) logger.warn(mikroOrmSchemaRepairWarning(repairs));
917
+ if (deferred.length > 0) logger.warn(mikroOrmDeferredAlterWarning(deferred, limit));
889
918
  } finally {
890
919
  await releaseSchemaLock(orm, dialect);
891
920
  }
892
921
  }
893
922
  __name(ensureMikroOrmDurableSchema, "ensureMikroOrmDurableSchema");
894
- async function healDurableSchema(orm) {
923
+ async function healDurableSchema(orm, dialect, blockingAlterRowLimit) {
895
924
  const sql = await orm.schema.getUpdateSchemaSQL({
896
925
  safe: true
897
926
  });
898
- const ours = sql.split(";").map((statement) => statement.trim()).filter((statement) => {
927
+ const ours = mergeColumnTypeChanges(sql.split(";").map((statement) => statement.trim()).filter((statement) => {
899
928
  const table = targetTable(statement);
900
929
  return table !== void 0 && DURABLE_TABLE_NAMES.has(table);
901
- });
930
+ }));
902
931
  const connection = orm.em.getConnection();
903
932
  const created = new Set(ours.filter((statement) => /^create\s+table\b/i.test(statement)).map((statement) => targetTable(statement)));
904
933
  const repairs = [];
934
+ const deferred = [];
905
935
  for (const statement of ours) {
936
+ const table = targetTable(statement);
937
+ if (dialect === "mysql" && !created.has(table) && isColumnTypeChange(statement)) {
938
+ const rows = await estimatedRowCount(orm, table);
939
+ if (rows > blockingAlterRowLimit) {
940
+ deferred.push({
941
+ table,
942
+ rows,
943
+ statement
944
+ });
945
+ continue;
946
+ }
947
+ }
906
948
  try {
907
949
  await connection.execute(statement);
908
- const table = targetTable(statement);
909
950
  const isCreateTable = /^create\s+table\b/i.test(statement);
910
951
  if (isCreateTable || !created.has(table)) repairs.push(...describeDurableStatement(statement));
911
952
  } catch (err) {
@@ -925,10 +966,45 @@ async function healDurableSchema(orm) {
925
966
  ].every((table) => created.has(table));
926
967
  return {
927
968
  repairs,
928
- freshInstall
969
+ freshInstall,
970
+ deferred
929
971
  };
930
972
  }
931
973
  __name(healDurableSchema, "healDurableSchema");
974
+ function mergeColumnTypeChanges(statements) {
975
+ const merged = [];
976
+ const at = /* @__PURE__ */ new Map();
977
+ for (const statement of statements) {
978
+ const table = targetTable(statement);
979
+ if (!table || !isColumnTypeChange(statement)) {
980
+ merged.push(statement);
981
+ continue;
982
+ }
983
+ const actions = statement.replace(/^alter\s+table\s+[`"']?[\w$]+[`"']?\s*/i, "");
984
+ const index = at.get(table);
985
+ if (index === void 0) {
986
+ at.set(table, merged.length);
987
+ merged.push(statement);
988
+ } else {
989
+ merged[index] = `${merged[index]}, ${actions}`;
990
+ }
991
+ }
992
+ return merged;
993
+ }
994
+ __name(mergeColumnTypeChanges, "mergeColumnTypeChanges");
995
+ function isColumnTypeChange(statement) {
996
+ if (!/^alter\s+table\b/i.test(statement)) return false;
997
+ const actions = splitTopLevel(statement.replace(/^alter\s+table\s+[`"']?[\w$]+[`"']?\s*/i, ""));
998
+ return actions.length > 0 && actions.every((action) => /^modify\b/i.test(action));
999
+ }
1000
+ __name(isColumnTypeChange, "isColumnTypeChange");
1001
+ async function estimatedRowCount(orm, table) {
1002
+ const rows = await orm.em.getConnection().execute("select table_rows as table_rows from information_schema.tables where table_schema = database() and table_name = ? limit 1", [
1003
+ table
1004
+ ]);
1005
+ return Number(rows[0]?.table_rows ?? 0);
1006
+ }
1007
+ __name(estimatedRowCount, "estimatedRowCount");
932
1008
  function isSafeSqlIdentifier(value) {
933
1009
  return /^[A-Za-z0-9_]+$/.test(value);
934
1010
  }
@@ -1012,7 +1088,8 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1012
1088
  }
1013
1089
  async ensureSchema() {
1014
1090
  await ensureMikroOrmDurableSchema(this.orm, {
1015
- logger: this.opts.schemaLogger
1091
+ logger: this.opts.schemaLogger,
1092
+ blockingAlterRowLimit: this.opts.schemaBlockingAlterRowLimit
1016
1093
  });
1017
1094
  }
1018
1095
  async tryAdmitSingleton(runId, tag, workflow, limit, retryWakeAt) {
@@ -2242,6 +2319,8 @@ export {
2242
2319
  BufferedEventRepository,
2243
2320
  BufferedSignalEntity,
2244
2321
  BufferedSignalRepository,
2322
+ DATETIME_PRECISION,
2323
+ DEFAULT_BLOCKING_ALTER_ROW_LIMIT,
2245
2324
  ENTITIES,
2246
2325
  MikroOrmStateStore,
2247
2326
  RunAttributeEntity,
@@ -2257,6 +2336,7 @@ export {
2257
2336
  durableEntities,
2258
2337
  durableManagedTables,
2259
2338
  ensureMikroOrmDurableSchema,
2339
+ mikroOrmDeferredAlterWarning,
2260
2340
  mikroOrmSchemaRepairWarning
2261
2341
  };
2262
2342
  //# sourceMappingURL=index.js.map