@dudousxd/nestjs-durable-store-mikro-orm 0.22.0 → 0.24.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,34 @@
1
1
  import * as _mikro_orm_core from '@mikro-orm/core';
2
- import { EntitySchema, MikroORM } from '@mikro-orm/core';
3
- import { StepKind, RunStatus, StateStore, WorkflowRun, RetentionPolicy, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, SignalWaiter } from '@dudousxd/nestjs-durable-core';
2
+ import { EntityRepository, EntityRepositoryType, EntitySchema, MikroORM } from '@mikro-orm/core';
3
+ import { StepKind, RunStatus, StateStore, WorkflowRun, RetentionPolicy, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, SignalWaiter } from '@dudousxd/nestjs-durable-core';
4
+
5
+ /**
6
+ * Custom repositories for the durable entities, so a host app can `@InjectRepository`/inject a
7
+ * `WorkflowRunRepository` by type instead of repeating `em.find(WorkflowRunEntity, …)` with the
8
+ * entity class as the first argument everywhere.
9
+ *
10
+ * They are wired per-schema inside `durableEntities()` (not with a decorator), because that factory
11
+ * can be called more than once with different column naming and each call must produce schemas that
12
+ * still point at these classes. Each entity also declares `[EntityRepositoryType]`, which is what
13
+ * makes `em.getRepository(WorkflowRunEntity)` resolve to the specific type rather than the base
14
+ * `EntityRepository`.
15
+ *
16
+ * The bodies are intentionally empty: the durable engine's own queries go through
17
+ * `MikroOrmStateStore`, and a durable-owned query added here would be a second, unversioned read
18
+ * path over the same tables. They exist as an injectable, correctly-typed handle for host code.
19
+ */
20
+ declare class WorkflowRunRepository extends EntityRepository<WorkflowRunEntity> {
21
+ }
22
+ declare class StepCheckpointRepository extends EntityRepository<StepCheckpointEntity> {
23
+ }
24
+ declare class RunAttributeRepository extends EntityRepository<RunAttributeEntity> {
25
+ }
26
+ declare class SignalWaiterRepository extends EntityRepository<SignalWaiterEntity> {
27
+ }
28
+ declare class BufferedSignalRepository extends EntityRepository<BufferedSignalEntity> {
29
+ }
30
+ declare class BufferedEventRepository extends EntityRepository<BufferedEventEntity> {
31
+ }
4
32
 
5
33
  /**
6
34
  * How property names map to physical column names. The durable tables are adapter-agnostic: a run
@@ -16,6 +44,7 @@ import { StepKind, RunStatus, StateStore, WorkflowRun, RetentionPolicy, StepChec
16
44
  */
17
45
  type DurableColumnNaming = 'snake_case' | 'preserve' | ((property: string) => string);
