dataiku-sdk 4.0.0 → 4.2.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.
Files changed (66) hide show
  1. package/README.md +1 -0
  2. package/dist/build-metadata.json +1 -1
  3. package/dist/packages/types/src/index.d.ts +5 -1
  4. package/dist/packages/types/src/index.js +5 -1
  5. package/dist/src/cli/command-syntax.json +4136 -2756
  6. package/dist/src/cli/commands/code-env.js +5 -3
  7. package/dist/src/cli/commands/dataset.js +6 -4
  8. package/dist/src/cli/commands/future.js +7 -2
  9. package/dist/src/cli/commands/index.js +4 -0
  10. package/dist/src/cli/commands/job.js +12 -9
  11. package/dist/src/cli/commands/messaging-channel.d.ts +17 -0
  12. package/dist/src/cli/commands/messaging-channel.js +385 -0
  13. package/dist/src/cli/commands/project-deployer.js +33 -0
  14. package/dist/src/cli/commands/project.js +30 -0
  15. package/dist/src/cli/commands/recipe.js +23 -2
  16. package/dist/src/cli/commands/scenario.d.ts +6 -0
  17. package/dist/src/cli/commands/scenario.js +495 -17
  18. package/dist/src/cli/contract.js +20 -4
  19. package/dist/src/cli/env.d.ts +6 -0
  20. package/dist/src/cli/env.js +8 -0
  21. package/dist/src/cli/flags.js +34 -0
  22. package/dist/src/cli/helpers/cleanup.js +11 -0
  23. package/dist/src/cli/list-shapes.js +5 -0
  24. package/dist/src/cli/output.d.ts +2 -0
  25. package/dist/src/cli/output.js +24 -4
  26. package/dist/src/cli/plans.js +94 -2
  27. package/dist/src/cli/read-shapes.js +92 -1
  28. package/dist/src/cli/result-shapes.d.ts +19 -1
  29. package/dist/src/cli/result-shapes.js +228 -4
  30. package/dist/src/cli/selector-suggestions.d.ts +14 -0
  31. package/dist/src/cli/selector-suggestions.js +56 -0
  32. package/dist/src/cli/usage.d.ts +1 -1
  33. package/dist/src/cli/usage.js +36 -4
  34. package/dist/src/cli/version.d.ts +2 -2
  35. package/dist/src/cli/version.js +2 -2
  36. package/dist/src/cli.js +62 -13
  37. package/dist/src/client.d.ts +16 -4
  38. package/dist/src/client.js +45 -9
  39. package/dist/src/errors.d.ts +2 -0
  40. package/dist/src/errors.js +12 -2
  41. package/dist/src/generated/action-output-schemas.json +296 -0
  42. package/dist/src/index.d.ts +1 -0
  43. package/dist/src/index.js +1 -0
  44. package/dist/src/resources/bundles.d.ts +18 -0
  45. package/dist/src/resources/bundles.js +26 -0
  46. package/dist/src/resources/futures.d.ts +3 -0
  47. package/dist/src/resources/futures.js +5 -1
  48. package/dist/src/resources/messaging-channels.d.ts +83 -0
  49. package/dist/src/resources/messaging-channels.js +108 -0
  50. package/dist/src/resources/projects.d.ts +24 -0
  51. package/dist/src/resources/projects.js +36 -0
  52. package/dist/src/resources/recipe-create.d.ts +4 -1
  53. package/dist/src/resources/recipe-create.js +11 -2
  54. package/dist/src/resources/scenario-authoring.d.ts +52 -0
  55. package/dist/src/resources/scenario-authoring.js +216 -0
  56. package/dist/src/resources/scenarios.d.ts +128 -11
  57. package/dist/src/resources/scenarios.js +273 -43
  58. package/dist/src/resources/sql.js +3 -3
  59. package/dist/src/utils/messaging-channel-secrets.d.ts +17 -0
  60. package/dist/src/utils/messaging-channel-secrets.js +114 -0
  61. package/package.json +2 -1
  62. package/plugin.json +1 -1
  63. package/skills/dataiku-dss/SKILL.md +15 -16
  64. package/skills/dataiku-dss/references/discovery.md +2 -2
  65. package/skills/dataiku-dss/references/notifications.md +29 -0
  66. package/skills/dataiku-dss/references/troubleshooting.md +1 -0
@@ -1,6 +1,8 @@
1
1
  import type { ScenarioDetails, ScenarioStatus, ScenarioSummary, ScenarioWaitResult } from "../schemas.js";
2
2
  import { BaseResource } from "./base.js";
3
- export declare const SCENARIO_CANONICAL_EDITABLE_FIELDS: readonly ["params.steps", "params.triggers", "params.reporters", "params.customScript", "active", "name"];
3
+ import { type ScenarioListSelector } from "./scenario-authoring.js";
4
+ /** DSS keeps triggers and reporters at the top level of the scenario (as dataikuapi does), not under params. */
5
+ export declare const SCENARIO_CANONICAL_EDITABLE_FIELDS: readonly ["params.steps", "triggers", "reporters", "params.customScript", "active", "name"];
4
6
  /** Default byte cap applied when fetching a scenario run log. */
5
7
  export declare const DEFAULT_SCENARIO_MAX_LOG_BYTES = 1048576;
6
8
  /** One entry of GET /scenarios/{id}/get-last-runs — a finished (or running) scenario run summary. */
@@ -44,6 +46,45 @@ export interface ScenarioRunDetails {
44
46
  };
45
47
  stepRuns?: ScenarioRunStepReport[];
46
48
  }
