@dudousxd/nestjs-durable-store-mikro-orm 0.20.0 → 0.22.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.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _mikro_orm_core from '@mikro-orm/core';
2
2
  import { EntitySchema, MikroORM } from '@mikro-orm/core';
3
- import { StepKind, RunStatus, StateStore, WorkflowRun, RetentionPolicy, StepCheckpoint, RunQuery, SignalWaiter } from '@dudousxd/nestjs-durable-core';
3
+ import { StepKind, RunStatus, StateStore, WorkflowRun, RetentionPolicy, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, SignalWaiter } from '@dudousxd/nestjs-durable-core';
4
4
 
5
5
  /**
6
6
  * How property names map to physical column names. The durable tables are adapter-agnostic: a run
@@ -32,6 +32,7 @@ declare class WorkflowRunEntity {
32
32
  searchAttributes?: Record<string, string | number | boolean> | null;
33
33
  priority?: number | null;
34
34
  namespace: string;
35
+ origin?: string | null;
35
36
  createdAt: Date;
36
37
  updatedAt: Date;
37
38
  }
@@ -152,7 +153,19 @@ declare class MikroOrmStateStore implements StateStore {
152
153
  tryLockRun(runId: string, owner: string, leaseUntilMs: number, nowMs: number): Promise<boolean>;
153
154
  releaseRunLock(runId: string): Promise<void>;
154
155
  renewRunLock(runId: string, owner: string, leaseUntilMs: number): Promise<boolean>;
156
+ /** The `where` every run query shares — {@link listRuns} pages it, {@link runFacets} groups it.
157
+ * Only the predicates MikroORM can express declaratively; `tag`/`attributes` need raw SQL and are
158
+ * applied by the QueryBuilder path in both callers. */
159
+ private runWhere;
155
160
  listRuns(query: RunQuery): Promise<WorkflowRun[]>;
161
+ /** A QueryBuilder over `durable_workflow_runs` (root alias `r`) carrying `where` plus the two
162
+ * predicates only raw SQL can express — the `tag` JSON LIKE and one EXISTS per attribute filter.
163
+ * Shared so a facet count and the page it labels are taken over the SAME set. */
164
+ private runQueryBuilder;
165
+ /** `GROUP BY status, origin` over the same predicates {@link listRuns} pages — one aggregate, so a
166
+ * console can show whole-set counts next to a bounded page instead of downloading every run to
167
+ * count them in the browser. */
168
+ runFacets(query: RunFacetQuery): Promise<RunFacetRow[]>;
156
169
  /** One attribute predicate as a raw EXISTS subquery on the side-table, correlated to the outer run
157
170
  * alias. `<>` (ne) also excludes runs where the attribute is absent (missing-key-never-matches):
158
171
  * EXISTS already requires a present row, so EXISTS(... <> ...) is exactly ne-with-present. Numeric
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _mikro_orm_core from '@mikro-orm/core';
2
2
  import { EntitySchema, MikroORM } from '@mikro-orm/core';
3
- import { StepKind, RunStatus, StateStore, WorkflowRun, RetentionPolicy, StepCheckpoint, RunQuery, SignalWaiter } from '@dudousxd/nestjs-durable-core';
3
+ import { StepKind, RunStatus, StateStore, WorkflowRun, RetentionPolicy, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, SignalWaiter } from '@dudousxd/nestjs-durable-core';
4
4
 
5
5
  /**
6
6
  * How property names map to physical column names. The durable tables are adapter-agnostic: a run
@@ -32,6 +32,7 @@ declare class WorkflowRunEntity {
32
32
  searchAttributes?: Record<string, string | number | boolean> | null;
33
33
  priority?: number | null;
34
34
  namespace: string;
35
+ origin?: string | null;
35
36
  createdAt: Date;
36
37
  updatedAt: Date;
37
38
  }
@@ -152,7 +153,19 @@ declare class MikroOrmStateStore implements StateStore {
152
153
  tryLockRun(runId: string, owner: string, leaseUntilMs: number, nowMs: number): Promise<boolean>;
153
154
  releaseRunLock(runId: string): Promise<void>;
154
155
  renewRunLock(runId: string, owner: string, leaseUntilMs: number): Promise<boolean>;
156
+ /** The `where` every run query shares — {@link listRuns} pages it, {@link runFacets} groups it.
157
+ * Only the predicates MikroORM can express declaratively; `tag`/`attributes` need raw SQL and are
158
+ * applied by the QueryBuilder path in both callers. */
159
+ private runWhere;
155
160
  listRuns(query: RunQuery): Promise<WorkflowRun[]>;
