dataiku-sdk 4.1.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 (46) hide show
  1. package/dist/build-metadata.json +1 -1
  2. package/dist/packages/types/src/index.d.ts +2 -0
  3. package/dist/packages/types/src/index.js +2 -0
  4. package/dist/src/cli/command-syntax.json +2426 -1262
  5. package/dist/src/cli/commands/future.js +7 -2
  6. package/dist/src/cli/commands/index.js +4 -0
  7. package/dist/src/cli/commands/messaging-channel.d.ts +17 -0
  8. package/dist/src/cli/commands/messaging-channel.js +385 -0
  9. package/dist/src/cli/commands/project-deployer.js +33 -0
  10. package/dist/src/cli/commands/project.js +30 -0
  11. package/dist/src/cli/commands/scenario.d.ts +6 -0
  12. package/dist/src/cli/commands/scenario.js +423 -13
  13. package/dist/src/cli/contract.js +17 -2
  14. package/dist/src/cli/flags.js +34 -0
  15. package/dist/src/cli/helpers/cleanup.js +11 -0
  16. package/dist/src/cli/list-shapes.js +5 -0
  17. package/dist/src/cli/plans.js +94 -2
  18. package/dist/src/cli/read-shapes.js +2 -1
  19. package/dist/src/cli/result-shapes.js +1 -0
  20. package/dist/src/cli/usage.js +7 -0
  21. package/dist/src/cli/version.d.ts +2 -2
  22. package/dist/src/cli/version.js +2 -2
  23. package/dist/src/client.d.ts +11 -0
  24. package/dist/src/client.js +21 -2
  25. package/dist/src/generated/action-output-schemas.json +82 -0
  26. package/dist/src/index.d.ts +1 -0
  27. package/dist/src/index.js +1 -0
  28. package/dist/src/resources/bundles.d.ts +18 -0
  29. package/dist/src/resources/bundles.js +26 -0
  30. package/dist/src/resources/futures.d.ts +3 -0
  31. package/dist/src/resources/futures.js +5 -1
  32. package/dist/src/resources/messaging-channels.d.ts +83 -0
  33. package/dist/src/resources/messaging-channels.js +108 -0
  34. package/dist/src/resources/projects.d.ts +24 -0
  35. package/dist/src/resources/projects.js +36 -0
  36. package/dist/src/resources/scenario-authoring.d.ts +52 -0
  37. package/dist/src/resources/scenario-authoring.js +216 -0
  38. package/dist/src/resources/scenarios.d.ts +128 -11
  39. package/dist/src/resources/scenarios.js +273 -43
  40. package/dist/src/utils/messaging-channel-secrets.d.ts +17 -0
  41. package/dist/src/utils/messaging-channel-secrets.js +114 -0
  42. package/package.json +1 -1
  43. package/plugin.json +1 -1
  44. package/skills/dataiku-dss/SKILL.md +4 -3
  45. package/skills/dataiku-dss/references/notifications.md +29 -0
  46. package/skills/dataiku-dss/references/troubleshooting.md +1 -1