49
+ /** One firing of a scenario trigger: what `POST /scenarios/{id}/run` returns (not a scenario run). */
50
+ export interface ScenarioTriggerFire {
51
+ triggerId: string;
52
+ triggerRunId: string;
53
+ cancelled: boolean;
54
+ }
55
+ /** A trigger fire resolved against the scenario run it started (if any). */
56
+ export interface ScenarioTriggerRunState extends ScenarioTriggerFire {
57
+ state: "PENDING" | "RUNNING" | "FINISHED" | "CANCELLED";
58
+ /** The scenario run id (what get-run and log take), once DSS started the run. */
59
+ runId?: string;
60
+ outcome?: string;
61
+ start?: number;
62
+ }
63
+ /** Test-scenario report text (JUnit XML), byte-bounded. */
64
+ export interface ScenarioRunReport {
65
+ text: string;
66
+ truncated: boolean;
67
+ maxBytes: number;
68
+ }
69
+ export type ScenarioListKind = "trigger" | "reporter";
70
+ /** Outcome of adding or removing one trigger/reporter. */
71
+ export interface ScenarioListEdit {
72
+ kind: ScenarioListKind;
73
+ /** Position of the added element (as stored) or of the removed one. */
74
+ index: number;
75
+ /** The element as DSS stored it (added) or as it was (removed). */
76
+ item: unknown;
77
+ /** Elements left in the list. */
78
+ count: number;
79
+ }
80
+ export interface ScenarioRunAsResult {
81
+ scenarioId: string;
82
+ /** The explicit run-as login, or null when the scenario runs as its last editor. */
83
+ runAsUser: string | null;
84
+ /** Who the scenario actually runs as (dataikuapi `effective_run_as`). */
85
+ effectiveRunAs?: string;
86
+ previousRunAsUser?: string;
87
+ }
47
88
  /** Bounded log retrieval result: text plus whether the byte cap cut it. */