161
+ /** A QueryBuilder over `durable_workflow_runs` (root alias `r`) carrying `where` plus the two
162
+ * predicates only raw SQL can express — the `tag` JSON LIKE and one EXISTS per attribute filter.
163
+ * Shared so a facet count and the page it labels are taken over the SAME set. */
164
+ private runQueryBuilder;
165
+ /** `GROUP BY status, origin` over the same predicates {@link listRuns} pages — one aggregate, so a
166
+ * console can show whole-set counts next to a bounded page instead of downloading every run to
167
+ * count them in the browser. */
168
+ runFacets(query: RunFacetQuery): Promise<RunFacetRow[]>;
156
169
  /** One attribute predicate as a raw EXISTS subquery on the side-table, correlated to the outer run
157
170
  * alias. `<>` (ne) also excludes runs where the attribute is absent (missing-key-never-matches):
158
171
  * EXISTS already requires a present row, so EXISTS(... <> ...) is exactly ne-with-present. Numeric
package/dist/index.js CHANGED
@@ -33,6 +33,7 @@ var WorkflowRunEntity = class {
33
33
  searchAttributes;
34
34
  priority;
35
35
  namespace;
36
+ origin;
36
37
  createdAt;
37
38
  updatedAt;
38
39
  };
@@ -230,6 +231,31 @@ function durableEntities(options = {}) {
230
231
  default: "default",
231
232
  fieldName: col("namespace")
232
233
  },
234
+ // Which library/module registered the workflow this run belongs to (e.g.
235
+ // `@dudousxd/nestjs-catalog-pipeline`), derived at registration time — never caller input.
236
+ //
237
+ // NULLABLE WITH NO DEFAULT, deliberately unlike `namespace` above. `namespace` can honestly
238
+ // default a pre-existing row to `'default'` (that IS the namespace an unscoped engine ran it
239
+ // in); an origin cannot be reconstructed after the fact, so a row written before this column
240
+ // existed must read back as `undefined` = "unknown". Stamping it `'app'`/`'unknown'` would be
241
+ // indistinguishable from a real registration in the dashboard's facet — a wrong origin is worse
242
+ // than an absent one. Two MikroORM specifics reinforce it: a `default` here would NOT be
243
+ // back-filled onto existing rows anyway (the DDL default only applies to new inserts, and a
244
+ // TypeScript field initialiser never runs for rows the ORM did not create), so a default would
245
+ // buy nothing but a lie for future rows.
246
+ //
247
+ // No index, unlike `namespace`. That one is on the HOT path — every poll tick filters
248
+ // `listPendingRuns`/`listDueTimers`/`listIncompleteRuns` by it, hence
249
+ // `durable_workflow_runs_namespace_status_idx`. Nothing filters by `origin` except the
250
+ // dashboard's `listRuns`, where it is ANDed with the `status`/`workflow` predicates that
251
+ // `durable_workflow_runs_status_wake_at_idx` / `..._workflow_status_idx` already bound. If a
252
+ // deployment ever makes origin-filtered listings hot, add the composite index in your own
253
+ // migration rather than here.
254
+ origin: {
255
+ type: "string",
256
+ nullable: true,
257
+ fieldName: col("origin")
258
+ },
233
259
  createdAt: {
234
260
  type: "Date",
235
261
  fieldName: col("createdAt")
@@ -472,7 +498,8 @@ __name(durableEntities, "durableEntities");
472
498
  var ENTITIES = durableEntities();
473
499
 
474
500
  // src/mikro-orm-state-store.ts
475
- import { attributeColumnFor, attributeOperand, normalizeAttributeRows, parseDuration, sqlComparator } from "@dudousxd/nestjs-durable-core";
501
+ import { attributeColumnFor, attributeOperand, mergeRunFacetRows, normalizeAttributeRows, parseDuration, sqlComparator } from "@dudousxd/nestjs-durable-core";
502
+ import { raw } from "@mikro-orm/core";
476
503
 
477
504
  // src/schema.ts
478
505
  import { createHash } from "node:crypto";
@@ -950,11 +977,14 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
950
977
  });
951
978
  return affected === 1;
952
979
  }
953
- async listRuns(query) {
954
- const em = this.fork();
980
+ /** The `where` every run query shares — {@link listRuns} pages it, {@link runFacets} groups it.
981
+ * Only the predicates MikroORM can express declaratively; `tag`/`attributes` need raw SQL and are
982
+ * applied by the QueryBuilder path in both callers. */
983
+ runWhere(query) {
955
984
  const where = {};
956
985
  if (query.workflow) where.workflow = query.workflow;
957
986
  if (query.namespace !== void 0) where.namespace = query.namespace;
987
+ if (query.origin !== void 0) where.origin = query.origin;
958
988
  if (query.status && query.statuses) {
959
989
  where.$and = [
960
990
  {
@@ -973,23 +1003,16 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
973
1003
  $in: query.statuses
974
1004
  };
975
1005
  }
1006
+ return where;
1007
+ }
1008
+ async listRuns(query) {
1009
+ const em = this.fork();
1010
+ const where = this.runWhere(query);
976
1011
  const orderBy = {
977
1012
  createdAt: "desc"
978
1013
  };
979
1014
  if (query.tag || query.attributes?.length) {
980
- if (this.scopeNamespace !== void 0) where.namespace = this.scopeNamespace;
981
- const quote = this.idQuote(em);
982
- const cols = this.attributeColumns(em);
983
- const qb = em.createQueryBuilder(WorkflowRunEntity, "r").where(where).orderBy(orderBy);
984
- if (query.tag) {
985
- const tagsCol = this.tagsColumn(em);
986
- const pattern = `%"${query.tag.replace(/'/g, "''")}"%`;
987
- const colExpr = this.jsonAsText(em, `${quote("r")}.${quote(tagsCol)}`);
988
- qb.andWhere(`${colExpr} LIKE '${pattern}'`);
989
- }
990
- for (const f of query.attributes ?? []) {
991
- qb.andWhere(this.attributeExistsSql(f, quote, "r", cols));
992
- }
1015
+ const qb = this.runQueryBuilder(em, query, where).orderBy(orderBy);
993
1016
  if (query.limit != null) qb.limit(query.limit);
994
1017
  if (query.offset != null) qb.offset(query.offset);
995
1018
  const rows2 = await qb.getResultList();
@@ -1006,6 +1029,45 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1006
1029
  });
1007
1030
  return rows.map(fromRunEntity);
1008
1031
  }