@@ -0,0 +1,108 @@
1
+ import { readFile, } from "node:fs/promises";
2
+ import { basename, } from "node:path";
3
+ import { ClientValidationError, } from "../errors.js";
4
+ import { BaseResource, requireArrayResponse, requireNonEmpty, requireObject, } from "./base.js";
5
+ /** The JSON `message` part of the send request, shaped like the official Python client's payload. */
6
+ export function mailMessagePayload(message) {
7
+ return {
8
+ from: message.from ?? null,
9
+ to: message.to,
10
+ cc: message.cc ?? null,
11
+ bcc: message.bcc ?? null,
12
+ subject: message.subject,
13
+ body: message.body,
14
+ plainText: message.plainText ?? false,
15
+ };
16
+ }
17
+ /** Query string of the send request. */
18
+ export function mailSendQuery(projectKey, sendAsSingleMessage) {
19
+ return new URLSearchParams({
20
+ projectKey,
21
+ sendAsSingleMessage: String(sendAsSingleMessage ?? false),
22
+ });
23
+ }
24
+ /** The POST body of a channel creation, as the official Python client sends it. */
25
+ export function messagingChannelCreateBody(request) {
26
+ return {
27
+ id: request.id ? requireNonEmpty(request.id, "channelId") : "",
28
+ type: requireNonEmpty(request.type, "type"),
29
+ configuration: request.configuration === undefined
30
+ ? {}
31
+ : requireObject(request.configuration, "configuration"),
32
+ permissions: request.permissions ?? null,
33
+ };
34
+ }
35
+ export class MessagingChannelsResource extends BaseResource {
36
+ /** List messaging channels visible to the caller, optionally filtered by type or family. */
37
+ async list(options = {}) {
38
+ const query = new URLSearchParams();
39
+ if (options.channelType !== undefined)
40
+ query.set("channelType", options.channelType);
41
+ if (options.channelFamily !== undefined)
42
+ query.set("channelFamily", options.channelFamily);
43
+ const suffix = query.size > 0 ? `?${query.toString()}` : "";
44
+ const channels = await this.client.get(`/public/api/messaging-channels/${suffix}`);
45
+ return requireArrayResponse(channels, "messagingChannels.list");
46
+ }
47
+ /** Get one channel (type, family, and for mail channels the sender). */
48
+ async get(channelId) {
49
+ return this.client.get(this.channelPath(channelId));
50
+ }
51
+ /** Create a channel (admin). DSS returns the created channel. */
52
+ async create(request) {
53
+ return this.client.post("/public/api/messaging-channels/", messagingChannelCreateBody(request));
54
+ }
55
+ /** Get a channel's settings, i.e. its configuration (admin). */
56
+ async getSettings(channelId) {
57
+ return this.client.get(`${this.channelPath(channelId)}/settings`);
58
+ }
59
+ /** Replace a channel's settings (admin); pass the object obtained from {@link getSettings}. */
60
+ async saveSettings(channelId, settings) {
61
+ await this.client.putVoid(`${this.channelPath(channelId)}/settings`, requireObject(settings, "settings"));
62
+ }
63
+ /** Get the users and groups allowed to use a channel. */
64
+ async getPermissions(channelId) {
65
+ const permissions = await this.client.get(`${this.channelPath(channelId)}/permissions`);
66
+ return requireArrayResponse(permissions, "messagingChannels.getPermissions");
67
+ }
68
+ /** Replace a channel's permission list. */
69
+ async setPermissions(channelId, permissions) {
70
+ await this.client.putVoid(`${this.channelPath(channelId)}/permissions`, permissions);
71
+ }
72
+ /** Delete a channel (admin). */
73
+ async delete(channelId) {
74
+ await this.client.del(this.channelPath(channelId));
75
+ }
76
+ /**
77
+ * Send an email through a mail-family channel, as a multipart request: a JSON
78
+ * `message` part plus one `attachments` part per file. This sends real mail
79
+ * and is never retried.
80
+ */
81
+ async send(channelId, message, options = {}) {
82
+ const parts = [{
83
+ name: "message",
84
+ blob: new Blob([JSON.stringify(mailMessagePayload(message)),], {
85
+ type: "application/json",
86
+ }),
87
+ fileName: "send-payload.json",
88
+ },];
89
+ for (const attachment of options.attachments ?? []) {
90
+ let blob;
91
+ try {
92
+ blob = new Blob([await readFile(attachment.path),]);
93
+ }
94
+ catch {
95
+ throw new ClientValidationError(`Attachment could not be read: ${attachment.path}`, "validation_failed", "Verify that the path names an existing, readable file.", { path: attachment.path, });
96
+ }
97
+ parts.push({
98
+ name: "attachments",
99
+ blob,
100
+ fileName: attachment.fileName ?? basename(attachment.path),
101
+ });
102
+ }
103
+ await this.client.uploadFormVoid(`${this.channelPath(channelId)}/actions/send`, parts, mailSendQuery(this.resolveProjectKey(options.projectKey), options.sendAsSingleMessage));
104
+ }
105
+ channelPath(channelId) {
106
+ return `/public/api/messaging-channels/${encodeURIComponent(requireNonEmpty(channelId, "channelId"))}`;
107
+ }
108
+ }
@@ -87,6 +87,12 @@ export interface ProjectImportResult extends ProjectImportProcessResult {
87
87
  /** SHA-256 fingerprint of the landed project's creationTag. Present on verified success. */
88
88
  projectIncarnationHash?: string;
89
89
  }
