@dudousxd/nestjs-durable-dashboard 0.43.2 → 0.44.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.
@@ -1,8 +1,10 @@
1
1
  import * as _dudousxd_nestjs_durable_core from '@dudousxd/nestjs-durable-core';
2
- import { RunGateway, StateStore, WorkflowEngine, RunQuery, RunListItem, RunFacetQuery, RunFacetRow, DurableTopology, GroupHealth, RunDetail, RunResult, UpdateResult, EngineEvent, RunStatus } from '@dudousxd/nestjs-durable-core';
2
+ import { RunGateway, StateStore, WorkflowEngine, RunQuery, RunListItem, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, DurableTopology, GroupHealth, RunDetail, RunResult, UpdateResult, EngineEvent, AttributeFilter } from '@dudousxd/nestjs-durable-core';
3
3
  export { RunDetail } from '@dudousxd/nestjs-durable-core';
4
4
  import * as rxjs from 'rxjs';
5
5
  import { Observable } from 'rxjs';
6
+ import * as _dudousxd_nestjs_filter from '@dudousxd/nestjs-filter';
7
+ import { FilterRunner } from '@dudousxd/nestjs-filter';
6
8
  import { DynamicModule, Type, CanActivate, Provider, InjectionToken, OptionalFactoryDependency, ExecutionContext, StreamableFile } from '@nestjs/common';
7
9
 
8
10
  /** The session user the host `login` hook returns to mint a cookie. */