48
89
  export interface ScenarioRunLog {
49
90
  text: string;
@@ -120,6 +161,8 @@ export interface ScenarioScriptRunResult {
120
161
  /** Observable throwaway-scenario cleanup outcome; never silently swallowed here. */
121
162
  cleanup: ScenarioScriptRunCleanup;
122
163
  }
164
+ /** Who a scenario really runs as: its `runAsUser`, else the last editor (dataikuapi `effective_run_as`). */
165
+ export declare function effectiveRunAs(scenario: Record<string, unknown>): string | undefined;
123
166
  export declare function normalizeScenarioUpdateData(data: Record<string, unknown>): {
124
167
  normalizedData: Record<string, unknown>;
125
168
  normalization: ScenarioUpdateNormalization[];
@@ -138,14 +181,23 @@ export declare class ScenariosResource extends BaseResource {
138
181
  data?: Record<string, unknown>;
139
182
  projectKey?: string;
140
183
  }): Promise<void>;
141
- /** Trigger a scenario run. */
142
- run(scenarioId: string, projectKey?: string): Promise<{
143
- runId: string;
144
- }>;
184
+ /**
185
+ * Trigger a scenario run. `params` is sent as the POST body (dataikuapi
186
+ * `run(params)`) and reaches the run as its trigger parameters. The result
187
+ * identifies the TRIGGER fire, not a scenario run: resolve it with
188
+ * {@link getRunForTrigger}. `cancelled` is already true when DSS refused the
189
+ * fire because the scenario is running.
190
+ */
191
+ run(scenarioId: string, opts?: {
192
+ params?: Record<string, unknown>;
193
+ projectKey?: string;
194
+ }): Promise<ScenarioTriggerFire>;
145
195
  /** Get the light/status view of a scenario. */
146
196
  status(scenarioId: string, projectKey?: string): Promise<ScenarioStatus>;
147
197
  /** Merge-update a scenario's definition, then refetch and verify requested fields persisted. */
148
198
  update(scenarioId: string, data: Record<string, unknown>, projectKey?: string): Promise<ScenarioUpdateResult>;
199
+ /** PUT the merge of `data` into an already-read scenario, refetch, and verify it persisted. */
200
+ private applyUpdate;
149
201
  /** Delete a scenario. */
150
202
  delete(scenarioId: string, projectKey?: string): Promise<void>;
151
203
  /**
@@ -153,6 +205,7 @@ export declare class ScenariosResource extends BaseResource {
153
205
  * Returns `{ success: false, timedOut: true }` on timeout rather than throwing.
154
206
  */
155
207
  runAndWait(scenarioId: string, opts?: {
208
+ params?: Record<string, unknown>;
156
209
  pollIntervalMs?: number;
157
210
  timeoutMs?: number;
158
211
  projectKey?: string;
@@ -177,11 +230,46 @@ export declare class ScenariosResource extends BaseResource {
177
230
  * actually stop, so this does not wait for termination.
178
231
  */
179
232
  abort(scenarioId: string, projectKey?: string): Promise<void>;
180
- /** Get the raw last-runs list (GET /scenarios/{id}/get-last-runs/?limit=N). */
233
+ /**
234
+ * Get the raw last-runs list (GET /scenarios/{id}/get-last-runs/?limit=N).
235
+ * `onlyFinished` drops runs still in progress (DSS `onlyFinishedRuns`).
236
+ */
181
237
  getLastRuns(scenarioId: string, opts?: {
182
238
  limit?: number;
239
+ onlyFinished?: boolean;
183
240
  projectKey?: string;
184
241
  }): Promise<ScenarioRunSummary[]>;
242
+ /**
243
+ * Runs that started between two days (GET /scenarios/{id}/get-runs-by-date).
244
+ * Dates are `YYYY-MM-DD` or ISO date-times (the time part is dropped, as
245
+ * dataikuapi does). DSS treats `to` as exclusive (verified live: `from=to=today`
246
+ * is empty and today's runs need `to=tomorrow`), so it defaults to tomorrow
247
+ * rather than dataikuapi's today, which would leave today's runs out.
248
+ */
249
+ getRunsByDate(scenarioId: string, opts: {
250
+ from: string;
251
+ to?: string;
252
+ projectKey?: string;
253
+ }): Promise<ScenarioRunSummary[]>;
254
+ /**
255
+ * JUnit XML report of a test scenario run (GET /scenarios/{id}/{runId}/scenario-run-report),
256
+ * or of one step with `stepId` (…/step-run-report?stepId=). DSS only builds
257
+ * reports for scenarios marked as test; the text is byte-bounded like the log.
258
+ */
259
+ getRunReport(scenarioId: string, runId: string, opts?: {
260
+ stepId?: string;
261
+ maxBytes?: number;
262
+ projectKey?: string;
263
+ }): Promise<ScenarioRunReport>;
264
+ /**
265
+ * Resolve a trigger run (what `run` returns) to its scenario run: the scenario
266
+ * run id once DSS started one, or `CANCELLED` when DSS cancelled the fire (a
267
+ * run already in progress suppresses it) and no run will ever exist.
268
+ */
269
+ getRunForTrigger(scenarioId: string, triggerRunId: string, opts?: {
270
+ triggerId?: string;
271
+ projectKey?: string;
272
+ }): Promise<ScenarioTriggerRunState>;
185
273
  /** Get the details of a specific run (GET /scenarios/{id}/{runId}). */
186
274
  getRunDetails(scenarioId: string, runId: string, opts?: {
187
275
  projectKey?: string;
@@ -216,14 +304,43 @@ export declare class ScenariosResource extends BaseResource {
216
304
  setActive(scenarioId: string, active: boolean, opts?: {
217
305
  projectKey?: string;
218
306
  }): Promise<ScenarioActiveUpdateResult>;
307
+ /** The scenario's triggers or reporters, in DSS order (GET /scenarios/{id}/). */
308
+ listItems(kind: ScenarioListKind, scenarioId: string, opts?: {
309
+ projectKey?: string;
310
+ }): Promise<unknown[]>;
311
+ private getSettings;
312
+ /**
313
+ * Append a trigger or reporter. `triggers`/`reporters` are arrays, which a merge
314
+ * update replaces wholesale, so this reads the current list and writes it back
315
+ * with the new element last (then verifies the stored result).
316
+ */
317
+ addItem(kind: ScenarioListKind, scenarioId: string, item: Record<string, unknown>, opts?: {
318
+ projectKey?: string;
319
+ }): Promise<ScenarioListEdit>;
320
+ /** Remove the trigger or reporter selected by `--index`/`--name`, keeping every other element. */
321
+ removeItem(kind: ScenarioListKind, scenarioId: string, selector: ScenarioListSelector, opts?: {
322
+ projectKey?: string;
323
+ }): Promise<ScenarioListEdit>;
324
+ /**
325
+ * Set (`user`) or clear (`null`) the scenario's `runAsUser` (admin only for a
326
+ * non-null value). Clearing deletes the key, which a merge update cannot do.
327
+ */
328
+ setRunAs(scenarioId: string, user: string | null, opts?: {
329
+ projectKey?: string;
330
+ }): Promise<ScenarioRunAsResult>;
219
331
  /**
220
- * Poll get-run-for-trigger until the triggered run reports an outcome or the
221
- * deadline elapses. Shared by runAndWait and runScript: POST /run/ returns a
222
- * TRIGGER run id, which differs from the actual scenario run id, so the real
223
- * run is resolved via get-run-for-trigger (the trigger id never matches
224
- * lastRun and would misreport a completed scenario as a timeout).
332
+ * Poll get-run-for-trigger until the triggered run reports an outcome, DSS
333
+ * cancels the trigger fire, or the deadline elapses. Shared by runAndWait and
334
+ * runScript: POST /run/ returns a TRIGGER run id, which differs from the actual
335
+ * scenario run id, so the real run is resolved via get-run-for-trigger (the
336
+ * trigger id never matches lastRun and would misreport a completed scenario as
337
+ * a timeout). While no scenario run exists yet, the trigger fire itself is read:
338
+ * DSS cancels a fire when the scenario is already running (or a newer fire
339
+ * replaces it), and then no run will ever appear.
225
340
  */
226
341
  private trackTriggerRun;
342
+ /** Whether DSS cancelled this trigger fire (GET /scenarios/trigger/{id}/{triggerId}?triggerRunId=). */
343
+ private isTriggerFireCancelled;
227
344
  /**
228
345
  * Best-effort step outcome summaries from the run report
229
346
  * (GET /scenarios/{id}/{runId}). Never fails the wait: an unavailable
@@ -4,16 +4,20 @@ import { deepMerge, } from "../utils/deep-merge.js";
4
4
  import { computeNextPollDelayMs, isRequestDeadlineError, } from "../utils/polling.js";
5
5
  import { isRecord, } from "../utils/records.js";
6
6
  import { BaseResource, requireArrayResponse, requireNonEmpty, } from "./base.js";
7
+ import { selectScenarioListIndex, } from "./scenario-authoring.js";
8
+ /** DSS keeps triggers and reporters at the top level of the scenario (as dataikuapi does), not under params. */
7
9
  export const SCENARIO_CANONICAL_EDITABLE_FIELDS = [
8
10
  "params.steps",
9
- "params.triggers",
10
- "params.reporters",
11
+ "triggers",
12
+ "reporters",
11
13
  "params.customScript",
12
14
  "active",
13
15
  "name",
14
16
  ];
15
17
  /** Default byte cap applied when fetching a scenario run log. */
16
18
  export const DEFAULT_SCENARIO_MAX_LOG_BYTES = 1_048_576;
19
+ /** Where each list lives on the scenario object (top level, as dataikuapi). */
20
+ const SCENARIO_LIST_FIELDS = { trigger: "triggers", reporter: "reporters", };
17
21
  /**
18
22
  * Wraps a primary code-run failure so the original error (and its taxonomy:
19
23
  * category, exit-code semantics) stays intact while the concurrent throwaway
@@ -48,11 +52,55 @@ function triggerRunIdOf(trigger, scenarioId) {
48
52
  }
49
53
  return runId;
50
54
  }
51
- function triggerQueryOf(trigger) {
52
- const triggerObj = trigger.trigger;
53
- const triggerId = triggerObj?.id ?? "manual";
54
- const triggerRunId = triggerRunIdOf(trigger);
55
- return `triggerId=${encodeURIComponent(triggerId)}&triggerRunId=${encodeURIComponent(triggerRunId)}`;
55
+ /** Trigger id (`manual` for API runs), trigger run id and cancellation flag of a POST /run response. */
56
+ function triggerFireOf(trigger, scenarioId) {
57
+ const triggerId = isRecord(trigger.trigger) && typeof trigger.trigger.id === "string"
58
+ ? trigger.trigger.id
59
+ : "manual";
60
+ return {
61
+ triggerId,
62
+ triggerRunId: triggerRunIdOf(trigger, scenarioId),
63
+ cancelled: trigger.cancelled === true,
64
+ };
65
+ }
66
+ /** The `triggers`/`reporters` array of a scenario object; absent means empty, any other shape is an error. */
67
+ function scenarioListField(scenario, kind) {
68
+ const items = scenario[SCENARIO_LIST_FIELDS[kind]];
69
+ return items === undefined ? [] : requireArrayResponse(items, `scenario ${kind}s`);
70
+ }
71
+ /**
72
+ * DSS stamps `params.lastUpdate` on every temporal trigger each time the scenario
73
+ * is saved, so echoing the value it last stored can never be read back (the
74
+ * verification after a trigger edit would report every trigger as unpersisted).
75
+ * Omitting it lets DSS stamp afresh, as it does for the other saved triggers.
76
+ */
77
+ function withoutServerStamp(item) {
78
+ if (!isRecord(item) || !isRecord(item.params) || !("lastUpdate" in item.params))
79
+ return item;
80
+ const { lastUpdate: _stamp, ...params } = item.params;
81
+ return { ...item, params, };
82
+ }
83
+ /** `YYYY-MM-DD` day of a date or ISO date-time (the day DSS filters runs by); anything else is a usage error. */
84
+ function scenarioDay(value, name) {
85
+ const match = /^(\d{4})-(\d{2})-(\d{2})(?:[T ].*)?$/.exec(value);
86
+ if (match) {
87
+ const day = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])));
88
+ if (day.getUTCMonth() === Number(match[2]) - 1 && day.getUTCDate() === Number(match[3])) {
89
+ return `${match[1]}-${match[2]}-${match[3]}`;
90
+ }
91
+ }
92
+ throw new ClientValidationError(`${name} must be a YYYY-MM-DD date or ISO date-time, got ${JSON.stringify(value)}.`, "invalid_flag_value");
93
+ }
94
+ /** Who a scenario really runs as: its `runAsUser`, else the last editor (dataikuapi `effective_run_as`). */
95
+ export function effectiveRunAs(scenario) {
96
+ if (typeof scenario.runAsUser === "string")
97
+ return scenario.runAsUser;
98
+ for (const tag of [scenario.versionTag, scenario.creationTag,]) {
99
+ if (isRecord(tag) && isRecord(tag.lastModifiedBy) && typeof tag.lastModifiedBy.login === "string") {
100
+ return tag.lastModifiedBy.login;
101
+ }
102
+ }
103
+ return undefined;
56
104
  }
57
105
  /** A finite number, or undefined for anything else (absent/malformed JSON fields). */
58
106
  function optionalNumber(value) {
@@ -320,12 +368,37 @@ export function scenarioUpdatePreview(current, data) {
320
368
  }
321
369
  function scenarioUpdateVerificationError(mismatches, normalization) {
322
370
  const mismatchPaths = mismatches.map((mismatch) => mismatch.path).join(", ");
323
- return new DataikuError(400, "Scenario Update Verification Failed", JSON.stringify({
371
+ const body = {
324
372
  message: `Scenario update did not persist requested fields after refetch: ${mismatchPaths}`,
325
373
  mismatches,
326
374
  canonicalEditableFields: SCENARIO_CANONICAL_EDITABLE_FIELDS,
327
375
  ...(normalization.length > 0 ? { normalization, } : {}),
328
- }));
376
+ };
377
+ const error = new DataikuError(400, "Scenario Update Verification Failed", JSON.stringify(body));
378
+ // CLI envelopes show `details`, never the body. Only paths go there: reporter and step
379
+ // values can hold webhook URLs, tokens, or credentials.
380
+ error.details = {
381
+ mismatches: mismatches.map((mismatch) => ({
382
+ path: mismatch.path,
383
+ differingPaths: differingLeafPaths(mismatch.expected, mismatch.actual, mismatch.path),
384
+ })),
385
+ canonicalEditableFields: SCENARIO_CANONICAL_EDITABLE_FIELDS,
386
+ };
387
+ return error;
388
+ }
389
+ /** Paths under `path` where the sent value is missing or different in what DSS stored (e.g. `reporters.0.messaging.channelId`). */
390
+ function differingLeafPaths(expected, actual, path) {
391
+ if (Array.isArray(expected)) {
392
+ if (!Array.isArray(actual))
393
+ return [path,];
394
+ return expected.flatMap((item, index) => differingLeafPaths(item, actual[index], `${path}.${index}`));
395
+ }
396
+ if (isRecord(expected)) {
397
+ if (!isRecord(actual))
398
+ return [path,];
399
+ return Object.entries(expected).flatMap(([key, value,]) => differingLeafPaths(value, actual[key], `${path}.${key}`));
400
+ }
401
+ return jsonValueEqual(expected, actual) ? [] : [path,];
329
402
  }
330
403
  export class ScenariosResource extends BaseResource {
331
404
  /** List all scenarios in a project. */
@@ -348,16 +421,24 @@ export class ScenariosResource extends BaseResource {
348
421
  name,
349
422
  projectKey: pk,
350
423
  type: scenarioType,
351
- params: scenarioType === "step_based" ? { steps: [], triggers: [], reporters: [], } : {},
424
+ ...(scenarioType === "step_based"
425
+ ? { params: { steps: [], }, triggers: [], reporters: [], }
426
+ : { params: {}, }),
352
427
  ...opts?.data,
353
428
  };
354
429
  await this.client.post(`/public/api/projects/${this.enc(opts?.projectKey)}/scenarios/`, body);
355
430
  }
356
- /** Trigger a scenario run. */
357
- async run(scenarioId, projectKey) {
431
+ /**
432
+ * Trigger a scenario run. `params` is sent as the POST body (dataikuapi
433
+ * `run(params)`) and reaches the run as its trigger parameters. The result
434
+ * identifies the TRIGGER fire, not a scenario run: resolve it with
435
+ * {@link getRunForTrigger}. `cancelled` is already true when DSS refused the
436
+ * fire because the scenario is running.
437
+ */
438
+ async run(scenarioId, opts) {
358
439
  const scEnc = encodeURIComponent(requireNonEmpty(scenarioId, "scenarioId"));
359
- const result = await this.client.post(`/public/api/projects/${this.enc(projectKey)}/scenarios/${scEnc}/run/`, {});
360
- return { runId: triggerRunIdOf(result, scenarioId), };
440
+ const result = await this.client.post(`/public/api/projects/${this.enc(opts?.projectKey)}/scenarios/${scEnc}/run/`, opts?.params ?? {});
441
+ return triggerFireOf(result, scenarioId);
361
442
  }
362
443
  /** Get the light/status view of a scenario. */
363
444
  async status(scenarioId, projectKey) {
@@ -367,12 +448,15 @@ export class ScenariosResource extends BaseResource {
367
448
  }
368
449
  /** Merge-update a scenario's definition, then refetch and verify requested fields persisted. */
369
450
  async update(scenarioId, data, projectKey) {
370
- const scEnc = encodeURIComponent(scenarioId);
371
- const pkEnc = this.enc(projectKey);
372
- const current = await this.client.get(`/public/api/projects/${pkEnc}/scenarios/${scEnc}/`);
451
+ const current = await this.client.get(`/public/api/projects/${this.enc(projectKey)}/scenarios/${encodeURIComponent(scenarioId)}/`);
452
+ return this.applyUpdate(scenarioId, current, data, projectKey);
453
+ }
454
+ /** PUT the merge of `data` into an already-read scenario, refetch, and verify it persisted. */
455
+ async applyUpdate(scenarioId, current, data, projectKey) {
456
+ const path = `/public/api/projects/${this.enc(projectKey)}/scenarios/${encodeURIComponent(scenarioId)}/`;
373
457
  const preview = scenarioUpdatePreview(current, data);
374
- await this.client.put(`/public/api/projects/${pkEnc}/scenarios/${scEnc}/`, preview.next);
375
- const after = await this.client.get(`/public/api/projects/${pkEnc}/scenarios/${scEnc}/`);
458
+ await this.client.put(path, preview.next);
459
+ const after = await this.client.get(path);
376
460
  const mismatches = scenarioFieldMismatches(after, preview.next, preview.normalizedData);
377
461
  if (mismatches.length > 0) {
378
462
  throw scenarioUpdateVerificationError(mismatches, preview.normalization);
@@ -406,14 +490,15 @@ export class ScenariosResource extends BaseResource {
406
490
  // POST /run/ returns a TRIGGER run id, which differs from the actual scenario
407
491
  // run id; resolve the real run via get-run-for-trigger so a completed scenario
408
492
  // is not misreported as a timeout (the trigger id never matches lastRun).
409
- const trigger = await this.client.post(`${base}/run/`, {});
410
- const tracked = await this.trackTriggerRun(scenarioId, base, trigger, {
493
+ const trigger = await this.client.post(`${base}/run/`, opts?.params ?? {});
494
+ const fire = triggerFireOf(trigger, scenarioId);
495
+ const tracked = await this.trackTriggerRun(scenarioId, opts?.projectKey, trigger, {
411
496
  startedAt,
412
497
  baseIntervalMs,
413
498
  adaptivePolling,
414
499
  timeout,
415
500
  });
416
- const resolvedRunId = tracked.runId || triggerRunIdOf(trigger);
501
+ const resolvedRunId = tracked.runId || fire.triggerRunId;
417
502
  const steps = tracked.timedOut || !tracked.runId
418
503
  ? undefined
419
504
  : await this.runStepSummaries(base, tracked.runId);
@@ -424,9 +509,10 @@ export class ScenariosResource extends BaseResource {
424
509
  success: tracked.outcome === "SUCCESS",
425
510
  elapsedMs: Date.now() - startedAt,
426
511
  pollCount: tracked.pollCount,
427
- ...(triggerRunIdOf(trigger) !== resolvedRunId
428
- ? { triggerRunId: triggerRunIdOf(trigger), }
512
+ ...(fire.triggerRunId !== resolvedRunId || tracked.cancelled
513
+ ? { triggerRunId: fire.triggerRunId, }
429
514
  : {}),
515
+ ...(tracked.cancelled ? { cancelled: true, } : {}),
430
516
  ...(tracked.timedOut ? { timedOut: true, } : {}),
431
517
  ...(steps ? { steps, } : {}),
432
518
  };
@@ -470,7 +556,7 @@ export class ScenariosResource extends BaseResource {
470
556
  extension: "py",
471
557
  });
472
558
  const trigger = await this.client.post(`${base}/run/`, {});
473
- const tracked = await this.trackTriggerRun(scenarioId, base, trigger, {
559
+ const tracked = await this.trackTriggerRun(scenarioId, opts?.projectKey, trigger, {
474
560
  startedAt,
475
561
  baseIntervalMs,
476
562
  adaptivePolling,
@@ -535,16 +621,81 @@ export class ScenariosResource extends BaseResource {
535
621
  const scEnc = encodeURIComponent(scenarioId);
536
622
  await this.client.post(`/public/api/projects/${this.enc(projectKey)}/scenarios/${scEnc}/abort`);
537
623
  }
538
- /** Get the raw last-runs list (GET /scenarios/{id}/get-last-runs/?limit=N). */
624
+ /**
625
+ * Get the raw last-runs list (GET /scenarios/{id}/get-last-runs/?limit=N).
626
+ * `onlyFinished` drops runs still in progress (DSS `onlyFinishedRuns`).
627
+ */
539
628
  async getLastRuns(scenarioId, opts) {
540
629
  if (opts?.limit !== undefined && (!Number.isInteger(opts.limit) || opts.limit < 1)) {
541
630
  throw new ClientValidationError(`limit must be a positive integer, got ${opts.limit}.`);
542
631
  }
543
632
  const scEnc = encodeURIComponent(scenarioId);
544
- const query = opts?.limit !== undefined ? `?limit=${opts.limit}` : "";
545
- const raw = await this.client.get(`/public/api/projects/${this.enc(opts?.projectKey)}/scenarios/${scEnc}/get-last-runs/${query}`);
633
+ const query = new URLSearchParams();
634
+ if (opts?.limit !== undefined)
635
+ query.set("limit", String(opts.limit));
636
+ if (opts?.onlyFinished === true)
637
+ query.set("onlyFinishedRuns", "true");
638
+ const suffix = query.size > 0 ? `?${query}` : "";
639
+ const raw = await this.client.get(`/public/api/projects/${this.enc(opts?.projectKey)}/scenarios/${scEnc}/get-last-runs/${suffix}`);
546
640
  return requireArrayResponse(raw, "scenarios.getLastRuns").map(scenarioRunSummaryFromRaw);
547
641
  }
642
+ /**
643
+ * Runs that started between two days (GET /scenarios/{id}/get-runs-by-date).
644
+ * Dates are `YYYY-MM-DD` or ISO date-times (the time part is dropped, as
645
+ * dataikuapi does). DSS treats `to` as exclusive (verified live: `from=to=today`
646
+ * is empty and today's runs need `to=tomorrow`), so it defaults to tomorrow
647
+ * rather than dataikuapi's today, which would leave today's runs out.
648
+ */
649
+ async getRunsByDate(scenarioId, opts) {
650
+ const query = new URLSearchParams({
651
+ fromDate: scenarioDay(opts.from, "from"),
652
+ toDate: scenarioDay(opts.to ?? new Date(Date.now() + 86_400_000).toLocaleDateString("en-CA"), "to"),
653
+ });
654
+ const raw = await this.client.get(`/public/api/projects/${this.enc(opts.projectKey)}/scenarios/${encodeURIComponent(requireNonEmpty(scenarioId, "scenarioId"))}/get-runs-by-date?${query}`);
655
+ return requireArrayResponse(raw, "scenarios.getRunsByDate").map(scenarioRunSummaryFromRaw);
656
+ }
657
+ /**
658
+ * JUnit XML report of a test scenario run (GET /scenarios/{id}/{runId}/scenario-run-report),
659
+ * or of one step with `stepId` (…/step-run-report?stepId=). DSS only builds
660
+ * reports for scenarios marked as test; the text is byte-bounded like the log.
661
+ */
662
+ async getRunReport(scenarioId, runId, opts) {
663
+ if (opts?.stepId !== undefined)
664
+ requireNonEmpty(opts.stepId, "stepId");
665
+ const maxBytes = Math.max(0, Math.floor(opts?.maxBytes ?? DEFAULT_SCENARIO_MAX_LOG_BYTES));
666
+ const runPath = `/public/api/projects/${this.enc(opts?.projectKey)}/scenarios/${encodeURIComponent(requireNonEmpty(scenarioId, "scenarioId"))}/${encodeURIComponent(requireNonEmpty(runId, "runId"))}`;
667
+ const { text, truncated, } = await this.client.getTextLimited(opts?.stepId === undefined
668
+ ? `${runPath}/scenario-run-report`
669
+ : `${runPath}/step-run-report?stepId=${encodeURIComponent(opts.stepId)}`, maxBytes);
670
+ return { text, truncated, maxBytes, };
671
+ }
672
+ /**
673
+ * Resolve a trigger run (what `run` returns) to its scenario run: the scenario
674
+ * run id once DSS started one, or `CANCELLED` when DSS cancelled the fire (a
675
+ * run already in progress suppresses it) and no run will ever exist.
676
+ */
677
+ async getRunForTrigger(scenarioId, triggerRunId, opts) {
678
+ const pkEnc = this.enc(opts?.projectKey);
679
+ const triggerId = requireNonEmpty(opts?.triggerId ?? "manual", "triggerId");
680
+ const fire = { triggerId, triggerRunId: requireNonEmpty(triggerRunId, "triggerRunId"), };
681
+ const run = await this.client.get(`/public/api/projects/${pkEnc}/scenarios/${encodeURIComponent(requireNonEmpty(scenarioId, "scenarioId"))}/get-run-for-trigger?triggerId=${encodeURIComponent(triggerId)}&triggerRunId=${encodeURIComponent(fire.triggerRunId)}`);
682
+ if (isRecord(run.scenarioRun)) {
683
+ const outcome = isRecord(run.scenarioRun.result) && typeof run.scenarioRun.result.outcome === "string"
684
+ ? run.scenarioRun.result.outcome
685
+ : undefined;
686
+ const start = optionalNumber(run.scenarioRun.start);
687
+ return {
688
+ ...fire,
689
+ cancelled: false,
690
+ state: outcome === undefined ? "RUNNING" : "FINISHED",
691
+ ...(typeof run.scenarioRun.runId === "string" ? { runId: run.scenarioRun.runId, } : {}),
692
+ ...(outcome === undefined ? {} : { outcome, }),
693
+ ...(start === undefined ? {} : { start, }),
694
+ };
695
+ }
696
+ const cancelled = await this.isTriggerFireCancelled(pkEnc, scenarioId, fire);
697
+ return { ...fire, cancelled, state: cancelled ? "CANCELLED" : "PENDING", };
698
+ }
548
699
  /** Get the details of a specific run (GET /scenarios/{id}/{runId}). */
549
700
  async getRunDetails(scenarioId, runId, opts) {
550
701
  return this.client.get(`/public/api/projects/${this.enc(opts?.projectKey)}/scenarios/${encodeURIComponent(scenarioId)}/${encodeURIComponent(runId)}/`);
@@ -604,34 +755,108 @@ export class ScenariosResource extends BaseResource {
604
755
  status: scenarioLightStatusFromRaw(after),
605
756
  };
606
757
  }
758
+ /** The scenario's triggers or reporters, in DSS order (GET /scenarios/{id}/). */
759
+ async listItems(kind, scenarioId, opts) {
760
+ return scenarioListField(await this.getSettings(scenarioId, opts?.projectKey), kind);
761
+ }
762
+ async getSettings(scenarioId, projectKey) {
763
+ return this.client.get(`/public/api/projects/${this.enc(projectKey)}/scenarios/${encodeURIComponent(requireNonEmpty(scenarioId, "scenarioId"))}/`);
764
+ }
607
765
  /**
608
- * Poll get-run-for-trigger until the triggered run reports an outcome or the
609
- * deadline elapses. Shared by runAndWait and runScript: POST /run/ returns a
610
- * TRIGGER run id, which differs from the actual scenario run id, so the real
611
- * run is resolved via get-run-for-trigger (the trigger id never matches
612
- * lastRun and would misreport a completed scenario as a timeout).
766
+ * Append a trigger or reporter. `triggers`/`reporters` are arrays, which a merge
767
+ * update replaces wholesale, so this reads the current list and writes it back
768
+ * with the new element last (then verifies the stored result).
613
769
  */
614
- async trackTriggerRun(scenarioId, base, trigger, timing) {
615
- triggerRunIdOf(trigger, scenarioId);
616
- const trigQuery = triggerQueryOf(trigger);
770
+ async addItem(kind, scenarioId, item, opts) {
771
+ const current = await this.getSettings(scenarioId, opts?.projectKey);
772
+ const items = scenarioListField(current, kind);
773
+ const result = await this.applyUpdate(scenarioId, current, {
774
+ [SCENARIO_LIST_FIELDS[kind]]: [...items.map(withoutServerStamp), item,],
775
+ }, opts?.projectKey);
776
+ const stored = scenarioListField(result.after, kind);
777
+ return { kind, index: items.length, item: stored[items.length], count: stored.length, };
778
+ }
779
+ /** Remove the trigger or reporter selected by `--index`/`--name`, keeping every other element. */
780
+ async removeItem(kind, scenarioId, selector, opts) {
781
+ const current = await this.getSettings(scenarioId, opts?.projectKey);
782
+ const items = scenarioListField(current, kind);
783
+ const index = selectScenarioListIndex(items, selector, kind);
784
+ const result = await this.applyUpdate(scenarioId, current, {
785
+ [SCENARIO_LIST_FIELDS[kind]]: items.flatMap((existing, position) => position === index ? [] : [withoutServerStamp(existing),]),
786
+ }, opts?.projectKey);
787
+ return {
788
+ kind,
789
+ index,
790
+ item: items[index],
791
+ count: scenarioListField(result.after, kind).length,
792
+ };
793
+ }
794
+ /**
795
+ * Set (`user`) or clear (`null`) the scenario's `runAsUser` (admin only for a
796
+ * non-null value). Clearing deletes the key, which a merge update cannot do.
797
+ */
798
+ async setRunAs(scenarioId, user, opts) {
799
+ const path = `/public/api/projects/${this.enc(opts?.projectKey)}/scenarios/${encodeURIComponent(requireNonEmpty(scenarioId, "scenarioId"))}/`;
800
+ const current = await this.client.get(path);
801
+ const { runAsUser: previous, ...withoutRunAs } = current;
802
+ await this.client.put(path, user === null ? withoutRunAs : { ...withoutRunAs, runAsUser: user, });
803
+ const after = await this.client.get(path);
804
+ const stored = typeof after.runAsUser === "string" ? after.runAsUser : null;
805
+ if (stored !== user) {
806
+ const error = new DataikuError(400, "Scenario Update Verification Failed", `Scenario run-as did not persist after refetch: expected ${user === null ? "no runAsUser" : "the requested user"}.`);
807
+ error.details = { mismatches: [{ path: "runAsUser", },], };
808
+ throw error;
809
+ }
810
+ return {
811
+ scenarioId,
812
+ runAsUser: stored,
813
+ effectiveRunAs: effectiveRunAs(after),
814
+ ...(typeof previous === "string" ? { previousRunAsUser: previous, } : {}),
815
+ };
816
+ }
817
+ /**
818
+ * Poll get-run-for-trigger until the triggered run reports an outcome, DSS
819
+ * cancels the trigger fire, or the deadline elapses. Shared by runAndWait and
820
+ * runScript: POST /run/ returns a TRIGGER run id, which differs from the actual
821
+ * scenario run id, so the real run is resolved via get-run-for-trigger (the
822
+ * trigger id never matches lastRun and would misreport a completed scenario as
823
+ * a timeout). While no scenario run exists yet, the trigger fire itself is read:
824
+ * DSS cancels a fire when the scenario is already running (or a newer fire
825
+ * replaces it), and then no run will ever appear.
826
+ */
827
+ async trackTriggerRun(scenarioId, projectKey, trigger, timing) {
828
+ const fire = triggerFireOf(trigger, scenarioId);
829
+ if (fire.cancelled) {
830
+ return { runId: "", outcome: "CANCELLED", timedOut: false, cancelled: true, pollCount: 0, };
831
+ }
832
+ const pkEnc = this.enc(projectKey);
833
+ const base = `/public/api/projects/${pkEnc}/scenarios/${encodeURIComponent(scenarioId)}`;
834
+ const trigQuery = `triggerId=${encodeURIComponent(fire.triggerId)}&triggerRunId=${encodeURIComponent(fire.triggerRunId)}`;
835
+ const remainingMs = () => Math.max(1, timing.timeout - (Date.now() - timing.startedAt));
617
836
  let runId = "";
618
837
  let pollCount = 0;
619
838
  while (true) {
620
839
  if (Date.now() - timing.startedAt >= timing.timeout) {
621
- return { runId, outcome: "TIMEOUT", timedOut: true, pollCount, };
840
+ return { runId, outcome: "TIMEOUT", timedOut: true, cancelled: false, pollCount, };
622
841
  }
623
842
  pollCount += 1;
624
- // Budget the per-poll GET by the remaining wait so a stalling
843
+ // Budget the per-poll GETs by the remaining wait so a stalling
625
844
  // trigger-status endpoint cannot defeat the overall run deadline.
626
- const remainingMs = Math.max(1, timing.timeout - (Date.now() - timing.startedAt));
627
845
  let run;
846
+ let cancelled = false;
628
847
  try {
629
- run = await this.client.get(`${base}/get-run-for-trigger?${trigQuery}`, { timeoutMs: remainingMs, });
848
+ run = await this.client.get(`${base}/get-run-for-trigger?${trigQuery}`, { timeoutMs: remainingMs(), });
849
+ if (!isRecord(run.scenarioRun)) {
850
+ cancelled = await this.isTriggerFireCancelled(pkEnc, scenarioId, fire, remainingMs());
851
+ }
630
852
  }
631
853
  catch (error) {
632
854
  if (!isRequestDeadlineError(error, timing.startedAt + timing.timeout))
633
855
  throw error;
634
- return { runId, outcome: "TIMEOUT", timedOut: true, pollCount, };
856
+ return { runId, outcome: "TIMEOUT", timedOut: true, cancelled: false, pollCount, };
857
+ }
858
+ if (cancelled) {
859
+ return { runId, outcome: "CANCELLED", timedOut: false, cancelled: true, pollCount, };
635
860
  }
636
861
  const scenarioRun = run.scenarioRun;
637
862
  if (scenarioRun) {
@@ -639,7 +864,7 @@ export class ScenariosResource extends BaseResource {
639
864
  const result = scenarioRun.result;
640
865
  const finished = result?.outcome;
641
866
  if (finished) {
642
- return { runId, outcome: finished, timedOut: false, pollCount, };
867
+ return { runId, outcome: finished, timedOut: false, cancelled: false, pollCount, };
643
868
  }
644
869
  }
645
870
  const nextDelayMs = computeNextPollDelayMs({
@@ -648,10 +873,15 @@ export class ScenariosResource extends BaseResource {
648
873
  adaptiveEnabled: timing.adaptivePolling,
649
874
  });
650
875
  const { promise, resolve, } = Promise.withResolvers();
651
- setTimeout(resolve, Math.min(nextDelayMs, Math.max(1, timing.timeout - (Date.now() - timing.startedAt))));
876
+ setTimeout(resolve, Math.min(nextDelayMs, remainingMs()));
652
877
  await promise;
653
878
  }
654
879
  }
880
+ /** Whether DSS cancelled this trigger fire (GET /scenarios/trigger/{id}/{triggerId}?triggerRunId=). */
881
+ async isTriggerFireCancelled(pkEnc, scenarioId, fire, timeoutMs) {
882
+ const raw = await this.client.get(`/public/api/projects/${pkEnc}/scenarios/trigger/${encodeURIComponent(scenarioId)}/${encodeURIComponent(fire.triggerId)}?triggerRunId=${encodeURIComponent(fire.triggerRunId)}`, timeoutMs === undefined ? undefined : { timeoutMs, });
883
+ return raw.cancelled === true;
884
+ }
655
885
  /**
656
886
  * Best-effort step outcome summaries from the run report
657
887
  * (GET /scenarios/{id}/{runId}). Never fails the wait: an unavailable