90
+ export interface TestScenarioRunsReportOptions {
91
+ /** Automation-node bundle whose test runs are reported; the active bundle when omitted. */
92
+ bundleId?: string;
93
+ /** `junit` (default) is JUnit XML; `html` is a rendered page. */
94
+ format?: "junit" | "html";
95
+ }
90
96
  export declare class ProjectsResource extends BaseResource {
91
97
  /** Create a new project. */
92
98
  createProject(projectKey: string, name: string, ownerLogin: string, settings?: ProjectLifecycleSettings | null): Promise<Record<string, unknown>>;
@@ -139,6 +145,24 @@ export declare class ProjectsResource extends BaseResource {
139
145
  * covers branches, commits, pulls, and remotes.
140
146
  */
141
147
  pushToGitRemote(remote: string, projectKey?: string): Promise<void>;
148
+ /**
149
+ * Get the testing status of a project: the worst outcome across the last
150
+ * runs of its test scenarios. On an Automation node, `bundleId` restricts
151
+ * the runs to those made while that bundle was active.
152
+ */
153
+ getTestingStatus(projectKey?: string, bundleId?: string): Promise<Record<string, unknown>>;
154
+ /**
155
+ * Fetch the report of the latest test-scenario runs as text: JUnit XML by
156
+ * default, an HTML page for `format: "html"`. On an Automation node,
157
+ * `bundleId` selects the bundle; otherwise the active bundle is reported.
158
+ */
159
+ getTestScenarioRunsReport(projectKey?: string, options?: TestScenarioRunsReportOptions): Promise<string>;
160
+ /**
161
+ * Download the latest test-scenario runs report to a local file and return
162
+ * the byte count written. The file is only published once complete.
163
+ */
164
+ downloadTestScenarioRunsReport(filePath: string, projectKey?: string, options?: TestScenarioRunsReportOptions): Promise<number>;
165
+ private testScenarioRoute;
142
166
  /** Get project-level tags (tag name → {color?}). */
143
167
  tags(projectKey?: string): Promise<ProjectTags>;
144
168
  /**
@@ -102,6 +102,11 @@ const DEFAULT_MAX_NODES = 300;
102
102
  const DEFAULT_MAX_EDGES = 600;
103
103
  const DEFAULT_METADATA_TIMEOUT_MS = 1_500;
104
104
  const PROJECT_IMPORT_AMBIGUOUS_REMEDIATION = "Import processing left DSS state indeterminate. Inspect the archive with `dss project inspect-archive <file>` and the live projects with `dss project list` before retrying the import.";
105
+ function testReportRoute(options) {
106
+ return options.format === "html"
107
+ ? "last-test-scenario-runs-html-report"
108
+ : "last-test-scenario-runs-report";
109
+ }
105
110
  export class ProjectsResource extends BaseResource {
106
111
  /** Create a new project. */
107
112
  async createProject(projectKey, name, ownerLogin, settings) {
@@ -319,6 +324,37 @@ export class ProjectsResource extends BaseResource {
319
324
  async pushToGitRemote(remote, projectKey) {
320
325
  await this.client.post(`/public/api/projects/${this.enc(projectKey)}/actions/push-to-git-remote?remote=${encodeURIComponent(remote)}`);
321
326
  }
327
+ /**
328
+ * Get the testing status of a project: the worst outcome across the last
329
+ * runs of its test scenarios. On an Automation node, `bundleId` restricts
330
+ * the runs to those made while that bundle was active.
331
+ */
332
+ async getTestingStatus(projectKey, bundleId) {
333
+ return this.client.get(this.testScenarioRoute(projectKey, "testing-status", bundleId));
334
+ }
335
+ /**
336
+ * Fetch the report of the latest test-scenario runs as text: JUnit XML by
337
+ * default, an HTML page for `format: "html"`. On an Automation node,
338
+ * `bundleId` selects the bundle; otherwise the active bundle is reported.
339
+ */
340
+ async getTestScenarioRunsReport(projectKey, options = {}) {
341
+ return this.client.getText(this.testScenarioRoute(projectKey, testReportRoute(options), options.bundleId));
342
+ }
343
+ /**
344
+ * Download the latest test-scenario runs report to a local file and return
345
+ * the byte count written. The file is only published once complete.
346
+ */
347
+ async downloadTestScenarioRunsReport(filePath, projectKey, options = {}) {
348
+ const res = await this.client.stream(this.testScenarioRoute(projectKey, testReportRoute(options), options.bundleId));
349
+ if (!res.body) {
350
+ throw unexpectedResponseError("projects.downloadTestScenarioRunsReport response did not include a body");
351
+ }
352
+ return writeResponseToFile(filePath, res);
353
+ }
354
+ testScenarioRoute(projectKey, route, bundleId) {
355
+ const query = bundleId === undefined ? "" : `?bundleId=${encodeURIComponent(bundleId)}`;
356
+ return `/public/api/projects/${this.enc(projectKey)}/scenarios/${route}${query}`;
357
+ }
322
358
  /** Get project-level tags (tag name → {color?}). */
323
359
  async tags(projectKey) {
324
360
  const enc = this.enc(projectKey);
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Pure builders for the JSON DSS stores under `triggers` and `reporters` of a
3
+ * scenario. Trigger shapes mirror dataikuapi's `add_periodic_trigger`,
4
+ * `add_hourly_trigger`, `add_daily_trigger` (which becomes a Weekly trigger when
5
+ * given days) and `add_monthly_trigger`; the mail reporter shape is the one DSS
6
+ * 15 stores for a `mail-scenario` reporter.
7
+ */
8
+ export declare const SCENARIO_TRIGGER_FREQUENCIES: readonly ["minutely", "hourly", "daily", "weekly", "monthly"];
9
+ export type ScenarioTriggerFrequency = typeof SCENARIO_TRIGGER_FREQUENCIES[number];
10
+ export declare const SCENARIO_WEEKDAYS: readonly ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
11
+ export declare const SCENARIO_MONTHLY_RUN_ON: readonly ["ON_THE_DAY", "LAST_DAY_OF_THE_MONTH", "FIRST_WEEK", "SECOND_WEEK", "THIRD_WEEK", "FOURTH_WEEK", "LAST_WEEK"];
12
+ export declare const SCENARIO_REPORTER_PHASES: readonly ["START", "END"];
13
+ export interface TemporalTriggerSpec {
14
+ frequency: ScenarioTriggerFrequency;
15
+ /** Repeat interval: minutes (minutely), hours, days, weeks or months. */
16
+ every?: number;
17
+ /** Hour of the day (daily, weekly, monthly) or hour the schedule starts from (hourly). */
18
+ hour?: number;
19
+ minute?: number;
20
+ /** Weekday names; required by `weekly`, rejected elsewhere. */
21
+ days?: readonly string[];
22
+ /** Day of the month of the start date (monthly, default 1). */
23
+ dayOfMonth?: number;
24
+ runOn?: string;
25
+ /** `YYYY-MM-DD` day before which the trigger does not run; default today (monthly: January 1st of this year). */
26
+ startDate?: string;
27
+ timezone?: string;
28
+ name?: string;
29
+ active?: boolean;
30
+ }
31
+ /** The scenario trigger object dataikuapi would append for this schedule. */
32
+ export declare function temporalTrigger(spec: TemporalTriggerSpec, now?: Date): Record<string, unknown>;
33
+ export interface MailReporterSpec {
34
+ channel: string;
35
+ recipient: string;
36
+ subject: string;
37
+ message: string;
38
+ /** DSS expression gating the report, e.g. `outcome == 'FAILED'`. */
39
+ condition?: string;
40
+ phase?: string;
41
+ name?: string;
42
+ active?: boolean;
43
+ }
44
+ /** A `mail-scenario` reporter as DSS 15 stores it (inline message, Freemarker, no attachments). */
45
+ export declare function mailReporter(spec: MailReporterSpec): Record<string, unknown>;
46
+ /** Which element of a trigger/reporter list to remove. */
47
+ export interface ScenarioListSelector {
48
+ index?: number;
49
+ name?: string;
50
+ }
51
+ /** Position of the selected element; ambiguous or missing selections throw. */
52
+ export declare function selectScenarioListIndex(items: readonly unknown[], selector: ScenarioListSelector, kind: "trigger" | "reporter"): number;
@@ -0,0 +1,216 @@
1
+ import { ClientValidationError, } from "../errors.js";
2
+ import { isRecord, } from "../utils/records.js";
3
+ /**
4
+ * Pure builders for the JSON DSS stores under `triggers` and `reporters` of a
5
+ * scenario. Trigger shapes mirror dataikuapi's `add_periodic_trigger`,
6
+ * `add_hourly_trigger`, `add_daily_trigger` (which becomes a Weekly trigger when
7
+ * given days) and `add_monthly_trigger`; the mail reporter shape is the one DSS
8
+ * 15 stores for a `mail-scenario` reporter.
9
+ */
10
+ export const SCENARIO_TRIGGER_FREQUENCIES = [
11
+ "minutely",
12
+ "hourly",
13
+ "daily",
14
+ "weekly",
15
+ "monthly",
16
+ ];
17
+ export const SCENARIO_WEEKDAYS = [
18
+ "Monday",
19
+ "Tuesday",
20
+ "Wednesday",
21
+ "Thursday",
22
+ "Friday",
23
+ "Saturday",
24
+ "Sunday",
25
+ ];
26
+ export const SCENARIO_MONTHLY_RUN_ON = [
27
+ "ON_THE_DAY",
28
+ "LAST_DAY_OF_THE_MONTH",
29
+ "FIRST_WEEK",
30
+ "SECOND_WEEK",
31
+ "THIRD_WEEK",
32
+ "FOURTH_WEEK",
33
+ "LAST_WEEK",
34
+ ];
35
+ export const SCENARIO_REPORTER_PHASES = ["START", "END",];
36
+ function invalid(message) {
37
+ return new ClientValidationError(message, "invalid_flag_value");
38
+ }
39
+ /** Membership test that narrows a free-form string to one of the allowed literals. */
40
+ function isOneOf(allowed, value) {
41
+ return allowed.includes(value);
42
+ }
43
+ function boundedInteger(value, name, min, max) {
44
+ if (!Number.isInteger(value) || value < min || value > max) {
45
+ throw invalid(`${name} must be an integer from ${min} to ${max}, got ${value}.`);
46
+ }
47
+ return value;
48
+ }
49
+ const pad = (value, width = 2) => String(value).padStart(width, "0");
50
+ /**
51
+ * dataikuapi's `startingFrom`: local midnight of the date, with milliseconds and
52
+ * a `+HHMM` offset (taken from the machine's time zone, as dataikuapi does).
53
+ */
54
+ function startingFrom(year, month, day) {
55
+ const local = new Date(year, month - 1, day, 0, 0, 0, 0);
56
+ if (local.getFullYear() !== year || local.getMonth() !== month - 1 || local.getDate() !== day) {
57
+ throw invalid(`${year}-${pad(month)}-${pad(day)} is not a calendar date.`);
58
+ }
59
+ const offsetMinutes = -local.getTimezoneOffset();
60
+ const sign = offsetMinutes < 0 ? "-" : "+";
61
+ const absolute = Math.abs(offsetMinutes);
62
+ return `${year}-${pad(month)}-${pad(day)}T00:00:00.000${sign}${pad(Math.floor(absolute / 60))}${pad(absolute % 60)}`;
63
+ }
64
+ function startingFromDate(startDate, fallback) {
65
+ if (startDate === undefined)
66
+ return fallback();
67
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(startDate);
68
+ if (!match)
69
+ throw invalid(`--start-date must be YYYY-MM-DD, got ${JSON.stringify(startDate)}.`);
70
+ return startingFrom(Number(match[1]), Number(match[2]), Number(match[3]));
71
+ }
72
+ /** The scenario trigger object dataikuapi would append for this schedule. */
73
+ export function temporalTrigger(spec, now = new Date()) {
74
+ const every = spec.every === undefined
75
+ ? undefined
76
+ : boundedInteger(spec.every, "--every", 1, 100_000);
77
+ const timezone = spec.timezone ?? "SERVER";
78
+ if (spec.frequency !== "weekly" && spec.days !== undefined) {
79
+ throw invalid(`--days applies to weekly triggers only (got --frequency ${spec.frequency}).`);
80
+ }
81
+ if (spec.frequency !== "monthly" && (spec.runOn !== undefined || spec.dayOfMonth !== undefined)) {
82
+ throw invalid(`--run-on and --day-of-month apply to monthly triggers only (got --frequency ${spec.frequency}).`);
83
+ }
84
+ if (spec.frequency === "minutely") {
85
+ for (const [flag, value,] of [["--hour", spec.hour,], ["--minute", spec.minute,], [
86
+ "--start-date",
87
+ spec.startDate,
88
+ ], ["--timezone", spec.timezone,],]) {
89
+ if (value !== undefined)
90
+ throw invalid(`${flag} does not apply to minutely triggers.`);
91
+ }
92
+ }
93
+ const today = () => startingFrom(now.getFullYear(), now.getMonth() + 1, now.getDate());
94
+ const trigger = (params) => ({
95
+ ...(spec.name === undefined ? {} : { name: spec.name, }),
96
+ active: spec.active ?? true,
97
+ type: "temporal",
98
+ params,
99
+ });
100
+ if (spec.frequency === "minutely") {
101
+ return trigger({ frequency: "Minutely", repeatFrequency: every ?? 5, });
102
+ }
103
+ const minute = boundedInteger(spec.minute ?? 0, "--minute", 0, 59);
104
+ if (spec.frequency === "hourly") {
105
+ return trigger({
106
+ frequency: "Hourly",
107
+ hour: boundedInteger(spec.hour ?? 0, "--hour", 0, 23),
108
+ minute,
109
+ startingFrom: startingFromDate(spec.startDate, today),
110
+ repeatFrequency: every ?? 1,
111
+ timezone,
112
+ });
113
+ }
114
+ const hour = boundedInteger(spec.hour ?? 2, "--hour", 0, 23);
115
+ if (spec.frequency === "daily") {
116
+ return trigger({
117
+ frequency: "Daily",
118
+ hour,
119
+ minute,
120
+ startingFrom: startingFromDate(spec.startDate, today),
121
+ repeatFrequency: every ?? 1,
122
+ timezone,
123
+ });
124
+ }
125
+ if (spec.frequency === "weekly") {
126
+ if (spec.days === undefined || spec.days.length === 0) {
127
+ throw invalid(`--days is required for weekly triggers (${SCENARIO_WEEKDAYS.join(", ")}, comma-separated).`);
128
+ }
129
+ const unknown = spec.days.filter((day) => !isOneOf(SCENARIO_WEEKDAYS, day));
130
+ if (unknown.length > 0) {
131
+ throw invalid(`Unknown weekday ${unknown.map((day) => JSON.stringify(day)).join(", ")}; use ${SCENARIO_WEEKDAYS.join(", ")}.`);
132
+ }
133
+ return trigger({
134
+ frequency: "Weekly",
135
+ hour,
136
+ minute,
137
+ startingFrom: startingFromDate(spec.startDate, today),
138
+ daysOfWeek: [...spec.days,],
139
+ repeatFrequency: every ?? 1,
140
+ timezone,
141
+ });
142
+ }
143
+ const runOn = spec.runOn ?? "ON_THE_DAY";
144
+ if (!isOneOf(SCENARIO_MONTHLY_RUN_ON, runOn)) {
145
+ throw invalid(`--run-on must be one of ${SCENARIO_MONTHLY_RUN_ON.join(", ")}, got ${runOn}.`);
146
+ }
147
+ if (spec.startDate !== undefined && spec.dayOfMonth !== undefined) {
148
+ throw invalid("--start-date and --day-of-month are mutually exclusive for monthly triggers.");
149
+ }
150
+ const dayOfMonth = boundedInteger(spec.dayOfMonth ?? 1, "--day-of-month", 1, 31);
151
+ return trigger({
152
+ frequency: "Monthly",
153
+ startingFrom: startingFromDate(spec.startDate,
154
+ // dataikuapi starts monthly schedules on January 1st (not today) so that day 31 never lands on a short month.
155
+ () => startingFrom(now.getFullYear(), 1, dayOfMonth)),
156
+ hour,
157
+ minute,
158
+ monthlyRunOn: runOn,
159
+ repeatFrequency: every ?? 1,
160
+ timezone,
161
+ });
162
+ }
163
+ function requiredText(value, name) {
164
+ if (typeof value !== "string" || value.trim().length === 0) {
165
+ throw invalid(`${name} must be a non-empty string.`);
166
+ }
167
+ return value;
168
+ }
169
+ /** A `mail-scenario` reporter as DSS 15 stores it (inline message, Freemarker, no attachments). */
170
+ export function mailReporter(spec) {
171
+ const phase = spec.phase ?? "END";
172
+ if (!isOneOf(SCENARIO_REPORTER_PHASES, phase)) {
173
+ throw invalid(`--phase must be ${SCENARIO_REPORTER_PHASES.join(" or ")}, got ${phase}.`);
174
+ }
175
+ if (spec.condition !== undefined)
176
+ requiredText(spec.condition, "--condition");
177
+ return {
178
+ ...(spec.name === undefined ? {} : { name: spec.name, }),
179
+ active: spec.active ?? true,
180
+ phase,
181
+ runConditionEnabled: spec.condition !== undefined,
182
+ runCondition: spec.condition ?? "true",
183
+ messaging: {
184
+ type: "mail-scenario",
185
+ configuration: {
186
+ channelId: requiredText(spec.channel, "--channel"),
187
+ recipient: requiredText(spec.recipient, "--recipient"),
188
+ subject: spec.subject,
189
+ message: spec.message,
190
+ messageSource: "INLINE",
191
+ templateFormat: "FREEMARKER",
192
+ sendAsHTML: false,
193
+ attachments: [],
194
+ },
195
+ },
196
+ };
197
+ }
198
+ /** Position of the selected element; ambiguous or missing selections throw. */
199
+ export function selectScenarioListIndex(items, selector, kind) {
200
+ if ((selector.index === undefined) === (selector.name === undefined)) {
201
+ throw invalid(`Pass exactly one of --index or --name to select the ${kind}.`);
202
+ }
203
+ if (selector.index !== undefined) {
204
+ if (!Number.isInteger(selector.index) || selector.index < 0 || selector.index >= items.length) {
205
+ throw invalid(`--index ${selector.index} is outside the ${items.length} ${kind}(s) of this scenario (0-based).`);
206
+ }
207
+ return selector.index;
208
+ }
209
+ const matches = items.flatMap((item, index) => isRecord(item) && item["name"] === selector.name ? [index,] : []);
210
+ if (matches.length !== 1) {
211
+ throw invalid(matches.length === 0
212
+ ? `No ${kind} is named ${JSON.stringify(selector.name)}.`
213
+ : `${matches.length} ${kind}s are named ${JSON.stringify(selector.name)} (indexes ${matches.join(", ")}); use --index.`);
214
+ }
215
+ return matches[0];
216
+ }
@@ -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