@dudousxd/nestjs-durable-store-mikro-orm 0.16.2 → 0.18.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
@@ -116,11 +116,28 @@ var BufferedSignalEntity = class {
116
116
  token;
117
117
  payload;
118
118
  };
119
+ function namespaceFilterCond(args) {
120
+ if (args.namespace === void 0) return {};
121
+ return {
122
+ namespace: args.namespace
123
+ };
124
+ }
125
+ __name(namespaceFilterCond, "namespaceFilterCond");
119
126
  function durableEntities(options = {}) {
120
127
  const col = columnNamer(options.naming ?? "snake_case");
121
128
  const workflowRuns = new import_core.EntitySchema({
122
129
  class: WorkflowRunEntity,
123
130
  tableName: "durable_workflow_runs",
131
+ // Global filter: confines every read to the tenant's namespace when the store is scoped.
132
+ // The `cond` returns `{}` (no-op) when the `namespace` arg is `undefined`, so an unscoped
133
+ // (operator / control-plane) store sees all rows unchanged — existing behaviour is preserved.
134
+ filters: {
135
+ namespace: {
136
+ name: "namespace",
137
+ cond: namespaceFilterCond,
138
+ default: true
139
+ }
140
+ },
124
141
  // Indexes mirror the Prisma adapter so a store swap keeps the same plan. The poller hits these
125
142
  // every tick: `(status, wakeAt)` serves both the due-timer scan (status='suspended' AND wakeAt<=now)
126
143
  // and, via its `status` prefix, the pending/incomplete status lookups; `(workflow, status)` serves
@@ -436,6 +453,7 @@ var ENTITIES = durableEntities();
436
453
  var import_nestjs_durable_core = require("@dudousxd/nestjs-durable-core");
437
454
 
438
455
  // src/schema.ts
456
+ var import_node_crypto = require("crypto");
439
457
  var DURABLE_TABLE_NAMES = /* @__PURE__ */ new Set([
440
458
  "durable_workflow_runs",
441
459
  "durable_step_checkpoints",
@@ -443,6 +461,14 @@ var DURABLE_TABLE_NAMES = /* @__PURE__ */ new Set([
443
461
  "durable_signal_waiters",
444
462
  "durable_buffered_signals"
445
463
  ]);
464
+ var MARKER_TABLE = "durable_schema_meta";
465
+ var MARKER_ROW_ID = "durable";
466
+ var SCHEMA_LOCK_NAME = "durable_schema";
467
+ var SCHEMA_REVISION = 1;
468
+ function compareStrings(a, b) {
469
+ return a < b ? -1 : a > b ? 1 : 0;
470
+ }
471
+ __name(compareStrings, "compareStrings");
446
472
  function targetTable(statement) {
447
473
  const match = statement.match(/^(?:create\s+(?:unique\s+)?index\s+.+?\s+on\s+|(?:create|alter)\s+table\s+(?:if\s+not\s+exists\s+)?)[`"']?([\w$]+)[`"']?/i);
448
474
  return match?.[1]?.toLowerCase();
@@ -452,7 +478,132 @@ function isRequiredStructure(statement) {
452
478
  return /\b(?:create\s+table|create\s+(?:unique\s+)?index|add\s+(?:column|index|constraint|key|unique|fulltext))\b/i.test(statement);
453
479
  }
454
480
  __name(isRequiredStructure, "isRequiredStructure");
481
+ function detectSqlDialect(orm) {
482
+ const platform = String(orm.em.getPlatform().constructor.name).toLowerCase();
483
+ if (platform.includes("mysql") || platform.includes("maria")) return "mysql";
484
+ if (platform.includes("postgre")) return "postgres";
485
+ if (platform.includes("sqlite") || platform.includes("libsql")) return "sqlite";
486
+ return "unknown";
487
+ }
488
+ __name(detectSqlDialect, "detectSqlDialect");
489
+ function normalizeIndexProperties(properties) {
490
+ if (properties === void 0) return [];
491
+ if (Array.isArray(properties)) return properties.map((property) => String(property));
492
+ return [
493
+ String(properties)
494
+ ];
495
+ }
496
+ __name(normalizeIndexProperties, "normalizeIndexProperties");
497
+ function computeExpectedFingerprint(orm, ownedTableNames) {
498
+ const ownedMetadata = [
499
+ ...orm.getMetadata().getAll().values()
500
+ ].filter((meta) => ownedTableNames.has(meta.tableName)).sort((a, b) => compareStrings(a.tableName, b.tableName));
501
+ const tables = ownedMetadata.map((meta) => {
502
+ const columns = [
503
+ ...meta.props
504
+ ].map((prop) => ({
505
+ prop,
506
+ columnName: prop.fieldNames[0] ?? String(prop.name)
507
+ })).sort((a, b) => compareStrings(a.columnName, b.columnName)).map(({ prop, columnName }) => ({
508
+ name: columnName,
509
+ type: prop.columnTypes[0] ?? String(prop.type),
510
+ nullable: prop.nullable === true,
511
+ primary: prop.primary === true,
512
+ default: prop.default ?? null,
513
+ autoincrement: prop.autoincrement === true
514
+ }));
515
+ const indexes = [
516
+ ...meta.indexes
517
+ ].map((index) => ({
518
+ name: index.name ?? "",
519
+ properties: normalizeIndexProperties(index.properties)
520
+ })).sort((a, b) => compareStrings(a.name, b.name));
521
+ return {
522
+ tableName: meta.tableName,
523
+ columns,
524
+ indexes
525
+ };
526
+ });
527
+ const canonical = `${canonicalize(tables)}|collate=${String(orm.config.get("collate") ?? "")}|rev=${SCHEMA_REVISION}`;
528
+ return (0, import_node_crypto.createHash)("sha256").update(canonical).digest("hex");
529
+ }
530
+ __name(computeExpectedFingerprint, "computeExpectedFingerprint");
531
+ function canonicalize(value) {
532
+ if (Array.isArray(value)) {
533
+ return `[${value.map(canonicalize).join(",")}]`;
534
+ }
535
+ if (value !== null && typeof value === "object") {
536
+ const entries = Object.entries(value).sort(([a], [b]) => compareStrings(a, b));
537
+ return `{${entries.map(([key, val]) => `${JSON.stringify(key)}:${canonicalize(val)}`).join(",")}}`;
538
+ }
539
+ return JSON.stringify(value);
540
+ }
541
+ __name(canonicalize, "canonicalize");
542
+ function createMarkerTableSql() {
543
+ return `create table if not exists ${MARKER_TABLE} (id varchar(32) not null primary key, fingerprint varchar(64) not null, applied_at bigint not null)`;
544
+ }
545
+ __name(createMarkerTableSql, "createMarkerTableSql");
546
+ async function readStoredFingerprint(orm) {
547
+ const rows = await orm.em.getConnection().execute(`select fingerprint from ${MARKER_TABLE} where id = ?`, [
548
+ MARKER_ROW_ID
549
+ ]);
550
+ return rows[0]?.fingerprint ?? void 0;
551
+ }
552
+ __name(readStoredFingerprint, "readStoredFingerprint");
553
+ async function writeFingerprint(orm, dialect, fingerprint) {
554
+ const upsert = dialect === "mysql" ? `insert into ${MARKER_TABLE} (id, fingerprint, applied_at) values (?, ?, ?) on duplicate key update fingerprint = values(fingerprint), applied_at = values(applied_at)` : `insert into ${MARKER_TABLE} (id, fingerprint, applied_at) values (?, ?, ?) on conflict (id) do update set fingerprint = excluded.fingerprint, applied_at = excluded.applied_at`;
555
+ await orm.em.getConnection().execute(upsert, [
556
+ MARKER_ROW_ID,
557
+ fingerprint,
558
+ Date.now()
559
+ ]);
560
+ }
561
+ __name(writeFingerprint, "writeFingerprint");
562
+ async function acquireSchemaLock(orm, dialect) {
563
+ try {
564
+ if (dialect === "mysql") {
565
+ await orm.em.getConnection().execute(`select get_lock('${SCHEMA_LOCK_NAME}', 10)`);
566
+ } else if (dialect === "postgres") {
567
+ await orm.em.getConnection().execute(`select pg_advisory_lock(hashtext('${SCHEMA_LOCK_NAME}'))`);
568
+ }
569
+ } catch (err) {
570
+ const message = err instanceof Error ? err.message : String(err);
571
+ console.warn(`[nestjs-durable-store-mikro-orm] could not acquire the durable-schema advisory lock (proceeding without it): ${message}`);
572
+ }
573
+ }
574
+ __name(acquireSchemaLock, "acquireSchemaLock");
575
+ async function releaseSchemaLock(orm, dialect) {
576
+ try {
577
+ if (dialect === "mysql") {
578
+ await orm.em.getConnection().execute(`select release_lock('${SCHEMA_LOCK_NAME}')`);
579
+ } else if (dialect === "postgres") {
580
+ await orm.em.getConnection().execute(`select pg_advisory_unlock(hashtext('${SCHEMA_LOCK_NAME}'))`);
581
+ }
582
+ } catch {
583
+ }
584
+ }
585
+ __name(releaseSchemaLock, "releaseSchemaLock");
455
586
  async function ensureMikroOrmDurableSchema(orm) {
587
+ const dialect = detectSqlDialect(orm);
588
+ await orm.em.getConnection().execute(createMarkerTableSql());
589
+ const expected = computeExpectedFingerprint(orm, DURABLE_TABLE_NAMES);
590
+ const stored = await readStoredFingerprint(orm);
591
+ if (stored === expected) {
592
+ return;
593
+ }
594
+ await acquireSchemaLock(orm, dialect);
595
+ try {
596
+ if (await readStoredFingerprint(orm) === expected) {
597
+ return;
598
+ }
599
+ await healDurableSchema(orm);
600
+ await writeFingerprint(orm, dialect, expected);
601
+ } finally {
602
+ await releaseSchemaLock(orm, dialect);
603
+ }
604
+ }
605
+ __name(ensureMikroOrmDurableSchema, "ensureMikroOrmDurableSchema");
606
+ async function healDurableSchema(orm) {
456
607
  const sql = await orm.schema.getUpdateSchemaSQL({
457
608
  safe: true
458
609
  });
@@ -474,7 +625,7 @@ async function ensureMikroOrmDurableSchema(orm) {
474
625
  }
475
626
  await alignDurableCollation(orm);
476
627
  }
477
- __name(ensureMikroOrmDurableSchema, "ensureMikroOrmDurableSchema");
628
+ __name(healDurableSchema, "healDurableSchema");
478
629
  function isSafeSqlIdentifier(value) {
479
630
  return /^[A-Za-z0-9_]+$/.test(value);
480
631
  }
@@ -512,25 +663,51 @@ async function alignDurableCollation(orm) {
512
663
  __name(alignDurableCollation, "alignDurableCollation");
513
664
 
514
665
  // src/mikro-orm-state-store.ts
515
- var MikroOrmStateStore = class {
666
+ var MikroOrmStateStore = class _MikroOrmStateStore {
516
667
  static {
517
668
  __name(this, "MikroOrmStateStore");
518
669
  }
519
670
  orm;
520
- constructor(orm) {
671
+ scopeNamespace;
672
+ constructor(orm, opts) {
521
673
  this.orm = orm;
674
+ this.scopeNamespace = opts?.scope?.namespace;
675
+ }
676
+ /**
677
+ * Derive a store confined to a single tenant `scope.namespace`, sharing this store's ORM (no new
678
+ * connection). Turns the operator (unscoped) store into a tenant-boundary view at wiring time —
679
+ * used by the NestJS module's `scopeReads` option, which receives a pre-built store and can't
680
+ * reconstruct it. Pass `{ namespace: undefined }` for the unscoped operator view.
681
+ */
682
+ withScope(scope) {
683
+ return new _MikroOrmStateStore(this.orm, {
684
+ scope
685
+ });
686
+ }
687
+ /**
688
+ * Create a forked EntityManager and activate the `namespace` global filter with the store's
689
+ * scope (if any). All read operations go through this so the tenant boundary is applied
690
+ * uniformly. Write operations (nativeDelete, nativeUpdate, upsert, insertMany) bypass global
691
+ * filters in MikroORM and are unaffected.
692
+ */
693
+ fork() {
694
+ const em = this.orm.em.fork();
695
+ em.setFilterParams("namespace", {
696
+ namespace: this.scopeNamespace
697
+ });
698
+ return em;
522
699
  }
523
700
  async ensureSchema() {
524
701
  await ensureMikroOrmDurableSchema(this.orm);
525
702
  }
526
703
  async createRun(run) {
527
- const em = this.orm.em.fork();
704
+ const em = this.fork();
528
705
  em.create(WorkflowRunEntity, toRunEntity(run));
529
706
  await em.flush();
530
707
  await this.reindexAttributes(run.id, run.searchAttributes);
531
708
  }
532
709
  async updateRun(runId, patch) {
533
- const em = this.orm.em.fork();
710
+ const em = this.fork();
534
711
  const entity = await em.findOneOrFail(WorkflowRunEntity, {
535
712
  id: runId
536
713
  });
@@ -544,7 +721,7 @@ var MikroOrmStateStore = class {
544
721
  /** Rewrite a run's normalized attribute rows: delete the old set, insert the current one. Mirrors
545
722
  * the in-memory store's reindex so the side-table always reflects the run's live searchAttributes. */
546
723
  async reindexAttributes(runId, attributes, forked) {
547
- const em = forked ?? this.orm.em.fork();
724
+ const em = forked ?? this.fork();
548
725
  await em.nativeDelete(RunAttributeEntity, {
549
726
  runId
550
727
  });
@@ -552,14 +729,14 @@ var MikroOrmStateStore = class {
552
729
  if (rows.length) await em.insertMany(RunAttributeEntity, rows);
553
730
  }
554
731
  async getRun(runId) {
555
- const em = this.orm.em.fork();
732
+ const em = this.fork();
556
733
  const entity = await em.findOne(WorkflowRunEntity, {
557
734
  id: runId
558
735
  });
559
736
  return entity ? fromRunEntity(entity) : null;
560
737
  }
561
738
  async deleteRun(runId) {
562
- const em = this.orm.em.fork();
739
+ const em = this.fork();
563
740
  await em.nativeDelete(StepCheckpointEntity, {
564
741
  runId
565
742
  });
@@ -575,7 +752,7 @@ var MikroOrmStateStore = class {
575
752
  }
576
753
  async pruneTerminalRuns(policy, nowMs, limit) {
577
754
  if (policy.statuses.length === 0 || limit <= 0) return 0;
578
- const em = this.orm.em.fork();
755
+ const em = this.fork();
579
756
  const status = {
580
757
  $in: policy.statuses
581
758
  };
@@ -641,7 +818,7 @@ var MikroOrmStateStore = class {
641
818
  return idList.length;
642
819
  }
643
820
  async getCheckpoint(runId, seq) {
644
- const em = this.orm.em.fork();
821
+ const em = this.fork();
645
822
  const entity = await em.findOne(StepCheckpointEntity, {
646
823
  runId,
647
824
  seq
@@ -649,12 +826,12 @@ var MikroOrmStateStore = class {
649
826
  return entity ? fromCheckpointEntity(entity) : null;
650
827
  }
651
828
  async saveCheckpoint(checkpoint) {
652
- const em = this.orm.em.fork();
829
+ const em = this.fork();
653
830
  await em.upsert(StepCheckpointEntity, toCheckpointEntity(checkpoint));
654
831
  await em.flush();
655
832
  }
656
833
  async transaction(work) {
657
- return this.orm.em.fork().transactional(async (em) => work({
834
+ return this.fork().transactional(async (em) => work({
658
835
  raw: em,
659
836
  saveCheckpoint: /* @__PURE__ */ __name(async (cp) => {
660
837
  await em.upsert(StepCheckpointEntity, toCheckpointEntity(cp));
@@ -662,7 +839,7 @@ var MikroOrmStateStore = class {
662
839
  }));
663
840
  }
664
841
  async listIncompleteRuns(namespace) {
665
- const em = this.orm.em.fork();
842
+ const em = this.fork();
666
843
  const rows = await em.find(WorkflowRunEntity, {
667
844
  status: {
668
845
  $in: [
@@ -677,7 +854,7 @@ var MikroOrmStateStore = class {
677
854
  return rows.map(fromRunEntity);
678
855
  }
679
856
  async listPendingRuns(limit, namespace) {
680
- const em = this.orm.em.fork();
857
+ const em = this.fork();
681
858
  const rows = await em.find(WorkflowRunEntity, {
682
859
  status: "pending",
683
860
  ...namespace !== void 0 ? {
@@ -692,7 +869,7 @@ var MikroOrmStateStore = class {
692
869
  return rows.map(fromRunEntity);
693
870
  }
694
871
  async listDueTimers(nowMs, namespace) {
695
- const em = this.orm.em.fork();
872
+ const em = this.fork();
696
873
  const rows = await em.find(WorkflowRunEntity, {
697
874
  status: "suspended",
698
875
  wakeAt: {
@@ -706,7 +883,7 @@ var MikroOrmStateStore = class {
706
883
  return rows.map(fromRunEntity);
707
884
  }
708
885
  async tryLockRun(runId, owner, leaseUntilMs, nowMs) {
709
- const em = this.orm.em.fork();
886
+ const em = this.fork();
710
887
  const affected = await em.nativeUpdate(WorkflowRunEntity, {
711
888
  id: runId,
712
889
  $or: [
@@ -726,7 +903,7 @@ var MikroOrmStateStore = class {
726
903
  return affected === 1;
727
904
  }
728
905
  async releaseRunLock(runId) {
729
- const em = this.orm.em.fork();
906
+ const em = this.fork();
730
907
  await em.nativeUpdate(WorkflowRunEntity, {
731
908
  id: runId
732
909
  }, {
@@ -735,7 +912,7 @@ var MikroOrmStateStore = class {
735
912
  });
736
913
  }
737
914
  async renewRunLock(runId, owner, leaseUntilMs) {
738
- const em = this.orm.em.fork();
915
+ const em = this.fork();
739
916
  const affected = await em.nativeUpdate(WorkflowRunEntity, {
740
917
  id: runId,
741
918
  lockedBy: owner
@@ -745,7 +922,7 @@ var MikroOrmStateStore = class {
745
922
  return affected === 1;
746
923
  }
747
924
  async listRuns(query) {
748
- const em = this.orm.em.fork();
925
+ const em = this.fork();
749
926
  const where = {};
750
927
  if (query.workflow) where.workflow = query.workflow;
751
928
  if (query.namespace !== void 0) where.namespace = query.namespace;
@@ -771,6 +948,7 @@ var MikroOrmStateStore = class {
771
948
  createdAt: "desc"
772
949
  };
773
950
  if (query.tag || query.attributes?.length) {
951
+ if (this.scopeNamespace !== void 0) where.namespace = this.scopeNamespace;
774
952
  const quote = this.idQuote(em);
775
953
  const cols = this.attributeColumns(em);
776
954
  const qb = em.createQueryBuilder(WorkflowRunEntity, "r").where(where).orderBy(orderBy);
@@ -853,7 +1031,7 @@ var MikroOrmStateStore = class {
853
1031
  return (id) => `${ch}${id}${ch}`;
854
1032
  }
855
1033
  async listCheckpoints(runId) {
856
- const em = this.orm.em.fork();
1034
+ const em = this.fork();
857
1035
  const rows = await em.find(StepCheckpointEntity, {
858
1036
  runId
859
1037
  }, {
@@ -864,7 +1042,7 @@ var MikroOrmStateStore = class {
864
1042
  return rows.map(fromCheckpointEntity);
865
1043
  }
866
1044
  async getLatestCheckpointByName(runId, name) {
867
- const em = this.orm.em.fork();
1045
+ const em = this.fork();
868
1046
  const entity = await em.findOne(StepCheckpointEntity, {
869
1047
  runId,
870
1048
  name
@@ -877,7 +1055,7 @@ var MikroOrmStateStore = class {
877
1055
  }
878
1056
  async listCheckpointsByNamePrefix(runId, prefixes) {
879
1057
  if (prefixes.length === 0) return [];
880
- const em = this.orm.em.fork();
1058
+ const em = this.fork();
881
1059
  const rows = await em.find(StepCheckpointEntity, {
882
1060
  runId,
883
1061
  $or: prefixes.map((p) => ({
@@ -893,7 +1071,7 @@ var MikroOrmStateStore = class {
893
1071
  return rows.map(fromCheckpointEntity);
894
1072
  }
895
1073
  async putSignalWaiter(waiter) {
896
- const em = this.orm.em.fork();
1074
+ const em = this.fork();
897
1075
  await em.upsert(SignalWaiterEntity, {
898
1076
  token: waiter.token,
899
1077
  runId: waiter.runId,
@@ -903,7 +1081,7 @@ var MikroOrmStateStore = class {
903
1081
  await em.flush();
904
1082
  }
905
1083
  async takeSignalWaiter(token) {
906
- const em = this.orm.em.fork();
1084
+ const em = this.fork();
907
1085
  const entity = await em.findOne(SignalWaiterEntity, {
908
1086
  token
909
1087
  });
@@ -918,7 +1096,7 @@ var MikroOrmStateStore = class {
918
1096
  return waiter;
919
1097
  }
920
1098
  async listSignalWaiters(prefix) {
921
- const em = this.orm.em.fork();
1099
+ const em = this.fork();
922
1100
  const rows = await em.find(SignalWaiterEntity, {
923
1101
  token: {
924
1102
  $like: `${prefix}%`
@@ -932,7 +1110,7 @@ var MikroOrmStateStore = class {
932
1110
  }));
933
1111
  }
934
1112
  async bufferSignal(token, payload) {
935
- const em = this.orm.em.fork();
1113
+ const em = this.fork();
936
1114
  const e = new BufferedSignalEntity();
937
1115
  e.token = token;
938
1116
  e.payload = payload ?? null;
@@ -940,7 +1118,7 @@ var MikroOrmStateStore = class {
940
1118
  await em.flush();
941
1119
  }
942
1120
  async takeBufferedSignal(token) {
943
- const em = this.orm.em.fork();
1121
+ const em = this.fork();
944
1122
  const entity = await em.findOne(BufferedSignalEntity, {
945
1123
  token
946
1124
  }, {