18
46
  declare class WorkflowRunEntity {
47
+ [EntityRepositoryType]?: WorkflowRunRepository;
19
48
  id: string;
20
49
  workflow: string;
21
50
  workflowVersion: string;
@@ -37,6 +66,7 @@ declare class WorkflowRunEntity {
37
66
  updatedAt: Date;
38
67
  }
39
68
  declare class StepCheckpointEntity {
69
+ [EntityRepositoryType]?: StepCheckpointRepository;
40
70
  runId: string;
41
71
  seq: number;
42
72
  name: string;
@@ -62,18 +92,21 @@ declare class StepCheckpointEntity {
62
92
  * `numValue`, strings/booleans in `strValue` (booleans as "true"/"false"); see normalizeAttributeRows.
63
93
  */
64
94
  declare class RunAttributeEntity {
95
+ [EntityRepositoryType]?: RunAttributeRepository;
65
96
  runId: string;
66
97
  key: string;
67
98
  strValue?: string | null;
68
99
  numValue?: number | null;
69
100
  }
70
101
  declare class SignalWaiterEntity {
102
+ [EntityRepositoryType]?: SignalWaiterRepository;
71
103
  token: string;
72
104
  runId: string;
73
105
  seq: number;
74
106
  parallelGroup?: string | null;
75
107
  }
76
108
  declare class BufferedSignalEntity {
109
+ [EntityRepositoryType]?: BufferedSignalRepository;
77
110
  id: number;
78
111
  token: string;
79
112
  payload?: unknown;
@@ -82,6 +115,7 @@ declare class BufferedSignalEntity {
82
115
  * caller-minted `id` (a uuid, not autoincrement — `removeBufferedEvent` targets it directly), not by
83
116
  * `runId`: events are name-based pub/sub, not tied to any one run. */
84
117
  declare class BufferedEventEntity {
118
+ [EntityRepositoryType]?: BufferedEventRepository;
85
119
  id: string;
86
120
  name: string;
87
121
  payload?: unknown;
@@ -162,15 +196,25 @@ declare class MikroOrmStateStore implements StateStore {
162
196
  * predicates only raw SQL can express — the `tag` JSON LIKE and one EXISTS per attribute filter.
163
197
  * Shared so a facet count and the page it labels are taken over the SAME set. */
164
198
  private runQueryBuilder;
199
+ /** `tags LIKE '%"etl"%'` — the quoted token, so `etl` doesn't match `etl-foo`. See the note in
200
+ * {@link listRuns} for why this is raw SQL rather than a MikroORM condition. */
201
+ private tagLikeSql;
165
202
  /** `GROUP BY status, origin` over the same predicates {@link listRuns} pages — one aggregate, so a
166
203
  * console can show whole-set counts next to a bounded page instead of downloading every run to
167
204
  * count them in the browser. */
168
205
  runFacets(query: RunFacetQuery): Promise<RunFacetRow[]>;
206
+ /** Distinct values of one filter axis over the same predicates {@link listRuns} pages. A run-table
207
+ * column is a `GROUP BY` like {@link runFacets}, exact over the whole matching set; the tag and
208
+ * attribute axes live outside the row (json array / side table) and are counted from a bounded
209
+ * page of runs instead — see {@link RunValueFacetOptions.scan}. */
210
+ runValueFacets(axis: RunValueAxis, query: RunFacetQuery, opts?: RunValueFacetOptions): Promise<RunValueFacetRow[]>;
169
211
  /** One attribute predicate as a raw EXISTS subquery on the side-table, correlated to the outer run
170
212
  * alias. `<>` (ne) also excludes runs where the attribute is absent (missing-key-never-matches):
171
213
  * EXISTS already requires a present row, so EXISTS(... <> ...) is exactly ne-with-present. Numeric
172
- * operands compare the num column, everything else the str column. Identifiers are quoted per
173
- * driver and operands are inlined as literals (string operands are escaped by doubling quotes). */
214
+ * operands compare the num column, everything else the str column. `in` contributes one comparison
215
+ * per member, ORed inside the single EXISTS — one subquery either way, and an `in` over no values
216
+ * yields no comparison and so matches nothing. Identifiers are quoted per driver and operands are
217
+ * inlined as literals (string operands are escaped by doubling quotes). */
174
218
  private attributeExistsSql;
175
219
  /** Resolve the side-table's actual column names from MikroORM metadata, so the raw EXISTS matches
176
220
  * the active naming strategy (underscore vs camelCase) rather than guessing. The run PK column is
@@ -254,4 +298,4 @@ declare function durableManagedTables(): string[];
254
298
  */
255
299
  declare function ensureMikroOrmDurableSchema(orm: MikroORM): Promise<void>;
256
300
 
257
- export { BufferedEventEntity, BufferedSignalEntity, type DurableColumnNaming, ENTITIES, MikroOrmStateStore, type MikroOrmStateStoreOptions, RunAttributeEntity, SignalWaiterEntity, StepCheckpointEntity, WorkflowRunEntity, durableEntities, durableManagedTables, ensureMikroOrmDurableSchema };
301
+ export { BufferedEventEntity, BufferedEventRepository, BufferedSignalEntity, BufferedSignalRepository, type DurableColumnNaming, ENTITIES, MikroOrmStateStore, type MikroOrmStateStoreOptions, RunAttributeEntity, RunAttributeRepository, SignalWaiterEntity, SignalWaiterRepository, StepCheckpointEntity, StepCheckpointRepository, WorkflowRunEntity, WorkflowRunRepository, durableEntities, durableManagedTables, ensureMikroOrmDurableSchema };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,34 @@
1
1
  import * as _mikro_orm_core from '@mikro-orm/core';
2
- import { EntitySchema, MikroORM } from '@mikro-orm/core';
3
- import { StepKind, RunStatus, StateStore, WorkflowRun, RetentionPolicy, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, SignalWaiter } from '@dudousxd/nestjs-durable-core';
2
+ import { EntityRepository, EntityRepositoryType, EntitySchema, MikroORM } from '@mikro-orm/core';
3
+ import { StepKind, RunStatus, StateStore, WorkflowRun, RetentionPolicy, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, SignalWaiter } from '@dudousxd/nestjs-durable-core';
4
+
5
+ /**
6
+ * Custom repositories for the durable entities, so a host app can `@InjectRepository`/inject a
7
+ * `WorkflowRunRepository` by type instead of repeating `em.find(WorkflowRunEntity, …)` with the
8
+ * entity class as the first argument everywhere.
9
+ *
10
+ * They are wired per-schema inside `durableEntities()` (not with a decorator), because that factory
11
+ * can be called more than once with different column naming and each call must produce schemas that
12
+ * still point at these classes. Each entity also declares `[EntityRepositoryType]`, which is what
13
+ * makes `em.getRepository(WorkflowRunEntity)` resolve to the specific type rather than the base
14
+ * `EntityRepository`.
15
+ *
16
+ * The bodies are intentionally empty: the durable engine's own queries go through
17
+ * `MikroOrmStateStore`, and a durable-owned query added here would be a second, unversioned read
18
+ * path over the same tables. They exist as an injectable, correctly-typed handle for host code.
19
+ */
20
+ declare class WorkflowRunRepository extends EntityRepository<WorkflowRunEntity> {
21
+ }
22
+ declare class StepCheckpointRepository extends EntityRepository<StepCheckpointEntity> {
23
+ }
24
+ declare class RunAttributeRepository extends EntityRepository<RunAttributeEntity> {
25
+ }
26
+ declare class SignalWaiterRepository extends EntityRepository<SignalWaiterEntity> {
27
+ }
28
+ declare class BufferedSignalRepository extends EntityRepository<BufferedSignalEntity> {
29
+ }
30
+ declare class BufferedEventRepository extends EntityRepository<BufferedEventEntity> {
31
+ }
4
32
 
5
33
  /**
6
34
  * How property names map to physical column names. The durable tables are adapter-agnostic: a run
@@ -16,6 +44,7 @@ import { StepKind, RunStatus, StateStore, WorkflowRun, RetentionPolicy, StepChec
16
44
  */
17
45
  type DurableColumnNaming = 'snake_case' | 'preserve' | ((property: string) => string);
18
46
  declare class WorkflowRunEntity {
47
+ [EntityRepositoryType]?: WorkflowRunRepository;
19
48
  id: string;
20
49
  workflow: string;
21
50
  workflowVersion: string;
@@ -37,6 +66,7 @@ declare class WorkflowRunEntity {
37
66
  updatedAt: Date;
38
67
  }
39
68
  declare class StepCheckpointEntity {
69
+ [EntityRepositoryType]?: StepCheckpointRepository;
40
70
  runId: string;
41
71
  seq: number;
42
72
  name: string;
@@ -62,18 +92,21 @@ declare class StepCheckpointEntity {
62
92
  * `numValue`, strings/booleans in `strValue` (booleans as "true"/"false"); see normalizeAttributeRows.
63
93
  */
64
94
  declare class RunAttributeEntity {
95
+ [EntityRepositoryType]?: RunAttributeRepository;
65
96
  runId: string;
66
97
  key: string;
67
98
  strValue?: string | null;
68
99
  numValue?: number | null;
69
100
  }
70
101
  declare class SignalWaiterEntity {
102
+ [EntityRepositoryType]?: SignalWaiterRepository;
71
103
  token: string;
72
104
  runId: string;
73
105
  seq: number;
74
106
  parallelGroup?: string | null;
75
107
  }
76
108
  declare class BufferedSignalEntity {
109
+ [EntityRepositoryType]?: BufferedSignalRepository;
77
110
  id: number;
78
111
  token: string;
79
112
  payload?: unknown;
@@ -82,6 +115,7 @@ declare class BufferedSignalEntity {
82
115
  * caller-minted `id` (a uuid, not autoincrement — `removeBufferedEvent` targets it directly), not by
83
116
  * `runId`: events are name-based pub/sub, not tied to any one run. */
84
117
  declare class BufferedEventEntity {
118
+ [EntityRepositoryType]?: BufferedEventRepository;
85
119
  id: string;
86
120
  name: string;
87
121
  payload?: unknown;
@@ -162,15 +196,25 @@ declare class MikroOrmStateStore implements StateStore {
162
196
  * predicates only raw SQL can express — the `tag` JSON LIKE and one EXISTS per attribute filter.
163
197
  * Shared so a facet count and the page it labels are taken over the SAME set. */
164
198
  private runQueryBuilder;
199
+ /** `tags LIKE '%"etl"%'` — the quoted token, so `etl` doesn't match `etl-foo`. See the note in
200
+ * {@link listRuns} for why this is raw SQL rather than a MikroORM condition. */
201
+ private tagLikeSql;
165
202
  /** `GROUP BY status, origin` over the same predicates {@link listRuns} pages — one aggregate, so a
166
203
  * console can show whole-set counts next to a bounded page instead of downloading every run to
167
204
  * count them in the browser. */
168
205
  runFacets(query: RunFacetQuery): Promise<RunFacetRow[]>;
206
+ /** Distinct values of one filter axis over the same predicates {@link listRuns} pages. A run-table
207
+ * column is a `GROUP BY` like {@link runFacets}, exact over the whole matching set; the tag and
208
+ * attribute axes live outside the row (json array / side table) and are counted from a bounded
209
+ * page of runs instead — see {@link RunValueFacetOptions.scan}. */
210
+ runValueFacets(axis: RunValueAxis, query: RunFacetQuery, opts?: RunValueFacetOptions): Promise<RunValueFacetRow[]>;
169
211
  /** One attribute predicate as a raw EXISTS subquery on the side-table, correlated to the outer run
170
212
  * alias. `<>` (ne) also excludes runs where the attribute is absent (missing-key-never-matches):
171
213
  * EXISTS already requires a present row, so EXISTS(... <> ...) is exactly ne-with-present. Numeric
172
- * operands compare the num column, everything else the str column. Identifiers are quoted per
173
- * driver and operands are inlined as literals (string operands are escaped by doubling quotes). */
214
+ * operands compare the num column, everything else the str column. `in` contributes one comparison
215
+ * per member, ORed inside the single EXISTS — one subquery either way, and an `in` over no values
216
+ * yields no comparison and so matches nothing. Identifiers are quoted per driver and operands are
217
+ * inlined as literals (string operands are escaped by doubling quotes). */
174
218
  private attributeExistsSql;
175
219
  /** Resolve the side-table's actual column names from MikroORM metadata, so the raw EXISTS matches
176
220
  * the active naming strategy (underscore vs camelCase) rather than guessing. The run PK column is
@@ -254,4 +298,4 @@ declare function durableManagedTables(): string[];
254
298
  */
255
299
  declare function ensureMikroOrmDurableSchema(orm: MikroORM): Promise<void>;
256
300
 
257
- export { BufferedEventEntity, BufferedSignalEntity, type DurableColumnNaming, ENTITIES, MikroOrmStateStore, type MikroOrmStateStoreOptions, RunAttributeEntity, SignalWaiterEntity, StepCheckpointEntity, WorkflowRunEntity, durableEntities, durableManagedTables, ensureMikroOrmDurableSchema };
301
+ export { BufferedEventEntity, BufferedEventRepository, BufferedSignalEntity, BufferedSignalRepository, type DurableColumnNaming, ENTITIES, MikroOrmStateStore, type MikroOrmStateStoreOptions, RunAttributeEntity, RunAttributeRepository, SignalWaiterEntity, SignalWaiterRepository, StepCheckpointEntity, StepCheckpointRepository, WorkflowRunEntity, WorkflowRunRepository, durableEntities, durableManagedTables, ensureMikroOrmDurableSchema };
package/dist/index.js CHANGED
@@ -2,7 +2,42 @@ var __defProp = Object.defineProperty;
2
2
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
3
 
4
4
  // src/entities.ts
5
- import { EntitySchema } from "@mikro-orm/core";
5
+ import { EntityRepositoryType, EntitySchema } from "@mikro-orm/core";
6
+
7
+ // src/repositories.ts
8
+ import { EntityRepository } from "@mikro-orm/core";
9
+ var WorkflowRunRepository = class extends EntityRepository {
10
+ static {
11
+ __name(this, "WorkflowRunRepository");
12
+ }
13
+ };
14
+ var StepCheckpointRepository = class extends EntityRepository {
15
+ static {
16
+ __name(this, "StepCheckpointRepository");
17
+ }
18
+ };
19
+ var RunAttributeRepository = class extends EntityRepository {
20
+ static {
21
+ __name(this, "RunAttributeRepository");
22
+ }
23
+ };
24
+ var SignalWaiterRepository = class extends EntityRepository {
25
+ static {
26
+ __name(this, "SignalWaiterRepository");
27
+ }
28
+ };
29
+ var BufferedSignalRepository = class extends EntityRepository {
30
+ static {
31
+ __name(this, "BufferedSignalRepository");
32
+ }
33
+ };
34
+ var BufferedEventRepository = class extends EntityRepository {
35
+ static {
36
+ __name(this, "BufferedEventRepository");
37
+ }
38
+ };
39
+
40
+ // src/entities.ts
6
41
  function snakeCase(property) {
7
42
  return property.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
8
43
  }
@@ -17,6 +52,7 @@ var WorkflowRunEntity = class {
17
52
  static {
18
53
  __name(this, "WorkflowRunEntity");
19
54
  }
55
+ [EntityRepositoryType];
20
56
  id;
21
57
  workflow;
22
58
  workflowVersion;
@@ -41,6 +77,7 @@ var StepCheckpointEntity = class {
41
77
  static {
42
78
  __name(this, "StepCheckpointEntity");
43
79
  }
80
+ [EntityRepositoryType];
44
81
  runId;
45
82
  seq;
46
83
  name;
@@ -63,6 +100,7 @@ var RunAttributeEntity = class {
63
100
  static {
64
101
  __name(this, "RunAttributeEntity");
65
102
  }
103
+ [EntityRepositoryType];
66
104
  runId;
67
105
  key;
68
106
  strValue;
@@ -72,6 +110,7 @@ var SignalWaiterEntity = class {
72
110
  static {
73
111
  __name(this, "SignalWaiterEntity");
74
112
  }
113
+ [EntityRepositoryType];
75
114
  token;
76
115
  runId;
77
116
  seq;
@@ -81,6 +120,7 @@ var BufferedSignalEntity = class {
81
120
  static {
82
121
  __name(this, "BufferedSignalEntity");
83
122
  }
123
+ [EntityRepositoryType];
84
124
  id;
85
125
  token;
86
126
  payload;
@@ -89,6 +129,7 @@ var BufferedEventEntity = class {
89
129
  static {
90
130
  __name(this, "BufferedEventEntity");
91
131
  }
132
+ [EntityRepositoryType];
92
133
  id;
93
134
  name;
94
135
  payload;
@@ -106,6 +147,9 @@ function durableEntities(options = {}) {
106
147
  const workflowRuns = new EntitySchema({
107
148
  class: WorkflowRunEntity,
108
149
  tableName: "durable_workflow_runs",
150
+ // Wired per schema rather than once on the class: `durableEntities` can be called again with a
151
+ // different `naming`, and each call builds a distinct EntitySchema over the same class.
152
+ repository: /* @__PURE__ */ __name(() => WorkflowRunRepository, "repository"),
109
153
  // Global filter: confines every read to the tenant's namespace when the store is scoped.
110
154
  // The `cond` returns `{}` (no-op) when the `namespace` arg is `undefined`, so an unscoped
111
155
  // (operator / control-plane) store sees all rows unchanged — existing behaviour is preserved.
@@ -269,6 +313,7 @@ function durableEntities(options = {}) {
269
313
  const stepCheckpoints = new EntitySchema({
270
314
  class: StepCheckpointEntity,
271
315
  tableName: "durable_step_checkpoints",
316
+ repository: /* @__PURE__ */ __name(() => StepCheckpointRepository, "repository"),
272
317
  properties: {
273
318
  runId: {
274
319
  type: "string",
@@ -357,6 +402,7 @@ function durableEntities(options = {}) {
357
402
  const runAttributes = new EntitySchema({
358
403
  class: RunAttributeEntity,
359
404
  tableName: "durable_run_attributes",
405
+ repository: /* @__PURE__ */ __name(() => RunAttributeRepository, "repository"),
360
406
  // The search-attribute EXISTS join (see the class docstring) pushes equality/range predicates down
361
407
  // onto `(key, numValue)` / `(key, strValue)`; mirror the Prisma adapter so those scans stay indexed.
362
408
  indexes: [
@@ -402,6 +448,7 @@ function durableEntities(options = {}) {
402
448
  const signalWaiters = new EntitySchema({
403
449
  class: SignalWaiterEntity,
404
450
  tableName: "durable_signal_waiters",
451
+ repository: /* @__PURE__ */ __name(() => SignalWaiterRepository, "repository"),
405
452
  properties: {
406
453
  token: {
407
454
  type: "string",
@@ -429,6 +476,7 @@ function durableEntities(options = {}) {
429
476
  const bufferedSignals = new EntitySchema({
430
477
  class: BufferedSignalEntity,
431
478
  tableName: "durable_buffered_signals",
479
+ repository: /* @__PURE__ */ __name(() => BufferedSignalRepository, "repository"),
432
480
  properties: {
433
481
  id: {
434
482
  type: "integer",
@@ -451,6 +499,7 @@ function durableEntities(options = {}) {
451
499
  const bufferedEvents = new EntitySchema({
452
500
  class: BufferedEventEntity,
453
501
  tableName: "durable_buffered_events",
502
+ repository: /* @__PURE__ */ __name(() => BufferedEventRepository, "repository"),
454
503
  // `(name, publishedAt)` serves both `listBufferedEvents`'s name-scoped scan and its oldest-first
455
504
  // ordering in one index.
456
505
  indexes: [
@@ -498,7 +547,7 @@ __name(durableEntities, "durableEntities");
498
547
  var ENTITIES = durableEntities();
499
548
 
500
549
  // src/mikro-orm-state-store.ts
501
- import { attributeColumnFor, attributeOperand, mergeRunFacetRows, normalizeAttributeRows, parseDuration, sqlComparator } from "@dudousxd/nestjs-durable-core";
550
+ import { RUN_VALUE_FACET_SCAN, attributePredicateOperands, axisIsRunColumn, mergeRunFacetRows, mergeRunValueFacetRows, normalizeAttributeRows, parseDuration, runValueFacetsFromRuns } from "@dudousxd/nestjs-durable-core";
502
551
  import { raw } from "@mikro-orm/core";
503
552
 
504
553
  // src/schema.ts
@@ -982,20 +1031,47 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
982
1031
  * applied by the QueryBuilder path in both callers. */
983
1032
  runWhere(query) {
984
1033
  const where = {};
1034
+ const and = [];
985
1035
  if (query.workflow) where.workflow = query.workflow;
1036
+ if (query.workflows) and.push({
1037
+ workflow: {
1038
+ $in: query.workflows
1039
+ }
1040
+ });
986
1041
  if (query.namespace !== void 0) where.namespace = query.namespace;
1042
+ if (query.namespaces) and.push({
1043
+ namespace: {
1044
+ $in: query.namespaces
1045
+ }
1046
+ });
987
1047
  if (query.origin !== void 0) where.origin = query.origin;
1048
+ if (query.origins) {
1049
+ const named = query.origins.filter((o) => o !== null);
1050
+ const branches = [];
1051
+ if (named.length) branches.push({
1052
+ origin: {
1053
+ $in: named
1054
+ }
1055
+ });
1056
+ if (query.origins.some((o) => o === null)) branches.push({
1057
+ origin: null
1058
+ });
1059
+ and.push(branches.length ? {
1060
+ $or: branches
1061
+ } : {
1062
+ origin: {
1063
+ $in: []
1064
+ }
1065
+ });
1066
+ }
988
1067
  if (query.status && query.statuses) {
989
- where.$and = [
990
- {
991
- status: query.status
992
- },
993
- {
994
- status: {
995
- $in: query.statuses
996
- }
1068
+ and.push({
1069
+ status: query.status
1070
+ }, {
1071
+ status: {
1072
+ $in: query.statuses
997
1073
  }
998
- ];
1074
+ });
999
1075
  } else if (query.status) {
1000
1076
  where.status = query.status;
1001
1077
  } else if (query.statuses) {
@@ -1003,6 +1079,7 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1003
1079
  $in: query.statuses
1004
1080
  };
1005
1081
  }
1082
+ if (and.length) where.$and = and;
1006
1083
  return where;
1007
1084
  }
1008
1085
  async listRuns(query) {
@@ -1011,7 +1088,7 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1011
1088
  const orderBy = {
1012
1089
  createdAt: "desc"
1013
1090
  };
1014
- if (query.tag || query.attributes?.length) {
1091
+ if (query.tag || query.tags || query.attributes?.length) {
1015
1092
  const qb = this.runQueryBuilder(em, query, where).orderBy(orderBy);
1016
1093
  if (query.limit != null) qb.limit(query.limit);
1017
1094
  if (query.offset != null) qb.offset(query.offset);
@@ -1037,17 +1114,23 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1037
1114
  const quote = this.idQuote(em);
1038
1115
  const cols = this.attributeColumns(em);
1039
1116
  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}'`);
1117
+ if (query.tag) qb.andWhere(this.tagLikeSql(em, query.tag, quote));
1118
+ if (query.tags) {
1119
+ const branches = query.tags.map((t) => this.tagLikeSql(em, t, quote));
1120
+ qb.andWhere(branches.length ? `(${branches.join(" OR ")})` : "1 = 0");
1045
1121
  }
1046
1122
  for (const f of query.attributes ?? []) {
1047
1123
  qb.andWhere(this.attributeExistsSql(f, quote, "r", cols));
1048
1124
  }
1049
1125
  return qb;
1050
1126
  }
1127
+ /** `tags LIKE '%"etl"%'` — the quoted token, so `etl` doesn't match `etl-foo`. See the note in
1128
+ * {@link listRuns} for why this is raw SQL rather than a MikroORM condition. */
1129
+ tagLikeSql(em, tag, quote) {
1130
+ const pattern = `%"${tag.replace(/'/g, "''")}"%`;
1131
+ const colExpr = this.jsonAsText(em, `${quote("r")}.${quote(this.tagsColumn(em))}`);
1132
+ return `${colExpr} LIKE '${pattern}'`;
1133
+ }
1051
1134
  /** `GROUP BY status, origin` over the same predicates {@link listRuns} pages — one aggregate, so a
1052
1135
  * console can show whole-set counts next to a bounded page instead of downloading every run to
1053
1136
  * count them in the browser. */
@@ -1068,20 +1151,50 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1068
1151
  count: Number(r.count)
1069
1152
  })));
1070
1153
  }
1154
+ /** Distinct values of one filter axis over the same predicates {@link listRuns} pages. A run-table
1155
+ * column is a `GROUP BY` like {@link runFacets}, exact over the whole matching set; the tag and
1156
+ * attribute axes live outside the row (json array / side table) and are counted from a bounded
1157
+ * page of runs instead — see {@link RunValueFacetOptions.scan}. */
1158
+ async runValueFacets(axis, query, opts) {
1159
+ if (!axisIsRunColumn(axis)) {
1160
+ const runs = await this.listRuns({
1161
+ ...query,
1162
+ limit: opts?.scan ?? RUN_VALUE_FACET_SCAN
1163
+ });
1164
+ return runValueFacetsFromRuns(runs, axis, opts);
1165
+ }
1166
+ const em = this.fork();
1167
+ const meta = em.getMetadata().get(WorkflowRunEntity);
1168
+ const col = meta.properties[axis.field]?.fieldNames?.[0] ?? axis.field;
1169
+ const rows = await this.runQueryBuilder(em, query, this.runWhere(query)).select([
1170
+ `r.${col}`
1171
+ ]).addSelect(raw("count(*) as count")).groupBy([
1172
+ `r.${col}`
1173
+ ]).execute();
1174
+ return mergeRunValueFacetRows(rows.map((row) => ({
1175
+ value: row[axis.field] ?? null,
1176
+ count: Number(row.count)
1177
+ })), opts);
1178
+ }
1071
1179
  /** One attribute predicate as a raw EXISTS subquery on the side-table, correlated to the outer run
1072
1180
  * alias. `<>` (ne) also excludes runs where the attribute is absent (missing-key-never-matches):
1073
1181
  * EXISTS already requires a present row, so EXISTS(... <> ...) is exactly ne-with-present. Numeric
1074
- * operands compare the num column, everything else the str column. Identifiers are quoted per
1075
- * driver and operands are inlined as literals (string operands are escaped by doubling quotes). */
1182
+ * operands compare the num column, everything else the str column. `in` contributes one comparison
1183
+ * per member, ORed inside the single EXISTS — one subquery either way, and an `in` over no values
1184
+ * yields no comparison and so matches nothing. Identifiers are quoted per driver and operands are
1185
+ * inlined as literals (string operands are escaped by doubling quotes). */
1076
1186
  attributeExistsSql(f, quote, alias, cols) {
1077
- const col = attributeColumnFor(f) === "numValue" ? cols.numValue : cols.strValue;
1078
- const cmp = sqlComparator(f.op);
1079
- const operand = attributeOperand(f);
1080
- const literal = typeof operand === "number" ? String(operand) : `'${String(operand).replace(/'/g, "''")}'`;
1187
+ const comparisons = attributePredicateOperands(f).map(({ column, comparator, operand }) => {
1188
+ const col = column === "numValue" ? cols.numValue : cols.strValue;
1189
+ const literal = typeof operand === "number" ? String(operand) : `'${String(operand).replace(/'/g, "''")}'`;
1190
+ return `${quote("a")}.${quote(col)} ${comparator} ${literal}`;
1191
+ });
1192
+ if (comparisons.length === 0) return "1 = 0";
1081
1193
  const a = quote(cols.table);
1082
1194
  const sub = quote("a");
1083
1195
  const outerId = `${quote(alias)}.${quote(cols.runPk)}`;
1084
- return `EXISTS (SELECT 1 FROM ${a} ${sub} WHERE ${sub}.${quote(cols.runId)} = ${outerId} AND ${sub}.${quote(cols.key)} = '${f.key.replace(/'/g, "''")}' AND ${sub}.${quote(col)} ${cmp} ${literal})`;
1196
+ const match = comparisons.length === 1 ? comparisons[0] : `(${comparisons.join(" OR ")})`;
1197
+ return `EXISTS (SELECT 1 FROM ${a} ${sub} WHERE ${sub}.${quote(cols.runId)} = ${outerId} AND ${sub}.${quote(cols.key)} = '${f.key.replace(/'/g, "''")}' AND ${match})`;
1085
1198
  }
1086
1199
  /** Resolve the side-table's actual column names from MikroORM metadata, so the raw EXISTS matches
1087
1200
  * the active naming strategy (underscore vs camelCase) rather than guessing. The run PK column is
@@ -1361,13 +1474,19 @@ function fromCheckpointEntity(e) {
1361
1474
  __name(fromCheckpointEntity, "fromCheckpointEntity");
1362
1475
  export {
1363
1476
  BufferedEventEntity,
1477
+ BufferedEventRepository,
1364
1478
  BufferedSignalEntity,
1479
+ BufferedSignalRepository,
1365
1480
  ENTITIES,
1366
1481
  MikroOrmStateStore,
1367
1482
  RunAttributeEntity,
1483
+ RunAttributeRepository,
1368
1484
  SignalWaiterEntity,
1485
+ SignalWaiterRepository,
1369
1486
  StepCheckpointEntity,
1487
+ StepCheckpointRepository,
1370
1488
  WorkflowRunEntity,
1489
+ WorkflowRunRepository,
1371
1490
  durableEntities,
1372
1491
  durableManagedTables,
1373
1492
  ensureMikroOrmDurableSchema