1032
+ /** A QueryBuilder over `durable_workflow_runs` (root alias `r`) carrying `where` plus the two
1033
+ * predicates only raw SQL can express — the `tag` JSON LIKE and one EXISTS per attribute filter.
1034
+ * Shared so a facet count and the page it labels are taken over the SAME set. */
1035
+ runQueryBuilder(em, query, where) {
1036
+ if (this.scopeNamespace !== void 0) where.namespace = this.scopeNamespace;
1037
+ const quote = this.idQuote(em);
1038
+ const cols = this.attributeColumns(em);
1039
+ const qb = em.createQueryBuilder(WorkflowRunEntity, "r").where(where);
1040
+ if (query.tag) {
1041
+ const tagsCol = this.tagsColumn(em);
1042
+ const pattern = `%"${query.tag.replace(/'/g, "''")}"%`;
1043
+ const colExpr = this.jsonAsText(em, `${quote("r")}.${quote(tagsCol)}`);
1044
+ qb.andWhere(`${colExpr} LIKE '${pattern}'`);
1045
+ }
1046
+ for (const f of query.attributes ?? []) {
1047
+ qb.andWhere(this.attributeExistsSql(f, quote, "r", cols));
1048
+ }
1049
+ return qb;
1050
+ }
1051
+ /** `GROUP BY status, origin` over the same predicates {@link listRuns} pages — one aggregate, so a
1052
+ * console can show whole-set counts next to a bounded page instead of downloading every run to
1053
+ * count them in the browser. */
1054
+ async runFacets(query) {
1055
+ const em = this.fork();
1056
+ const meta = em.getMetadata().get(WorkflowRunEntity);
1057
+ const statusCol = meta.properties.status?.fieldNames?.[0] ?? "status";
1058
+ const originCol = meta.properties.origin?.fieldNames?.[0] ?? "origin";
1059
+ const rows = await this.runQueryBuilder(em, query, this.runWhere(query)).select([
1060
+ `r.${statusCol}`,
1061
+ `r.${originCol}`
1062
+ ]).addSelect(raw("count(*) as count")).groupBy([
1063
+ `r.${statusCol}`,
1064
+ `r.${originCol}`
1065
+ ]).execute();
1066
+ return mergeRunFacetRows(rows.map((r) => ({
1067
+ ...r,
1068
+ count: Number(r.count)
1069
+ })));
1070
+ }
1009
1071
  /** One attribute predicate as a raw EXISTS subquery on the side-table, correlated to the outer run
1010
1072
  * alias. `<>` (ne) also excludes runs where the attribute is absent (missing-key-never-matches):
1011
1073
  * EXISTS already requires a present row, so EXISTS(... <> ...) is exactly ne-with-present. Numeric
@@ -1222,6 +1284,7 @@ function toRunEntity(run) {
1222
1284
  e.searchAttributes = run.searchAttributes ?? null;
1223
1285
  e.priority = run.priority ?? null;
1224
1286
  e.namespace = run.namespace ?? "default";
1287
+ e.origin = run.origin ?? null;
1225
1288
  e.createdAt = run.createdAt;
1226
1289
  e.updatedAt = run.updatedAt;
1227
1290
  return e;
@@ -1245,6 +1308,8 @@ function fromRunEntity(e) {
1245
1308
  searchAttributes: e.searchAttributes ?? void 0,
1246
1309
  priority: e.priority ?? void 0,
1247
1310
  namespace: e.namespace,
1311
+ // NULL (a row written before the column existed) surfaces as `undefined` = unknown origin.
1312
+ origin: e.origin ?? void 0,
1248
1313
  createdAt: e.createdAt,
1249
1314
  updatedAt: e.updatedAt
1250
1315
  };