@@ -137,6 +139,10 @@ declare class DashboardService {
137
139
  /** `(status, origin)` counts over the same predicates {@link listRuns} pages — what keeps the
138
140
  * console's chips exact while its list is bounded to a page. */
139
141
  runFacets(query: RunFacetQuery): Promise<RunFacetRow[]>;
142
+ /** The distinct values of one filter axis over the same predicates — what fills the console's
143
+ * tenant/tag/attribute pickers, so an operator picks a value that exists instead of typing one
144
+ * blind. `[]` when the store cannot enumerate them; the pickers then fall back to free text. */
145
+ runValueFacets(axis: RunValueAxis, query: RunFacetQuery, opts?: RunValueFacetOptions): Promise<RunValueFacetRow[]>;
140
146
  /** This deployment's durable role (control plane vs tenant) + tenant name — for the dashboard header
141
147
  * badge. Routes through the gateway, which knows its own topology (store-backed = operator, proxy =
142
148
  * tenant). Works on both topologies (no store/engine needed). */
@@ -210,28 +216,68 @@ declare class DashboardService {
210
216
  */
211
217
  type RunListRow = Omit<RunListItem, 'input' | 'output' | 'error'>;
212
218
 
219
+ /**
220
+ * What this adapter hands the filter runner in place of a query builder: a {@link RunQuery} under
221
+ * construction.
222
+ *
223
+ * The runner never inspects it — it only passes it back to the adapter's own methods — so the shape
224
+ * is ours to choose, and a plain predicate bag is the honest one. There is no SQL to append to here;
225
+ * a run query is a fixed set of named predicates that the gateway resolves, so "building" it is
226
+ * filling fields in, and everything the filter lib can express but `RunQuery` cannot is rejected at
227
+ * the point of translation rather than accumulated and silently dropped.
228
+ */
229
+ declare class RunQueryDraft {
230
+ readonly query: RunQuery;
231
+ /** Narrow by one or more predicates. Later calls overwrite the same field — the runner applies one
232
+ * clause per field, so the last one wins, exactly as a repeated `where` on a builder would. */
233
+ narrow(patch: RunQuery): void;
234
+ /** Add one search-attribute predicate. These ACCUMULATE (unlike {@link narrow}) because
235
+ * `RunQuery.attributes` is an ANDed list — two predicates on different keys are both meant. */
236
+ attribute(filter: AttributeFilter): void;
237
+ /** The predicates minus the page window and the axes a facet count reports on — what
238
+ * `runFacets`/`runValueFacets` are taken over. */
239
+ facetQuery(): RunFacetQuery;
240
+ }
241
+
213
242
  /** JSON API consumed by the control-plane SPA. Mounted at `apiBasePath` (set by RouterModule). */
214
243
  declare class DurableApiController {
215
244
  private readonly dashboard;
216
- constructor(dashboard: DashboardService);
245
+ private readonly runner;
246
+ constructor(dashboard: DashboardService, runner: FilterRunner);
217
247
  /**
218
- * The run list, filtered by whatever the operator asked for. Every predicate is OPTIONAL and an
219
- * omitted one means "don't narrow on this axis" — in particular `namespace`, whose absence keeps
220
- * the historical, deliberate behaviour that read paths are NOT namespace-scoped (core,
221
- * `WorkflowRun.namespace`): the console shows every tenant's runs until an operator chooses one.
222
- * A blank param (`?namespace=`, from a cleared filter box) is the same as an absent one; passing
223
- * `''` through would be an exact match on a tenant nobody has, i.e. a silently empty console.
248
+ * The run list, filtered by whatever the operator asked for.
249
+ *
250
+ * The predicates are parsed, validated and translated by `@dudousxd/nestjs-filter` (see
251
+ * `RunFilter` for the two spellings it accepts, and `RunQueryAdapter` for how they become a
252
+ * `RunQuery`). Every predicate is OPTIONAL and an omitted one means "don't narrow on this axis" —
253
+ * in particular `namespace`, whose absence keeps the historical, deliberate behaviour that read
254
+ * paths are NOT namespace-scoped (core, `WorkflowRun.namespace`): the console shows every tenant's
255
+ * runs until an operator chooses one.
224
256
  */
225
- runs(status?: RunStatus | RunStatus[], workflow?: string, tag?: string, attr?: string | string[], namespace?: string, origin?: string, unattributed?: string, limit?: string, offset?: string): Promise<RunListRow[]>;
257
+ runs(draft: RunQueryDraft): Promise<RunListRow[]>;
226
258
  /**
227
259
  * `(status, origin)` counts for the runs matching the same tag/tenant/attribute predicates as
228
260
  * `GET runs`. This is what lets that listing be PAGED at all: the page bounds what the console
229
261
  * renders, this bounds nothing and keeps its status and origin chips exact.
230
262
  *
231
- * `status`, `origin` and the page bounds are deliberately NOT accepted — they are the axes being
232
- * counted, so narrowing by them would report the answer back to itself.
263
+ * `status`, `origin` and the page bounds are accepted (the route takes the same filter) but
264
+ * deliberately DROPPED before counting — they are the axes being counted, so narrowing by them
265
+ * would report the answer back to itself.
233
266
  */
234
- facets(workflow?: string, tag?: string, attr?: string | string[], namespace?: string): Promise<_dudousxd_nestjs_durable_core.RunFacetRow[]>;
267
+ facets(draft: RunQueryDraft): Promise<_dudousxd_nestjs_durable_core.RunFacetRow[]>;
268
+ /**
269
+ * The distinct values one filter field takes across the runs matching every OTHER active
270
+ * predicate, with counts — what the console's tenant/tag/attribute pickers list.
271
+ *
272
+ * `?groupByCount[field]=tag&groupByCount[limit]=20`, with the filters in the structured
273
+ * `filter[where][...]` form (what `filterQuery()` builds). Scoping the values to the active
274
+ * filters is the point: picking a tenant leaves the tag picker offering only that tenant's tags,
275
+ * so a picker never offers a value whose result set is empty.
276
+ *
277
+ * `limit` matters rather than being a nicety — tag and attribute cardinality grows with the data
278
+ * (a `singleton:<key>` tag is minted per key), so the unbounded answer is a listing.
279
+ */
280
+ values(query: Record<string, unknown>): Promise<_dudousxd_nestjs_filter.GroupByCountResult>;
235
281
  /** Prometheus-text metrics (runs/steps by outcome, per-workflow counts) for a scrape. */
236
282
  metrics(): Promise<string>;
237
283
  /** Per-group worker health (queue backlog + live worker heartbeats) for the Workers panel. */
@@ -256,7 +302,7 @@ declare class DurableApiController {
256
302
  * The filter takes the SAME params as `GET runs` and is parsed identically, so "retry all" acts on
257
303
  * exactly the set the operator was looking at rather than a wider one.
258
304
  */
259
- bulk(action: 'retry' | 'cancel', status?: RunStatus, tag?: string, workflow?: string, attr?: string | string[], compensate?: string, namespace?: string, origin?: string, unattributed?: string): Promise<{
305
+ bulk(action: 'retry' | 'cancel', draft: RunQueryDraft, compensate?: string): Promise<{
260
306
  matched: number;
261
307
  applied: number;
262
308
  }>;
@@ -402,6 +448,24 @@ interface DurableDashboardOptions {
402
448
  * (e.g. an `EntityManager` to look up an admin user).
403
449
  */
404
450
  dashboardAuth?: DashboardAuthOptions;
451
+ /**
452
+ * Who registers `@dudousxd/nestjs-filter`'s core — the console's run filters are written with it.
453
+ *
454
+ * - `'bootstrap'` (default): this module calls `FilterModule.forRoot()` itself, so the console
455
+ * works in an app that has never heard of the filter library.
456
+ * - `'host'`: your app already calls `FilterModule.forRoot(...)`; this module registers only its
457
+ * own filter and adapter.
458
+ *
459
+ * It has to be YOUR call rather than something detected, because `forRoot` is a GLOBAL
460
+ * registration: called twice, the second one's module options (validation policy, input format,
461
+ * page-size ceiling) shadow the first's, and the app-wide interceptor is registered twice. There
462
+ * is no moment at module-definition time when this module could see whether you have called it.
463
+ *
464
+ * Only the core is shared. The console's adapter never is: `RunFilter` names its own via
465
+ * `@Filterable({ adapter })`, so your global adapter (your ORM's) keeps answering for your filters
466
+ * and the run gateway answers for the console's, in the same process.
467
+ */
468
+ filterModule?: 'bootstrap' | 'host';
405
469
  }
406
470
  /**
407
471
  * Async variant of {@link DurableDashboardOptions}. The mount paths and `guards` stay static (the
@@ -416,6 +480,8 @@ interface DurableDashboardAsyncOptions {
416
480
  apiBasePath?: string;
417
481
  guards?: Type<CanActivate>[];
418
482
  imports?: DynamicModule['imports'];
483
+ /** See {@link DurableDashboardOptions.filterModule}. */
484
+ filterModule?: 'bootstrap' | 'host';
419
485
  /** Providers injected into `useDashboardAuth`, in order. Shared with a `guards` class's own deps. */
420
486
  inject?: Array<InjectionToken | OptionalFactoryDependency>;
421
487
  /** Build the `dashboardAuth` config from injected deps (or `undefined` to leave the console open). */
@@ -439,6 +505,8 @@ declare class DurableApiModule {
439
505
  * (true whenever `dashboardAuth` was set, or unconditionally for `forRootAsync`, where it's
440
506
  * resolved at runtime and is a no-op if it ends up unconfigured). */
441
507
  authMayBeConfigured: boolean;
508
+ /** See {@link DurableDashboardOptions.filterModule}. */
509
+ filterModule?: 'bootstrap' | 'host';
442
510
  }): DynamicModule;
443
511
  }
444
512
  /**
@@ -1,8 +1,10 @@
1
1
  import * as _dudousxd_nestjs_durable_core from '@dudousxd/nestjs-durable-core';
2
- import { RunGateway, StateStore, WorkflowEngine, RunQuery, RunListItem, RunFacetQuery, RunFacetRow, DurableTopology, GroupHealth, RunDetail, RunResult, UpdateResult, EngineEvent, RunStatus } from '@dudousxd/nestjs-durable-core';
2
+ import { RunGateway, StateStore, WorkflowEngine, RunQuery, RunListItem, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, DurableTopology, GroupHealth, RunDetail, RunResult, UpdateResult, EngineEvent, AttributeFilter } from '@dudousxd/nestjs-durable-core';
3
3
  export { RunDetail } from '@dudousxd/nestjs-durable-core';
4
4
  import * as rxjs from 'rxjs';
5
5
  import { Observable } from 'rxjs';
6
+ import * as _dudousxd_nestjs_filter from '@dudousxd/nestjs-filter';
7
+ import { FilterRunner } from '@dudousxd/nestjs-filter';
6
8
  import { DynamicModule, Type, CanActivate, Provider, InjectionToken, OptionalFactoryDependency, ExecutionContext, StreamableFile } from '@nestjs/common';
7
9
 
8
10
  /** The session user the host `login` hook returns to mint a cookie. */
@@ -137,6 +139,10 @@ declare class DashboardService {
137
139
  /** `(status, origin)` counts over the same predicates {@link listRuns} pages — what keeps the
138
140
  * console's chips exact while its list is bounded to a page. */
139
141
  runFacets(query: RunFacetQuery): Promise<RunFacetRow[]>;
142
+ /** The distinct values of one filter axis over the same predicates — what fills the console's
143
+ * tenant/tag/attribute pickers, so an operator picks a value that exists instead of typing one
144
+ * blind. `[]` when the store cannot enumerate them; the pickers then fall back to free text. */
145
+ runValueFacets(axis: RunValueAxis, query: RunFacetQuery, opts?: RunValueFacetOptions): Promise<RunValueFacetRow[]>;
140
146
  /** This deployment's durable role (control plane vs tenant) + tenant name — for the dashboard header
141
147
  * badge. Routes through the gateway, which knows its own topology (store-backed = operator, proxy =
142
148
  * tenant). Works on both topologies (no store/engine needed). */
@@ -210,28 +216,68 @@ declare class DashboardService {
210
216
  */
211
217
  type RunListRow = Omit<RunListItem, 'input' | 'output' | 'error'>;
212
218
 
219
+ /**
220
+ * What this adapter hands the filter runner in place of a query builder: a {@link RunQuery} under
221
+ * construction.
222
+ *
223
+ * The runner never inspects it — it only passes it back to the adapter's own methods — so the shape
224
+ * is ours to choose, and a plain predicate bag is the honest one. There is no SQL to append to here;
225
+ * a run query is a fixed set of named predicates that the gateway resolves, so "building" it is
226
+ * filling fields in, and everything the filter lib can express but `RunQuery` cannot is rejected at
227
+ * the point of translation rather than accumulated and silently dropped.
228
+ */
229
+ declare class RunQueryDraft {
230
+ readonly query: RunQuery;
231
+ /** Narrow by one or more predicates. Later calls overwrite the same field — the runner applies one
232
+ * clause per field, so the last one wins, exactly as a repeated `where` on a builder would. */
233
+ narrow(patch: RunQuery): void;
234
+ /** Add one search-attribute predicate. These ACCUMULATE (unlike {@link narrow}) because
235
+ * `RunQuery.attributes` is an ANDed list — two predicates on different keys are both meant. */
236
+ attribute(filter: AttributeFilter): void;
237
+ /** The predicates minus the page window and the axes a facet count reports on — what
238
+ * `runFacets`/`runValueFacets` are taken over. */
239
+ facetQuery(): RunFacetQuery;
240
+ }
241
+
213
242
  /** JSON API consumed by the control-plane SPA. Mounted at `apiBasePath` (set by RouterModule). */
214
243
  declare class DurableApiController {
215
244
  private readonly dashboard;
216
- constructor(dashboard: DashboardService);
245
+ private readonly runner;
246
+ constructor(dashboard: DashboardService, runner: FilterRunner);
217
247
  /**
218
- * The run list, filtered by whatever the operator asked for. Every predicate is OPTIONAL and an
219
- * omitted one means "don't narrow on this axis" — in particular `namespace`, whose absence keeps
220
- * the historical, deliberate behaviour that read paths are NOT namespace-scoped (core,
221
- * `WorkflowRun.namespace`): the console shows every tenant's runs until an operator chooses one.
222
- * A blank param (`?namespace=`, from a cleared filter box) is the same as an absent one; passing
223
- * `''` through would be an exact match on a tenant nobody has, i.e. a silently empty console.
248
+ * The run list, filtered by whatever the operator asked for.
249
+ *
250
+ * The predicates are parsed, validated and translated by `@dudousxd/nestjs-filter` (see
251
+ * `RunFilter` for the two spellings it accepts, and `RunQueryAdapter` for how they become a
252
+ * `RunQuery`). Every predicate is OPTIONAL and an omitted one means "don't narrow on this axis" —
253
+ * in particular `namespace`, whose absence keeps the historical, deliberate behaviour that read
254
+ * paths are NOT namespace-scoped (core, `WorkflowRun.namespace`): the console shows every tenant's
255
+ * runs until an operator chooses one.
224
256
  */
225
- runs(status?: RunStatus | RunStatus[], workflow?: string, tag?: string, attr?: string | string[], namespace?: string, origin?: string, unattributed?: string, limit?: string, offset?: string): Promise<RunListRow[]>;
257
+ runs(draft: RunQueryDraft): Promise<RunListRow[]>;
226
258
  /**
227
259
  * `(status, origin)` counts for the runs matching the same tag/tenant/attribute predicates as
228
260
  * `GET runs`. This is what lets that listing be PAGED at all: the page bounds what the console
229
261
  * renders, this bounds nothing and keeps its status and origin chips exact.
230
262
  *
231
- * `status`, `origin` and the page bounds are deliberately NOT accepted — they are the axes being
232
- * counted, so narrowing by them would report the answer back to itself.
263
+ * `status`, `origin` and the page bounds are accepted (the route takes the same filter) but
264
+ * deliberately DROPPED before counting — they are the axes being counted, so narrowing by them
265
+ * would report the answer back to itself.
233
266
  */
234
- facets(workflow?: string, tag?: string, attr?: string | string[], namespace?: string): Promise<_dudousxd_nestjs_durable_core.RunFacetRow[]>;
267
+ facets(draft: RunQueryDraft): Promise<_dudousxd_nestjs_durable_core.RunFacetRow[]>;
268
+ /**
269
+ * The distinct values one filter field takes across the runs matching every OTHER active
270
+ * predicate, with counts — what the console's tenant/tag/attribute pickers list.
271
+ *
272
+ * `?groupByCount[field]=tag&groupByCount[limit]=20`, with the filters in the structured
273
+ * `filter[where][...]` form (what `filterQuery()` builds). Scoping the values to the active
274
+ * filters is the point: picking a tenant leaves the tag picker offering only that tenant's tags,
275
+ * so a picker never offers a value whose result set is empty.
276
+ *
277
+ * `limit` matters rather than being a nicety — tag and attribute cardinality grows with the data
278
+ * (a `singleton:<key>` tag is minted per key), so the unbounded answer is a listing.
279
+ */
280
+ values(query: Record<string, unknown>): Promise<_dudousxd_nestjs_filter.GroupByCountResult>;
235
281
  /** Prometheus-text metrics (runs/steps by outcome, per-workflow counts) for a scrape. */
236
282
  metrics(): Promise<string>;
237
283
  /** Per-group worker health (queue backlog + live worker heartbeats) for the Workers panel. */
@@ -256,7 +302,7 @@ declare class DurableApiController {
256
302
  * The filter takes the SAME params as `GET runs` and is parsed identically, so "retry all" acts on
257
303
  * exactly the set the operator was looking at rather than a wider one.
258
304
  */
259
- bulk(action: 'retry' | 'cancel', status?: RunStatus, tag?: string, workflow?: string, attr?: string | string[], compensate?: string, namespace?: string, origin?: string, unattributed?: string): Promise<{
305
+ bulk(action: 'retry' | 'cancel', draft: RunQueryDraft, compensate?: string): Promise<{
260
306
  matched: number;
261
307
  applied: number;
262
308
  }>;
@@ -402,6 +448,24 @@ interface DurableDashboardOptions {
402
448
  * (e.g. an `EntityManager` to look up an admin user).
403
449
  */
404
450
  dashboardAuth?: DashboardAuthOptions;
451
+ /**
452
+ * Who registers `@dudousxd/nestjs-filter`'s core — the console's run filters are written with it.
453
+ *
454
+ * - `'bootstrap'` (default): this module calls `FilterModule.forRoot()` itself, so the console
455
+ * works in an app that has never heard of the filter library.
456
+ * - `'host'`: your app already calls `FilterModule.forRoot(...)`; this module registers only its
457
+ * own filter and adapter.
458
+ *
459
+ * It has to be YOUR call rather than something detected, because `forRoot` is a GLOBAL
460
+ * registration: called twice, the second one's module options (validation policy, input format,
461
+ * page-size ceiling) shadow the first's, and the app-wide interceptor is registered twice. There
462
+ * is no moment at module-definition time when this module could see whether you have called it.
463
+ *
464
+ * Only the core is shared. The console's adapter never is: `RunFilter` names its own via
465
+ * `@Filterable({ adapter })`, so your global adapter (your ORM's) keeps answering for your filters
466
+ * and the run gateway answers for the console's, in the same process.
467
+ */
468
+ filterModule?: 'bootstrap' | 'host';
405
469
  }
406
470
  /**
407
471
  * Async variant of {@link DurableDashboardOptions}. The mount paths and `guards` stay static (the
@@ -416,6 +480,8 @@ interface DurableDashboardAsyncOptions {
416
480
  apiBasePath?: string;
417
481
  guards?: Type<CanActivate>[];
418
482
  imports?: DynamicModule['imports'];
483
+ /** See {@link DurableDashboardOptions.filterModule}. */
484
+ filterModule?: 'bootstrap' | 'host';
419
485
  /** Providers injected into `useDashboardAuth`, in order. Shared with a `guards` class's own deps. */
420
486
  inject?: Array<InjectionToken | OptionalFactoryDependency>;
421
487
  /** Build the `dashboardAuth` config from injected deps (or `undefined` to leave the console open). */
@@ -439,6 +505,8 @@ declare class DurableApiModule {
439
505
  * (true whenever `dashboardAuth` was set, or unconditionally for `forRootAsync`, where it's
440
506
  * resolved at runtime and is a no-op if it ends up unconfigured). */
441
507
  authMayBeConfigured: boolean;
508
+ /** See {@link DurableDashboardOptions.filterModule}. */
509
+ filterModule?: 'bootstrap' | 'host';
442
510
  }): DynamicModule;
443
511
  }
444
512
  /**