dataiku-sdk 0.6.0 → 0.6.1

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.
@@ -6,16 +6,16 @@ export { CodeEnvsResource, } from "./resources/code-envs.js";
6
6
  export { type ConnectionSchemaListOptions, ConnectionsResource, type ConnectionTableListOptions, } from "./resources/connections.js";
7
7
  export { DashboardsResource, } from "./resources/dashboards.js";
8
8
  export { DataQualityResource, } from "./resources/data-quality.js";
9
- export { type DatasetBuildValidationResult, type DatasetSchemaColumnInput, DatasetsResource, } from "./resources/datasets.js";
9
+ export { type DatasetBuildValidationResult, type DatasetCloneOptions, type DatasetCloneResult, type DatasetSchemaColumnInput, DatasetsResource, } from "./resources/datasets.js";
10
10
  export { type FlowZoneItemInput, FlowZonesResource, } from "./resources/flow-zones.js";
11
11
  export { FoldersResource, } from "./resources/folders.js";
12
12
  export { FuturesResource, } from "./resources/futures.js";
13
13
  export { InsightsResource, } from "./resources/insights.js";
14
- export { computeNextPollDelayMs, type JobBuildAndWaitOptions, type JobBuildOptions, type JobBuildTarget, type JobBuildTargetType, type JobLogFilter, type JobLogSummary, JobsResource, } from "./resources/jobs.js";
14
+ export { computeNextPollDelayMs, type JobBuildAndWaitOptions, type JobBuildOptions, type JobBuildTarget, type JobBuildTargetType, type JobLogFilter, type JobLogProgress, type JobLogSummary, JobsResource, parseJobLogProgress, } from "./resources/jobs.js";
15
15
  export { NotebooksResource, } from "./resources/notebooks.js";
16
16
  export { type FlowMapResult, ProjectsResource, } from "./resources/projects.js";
17
- export { type RecipeGraphReference, type RecipeGraphValidationResult, type RecipeRunOptions, type RecipeRunOutput, type RecipeRunResult, RecipesResource, } from "./resources/recipes.js";
18
- export { ScenariosResource, } from "./resources/scenarios.js";
17
+ export { type RecipeCloneOptions, type RecipeCloneResult, type RecipeGraphReference, type RecipeGraphValidationResult, type RecipeRunOptions, type RecipeRunOutput, type RecipeRunResult, RecipesResource, } from "./resources/recipes.js";
18
+ export { normalizeScenarioUpdateData, SCENARIO_CANONICAL_EDITABLE_FIELDS, type ScenarioFieldChange, type ScenarioFieldMismatch, ScenariosResource, type ScenarioUpdateNormalization, type ScenarioUpdatePreview, scenarioUpdatePreview, type ScenarioUpdateResult, } from "./resources/scenarios.js";
19
19
  export { SqlResource, } from "./resources/sql.js";
20
20
  export { VariablesResource, } from "./resources/variables.js";
21
21
  export { WikiResource, } from "./resources/wiki.js";
package/dist/src/index.js CHANGED
@@ -15,11 +15,11 @@ export { FlowZonesResource, } from "./resources/flow-zones.js";
15
15
  export { FoldersResource, } from "./resources/folders.js";
16
16
  export { FuturesResource, } from "./resources/futures.js";
17
17
  export { InsightsResource, } from "./resources/insights.js";
18
- export { computeNextPollDelayMs, JobsResource, } from "./resources/jobs.js";
18
+ export { computeNextPollDelayMs, JobsResource, parseJobLogProgress, } from "./resources/jobs.js";
19
19
  export { NotebooksResource, } from "./resources/notebooks.js";
20
20
  export { ProjectsResource, } from "./resources/projects.js";
21
21
  export { RecipesResource, } from "./resources/recipes.js";
22
- export { ScenariosResource, } from "./resources/scenarios.js";
22
+ export { normalizeScenarioUpdateData, SCENARIO_CANONICAL_EDITABLE_FIELDS, ScenariosResource, scenarioUpdatePreview, } from "./resources/scenarios.js";
23
23
  export { SqlResource, } from "./resources/sql.js";
24
24
  export { VariablesResource, } from "./resources/variables.js";
25
25
  export { WikiResource, } from "./resources/wiki.js";
@@ -14,6 +14,21 @@ export interface DatasetSchemaColumnInput {
14
14
  type: string;
15
15
  comment?: string;
16
16
  }
17
+ export interface DatasetCloneOptions {
18
+ projectKey?: string;
19
+ path?: string;
20
+ table?: string;
21
+ metastoreTableName?: string;
22
+ overrides?: Record<string, unknown>;
23
+ allowSamePath?: boolean;
24
+ }
25
+ export interface DatasetCloneResult {
26
+ source: string;
27
+ target: string;
28
+ projectKey: string;
29
+ created: Record<string, unknown>;
30
+ settings: Record<string, unknown>;
31
+ }
17
32
  /**
18
33
  * Compare streamed TSV header columns against a known dataset schema.
19
34
  * Returns an array of warning strings (empty if all columns match).
@@ -21,6 +36,7 @@ export interface DatasetSchemaColumnInput {
21
36
  export declare function validateStreamColumns(headerRow: string[], expectedColumns: {
22
37
  name: string;
23
38
  }[]): string[];
39
+ export declare function buildDatasetCloneSettings(source: DatasetDetails, targetName: string, projectKey: string, opts: DatasetCloneOptions): Record<string, unknown>;
24
40
  export declare class DatasetsResource extends BaseResource {
25
41
  /** List all datasets in a project. */
26
42
  list(projectKey?: string): Promise<DatasetSummary[]>;
@@ -69,6 +85,8 @@ export declare class DatasetsResource extends BaseResource {
69
85
  create(opts: DatasetCreateOptions): Promise<Record<string, unknown>>;
70
86
  /** Validate common build blockers before running a dataset build. */
71
87
  validateBuildSettings(datasetName: string, projectKey?: string): Promise<DatasetBuildValidationResult>;
88
+ /** Clone dataset settings, preserving connection/storage, format, and schema fields. */
89
+ clone(sourceName: string, targetName: string, opts?: DatasetCloneOptions): Promise<DatasetCloneResult>;
72
90
  /** Update a dataset by deep-merging a patch into the current definition. */
73
91
  update(datasetName: string, data: Record<string, unknown>, projectKey?: string): Promise<void>;
74
92
  /** Delete a dataset. */
@@ -306,6 +306,38 @@ function buildDatasetCreateBody(opts) {
306
306
  managed: opts.managed ?? true,
307
307
  };
308
308
  }
309
+ export function buildDatasetCloneSettings(source, targetName, projectKey, opts) {
310
+ const params = {
311
+ ...source.params,
312
+ ...(opts.path !== undefined ? { path: opts.path, } : {}),
313
+ ...(opts.table !== undefined ? { table: opts.table, mode: "table", } : {}),
314
+ ...(opts.metastoreTableName !== undefined
315
+ ? { metastoreTableName: opts.metastoreTableName, }
316
+ : {}),
317
+ };
318
+ const cloned = {
319
+ name: targetName,
320
+ projectKey,
321
+ ...(source.type !== undefined ? { type: source.type, } : {}),
322
+ ...(source.managed !== undefined ? { managed: source.managed, } : {}),
323
+ ...(Object.keys(params).length > 0 ? { params, } : {}),
324
+ ...(source.formatType !== undefined ? { formatType: source.formatType, } : {}),
325
+ ...(source.formatParams !== undefined ? { formatParams: source.formatParams, } : {}),
326
+ ...(source.schema !== undefined ? { schema: source.schema, } : {}),
327
+ };
328
+ const settings = opts.overrides ? deepMerge(cloned, opts.overrides) : cloned;
329
+ const settingsParams = settings.params && typeof settings.params === "object" && !Array.isArray(settings.params)
330
+ ? settings.params
331
+ : {};
332
+ const sourcePath = typeof source.params?.path === "string" ? source.params.path : undefined;
333
+ if (opts.allowSamePath !== true
334
+ && source.managed === true
335
+ && sourcePath !== undefined
336
+ && settingsParams.path === sourcePath) {
337
+ throw new Error(`Refusing to clone managed dataset "${source.name}" with the same storage path. Pass a new path or allowSamePath: true.`);
338
+ }
339
+ return settings;
340
+ }
309
341
  // ---------------------------------------------------------------------------
310
342
  // Resource
311
343
  // ---------------------------------------------------------------------------
@@ -485,6 +517,13 @@ export class DatasetsResource extends BaseResource {
485
517
  warnings,
486
518
  };
487
519
  }
520
+ /** Clone dataset settings, preserving connection/storage, format, and schema fields. */
521
+ async clone(sourceName, targetName, opts = {}) {
522
+ const pk = this.resolveProjectKey(opts.projectKey);
523
+ const settings = buildDatasetCloneSettings(await this.get(sourceName, pk), targetName, pk, opts);
524
+ const created = await this.client.post(`/public/api/projects/${encodeURIComponent(pk)}/datasets/`, settings);
525
+ return { source: sourceName, target: targetName, projectKey: pk, created, settings, };
526
+ }
488
527
  /** Update a dataset by deep-merging a patch into the current definition. */
489
528
  async update(datasetName, data, projectKey) {
490
529
  const dsEnc = encodeURIComponent(datasetName);
@@ -2,10 +2,17 @@ import type { BuildMode, JobSummary, JobWaitResult } from "../schemas.js";
2
2
  import { BaseResource } from "./base.js";
3
3
  export type JobBuildTargetType = "DATASET" | "MANAGED_FOLDER";
4
4
  export type JobLogFilter = "stdout" | "stderr" | "user" | "errors";
5
+ export interface JobLogProgress {
6
+ lastProgressLine?: string;
7
+ doneLine?: string;
8
+ counters: Record<string, number>;
9
+ rowsPerMinute?: number;
10
+ }
5
11
  export interface JobLogSummary {
6
12
  state: string;
7
13
  lineCount: number;
8
14
  lines: string[];
15
+ progress?: JobLogProgress;
9
16
  }
10
17
  export interface JobBuildTarget {
11
18
  id: string;
@@ -27,6 +34,7 @@ export interface JobBuildAndWaitOptions extends JobBuildOptions {
27
34
  pollIntervalMs?: number;
28
35
  timeoutMs?: number;
29
36
  logFilter?: JobLogFilter;
37
+ logId?: string;
30
38
  summary?: boolean;
31
39
  }
32
40
  interface ComputeNextPollDelayMsOptions {
@@ -40,6 +48,7 @@ interface ComputeNextPollDelayMsOptions {
40
48
  * capped at MAX_POLL_INTERVAL_MS (or baseIntervalMs if it's larger).
41
49
  */
42
50
  export declare function computeNextPollDelayMs({ pollCount, baseIntervalMs, adaptiveEnabled, }: ComputeNextPollDelayMsOptions): number;
51
+ export declare function parseJobLogProgress(log: string, elapsedMs?: number): JobLogProgress | undefined;
43
52
  export declare class JobsResource extends BaseResource {
44
53
  /** List jobs in a project. */
45
54
  list(projectKey?: string): Promise<JobSummary[]>;
@@ -52,9 +61,13 @@ export declare class JobsResource extends BaseResource {
52
61
  */
53
62
  log(jobId: string, opts?: {
54
63
  activity?: string;
64
+ logId?: string;
55
65
  maxLogLines?: number;
56
66
  projectKey?: string;
57
67
  }): Promise<string>;
68
+ logFromUrl(logUrl: string, opts?: {
69
+ maxLogLines?: number;
70
+ }): Promise<string>;
58
71
  /**
59
72
  * Start a build job for one or more dataset or managed-folder outputs.
60
73
  * Returns the new job's ID.
@@ -95,6 +108,7 @@ export declare class JobsResource extends BaseResource {
95
108
  activity?: string;
96
109
  includeLogs?: boolean;
97
110
  logFilter?: JobLogFilter;
111
+ logId?: string;
98
112
  maxLogLines?: number;
99
113
  pollIntervalMs?: number;
100
114
  summary?: boolean;
@@ -104,13 +104,58 @@ function limitJobLog(log, maxLines) {
104
104
  const tail = lines.slice(-Math.max(1, limit)).join("\n");
105
105
  return hasTrailingLineBreak ? `${tail}\n` : tail;
106
106
  }
107
- function summarizeJobLog(state, log, maxLines) {
107
+ function parsedCounterValue(value) {
108
+ return Number(value.replace(/,/g, ""));
109
+ }
110
+ export function parseJobLogProgress(log, elapsedMs) {
111
+ const counters = {};
112
+ let lastProgressLine;
113
+ let doneLine;
114
+ for (const line of jobLogLines(log)) {
115
+ const normalized = line.trim();
116
+ if (!normalized)
117
+ continue;
118
+ const lower = normalized.toLowerCase();
119
+ let matched = false;
120
+ for (const match of normalized.matchAll(/\b(scanned|matched|joined|written|emitted)\s+([0-9][0-9,]*)/gi)) {
121
+ counters[match[1].toLowerCase()] = parsedCounterValue(match[2]);
122
+ matched = true;
123
+ }
124
+ const written = normalized.match(/\b([0-9][0-9,]*)\s+rows\s+successfully\s+written\b/i);
125
+ if (written) {
126
+ counters.written = parsedCounterValue(written[1]);
127
+ doneLine = normalized;
128
+ matched = true;
129
+ }
130
+ if (lower.includes("done!")) {
131
+ doneLine = normalized;
132
+ matched = true;
133
+ }
134
+ if (matched)
135
+ lastProgressLine = normalized;
136
+ }
137
+ if (lastProgressLine === undefined && doneLine === undefined)
138
+ return undefined;
139
+ const writtenRows = counters.written ?? counters.emitted;
140
+ const rowsPerMinute = writtenRows !== undefined && elapsedMs !== undefined && elapsedMs > 0
141
+ ? writtenRows / (elapsedMs / 60_000)
142
+ : undefined;
143
+ return {
144
+ ...(lastProgressLine ? { lastProgressLine, } : {}),
145
+ ...(doneLine ? { doneLine, } : {}),
146
+ counters,
147
+ ...(rowsPerMinute !== undefined ? { rowsPerMinute, } : {}),
148
+ };
149
+ }
150
+ function summarizeJobLog(state, log, maxLines, elapsedMs) {
108
151
  const lines = jobLogLines(log).map((line) => line.trim()).filter((line) => line.length > 0);
109
152
  const summaryLines = lines.slice(-Math.max(1, maxLines));
153
+ const progress = parseJobLogProgress(log, elapsedMs);
110
154
  return {
111
155
  state,
112
156
  lineCount: lines.length,
113
157
  lines: summaryLines,
158
+ ...(progress ? { progress, } : {}),
114
159
  };
115
160
  }
116
161
  export class JobsResource extends BaseResource {
@@ -133,9 +178,21 @@ export class JobsResource extends BaseResource {
133
178
  async log(jobId, opts) {
134
179
  const jobEnc = encodeURIComponent(jobId);
135
180
  const query = opts?.activity ? `?activity=${encodeURIComponent(opts.activity)}` : "";
136
- const log = await this.client.getText(`/public/api/projects/${this.enc(opts?.projectKey)}/jobs/${jobEnc}/log/${query}`);
181
+ // DSS cat-activity-log URLs require a browser session; API-key callers must use the public log endpoint.
182
+ const path = `/public/api/projects/${this.enc(opts?.projectKey)}/jobs/${jobEnc}/log/${query}`;
183
+ const log = await this.client.getText(path);
137
184
  return limitJobLog(log, opts?.maxLogLines);
138
185
  }
186
+ async logFromUrl(logUrl, opts) {
187
+ const parsed = new URL(logUrl, "http://dss.local");
188
+ const projectKey = parsed.searchParams.get("projectKey") ?? undefined;
189
+ const jobId = parsed.searchParams.get("jobId") ?? undefined;
190
+ const activity = parsed.searchParams.get("activityId") ?? undefined;
191
+ if (!projectKey || !jobId || !activity) {
192
+ throw new Error("Log URL must include projectKey, jobId, and activityId query parameters.");
193
+ }
194
+ return this.log(jobId, { activity, projectKey, maxLogLines: opts?.maxLogLines, });
195
+ }
139
196
  /**
140
197
  * Start a build job for one or more dataset or managed-folder outputs.
141
198
  * Returns the new job's ID.
@@ -168,6 +225,7 @@ export class JobsResource extends BaseResource {
168
225
  activity: opts?.activity,
169
226
  includeLogs: opts?.includeLogs,
170
227
  logFilter: opts?.logFilter,
228
+ logId: opts?.logId,
171
229
  maxLogLines: opts?.maxLogLines,
172
230
  pollIntervalMs: opts?.pollIntervalMs,
173
231
  summary: opts?.summary,
@@ -218,13 +276,14 @@ export class JobsResource extends BaseResource {
218
276
  const rawLog = await this.log(jobId, {
219
277
  activity: opts.activity,
220
278
  maxLogLines: opts.summary ? 0 : opts.maxLogLines,
279
+ logId: opts.logId,
221
280
  projectKey: opts.projectKey,
222
281
  });
223
282
  const filteredLog = filterJobLog(rawLog, opts.logFilter);
224
283
  if (opts.includeLogs)
225
284
  log = limitJobLog(filteredLog, opts.maxLogLines);
226
285
  if (opts.summary) {
227
- logSummary = summarizeJobLog(state, filteredLog, opts.maxLogLines ?? 20);
286
+ logSummary = summarizeJobLog(state, filteredLog, opts.maxLogLines ?? 20, elapsedMs);
228
287
  }
229
288
  }
230
289
  return {
@@ -42,6 +42,28 @@ export type RecipeRunResult = {
42
42
  } & ({
43
43
  jobId: string;
44
44
  } | JobWaitResult);
45
+ export interface RecipeCloneOptions {
46
+ projectKey?: string;
47
+ name: string;
48
+ outputDataset?: string;
49
+ outputRewrites?: Record<string, string>;
50
+ inputRewrites?: Record<string, string>;
51
+ payloadRewrites?: Record<string, string>;
52
+ payloadTextRewrites?: Record<string, string>;
53
+ copyOutputSettings?: boolean;
54
+ outputPath?: string;
55
+ metastoreTableName?: string;
56
+ }
57
+ export interface RecipeCloneResult {
58
+ sourceRecipeName: string;
59
+ recipeName: string;
60
+ projectKey: string;
61
+ outputRewrites: Record<string, string>;
62
+ inputRewrites: Record<string, string>;
63
+ payloadRewrites: Record<string, string>;
64
+ payloadTextRewrites: Record<string, string>;
65
+ copiedOutputDatasets: string[];
66
+ }
45
67
  export declare class RecipesResource extends BaseResource {
46
68
  /** List all recipes in a project. */
47
69
  list(projectKey?: string): Promise<RecipeSummary[]>;
@@ -75,6 +97,10 @@ export declare class RecipesResource extends BaseResource {
75
97
  * The `recipe` sub-object is deep-merged to preserve nested fields.
76
98
  */
77
99
  update(recipeName: string, data: Record<string, unknown>, projectKey?: string): Promise<void>;
100
+ /** Replace a full recipe API document. */
101
+ replace(recipeName: string, document: Record<string, unknown>, projectKey?: string): Promise<void>;
102
+ /** Clone recipe graph/settings and optionally clone a dataset output. */
103
+ clone(sourceName: string, opts: RecipeCloneOptions): Promise<RecipeCloneResult>;
78
104
  /**
79
105
  * Download a recipe code payload to a local file.
80
106
 
@@ -86,6 +86,54 @@ function recipeInputItems(recipe) {
86
86
  }
87
87
  return result;
88
88
  }
89
+ function rewriteRefs(value, rewrites) {
90
+ if (Object.keys(rewrites).length === 0)
91
+ return value;
92
+ if (Array.isArray(value))
93
+ return value.map((item) => rewriteRefs(item, rewrites));
94
+ const record = asRecord(value);
95
+ if (!record)
96
+ return value;
97
+ const next = {};
98
+ for (const [key, item,] of Object.entries(record)) {
99
+ if (key === "ref" && typeof item === "string" && rewrites[item]) {
100
+ next[key] = rewrites[item];
101
+ }
102
+ else {
103
+ next[key] = rewriteRefs(item, rewrites);
104
+ }
105
+ }
106
+ return next;
107
+ }
108
+ function escapedRegExp(value) {
109
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
110
+ }
111
+ function rewritePayload(payload, rewrites, payloadTextRewrites = {}) {
112
+ if (payload === undefined
113
+ || (Object.keys(rewrites).length === 0 && Object.keys(payloadTextRewrites).length === 0)) {
114
+ return payload;
115
+ }
116
+ let next = payload;
117
+ for (const [from, to,] of Object.entries(rewrites)) {
118
+ if (!from)
119
+ continue;
120
+ const escaped = escapedRegExp(from);
121
+ next = next.replace(new RegExp(`\\bdataiku\\.(Dataset|Folder)\\(\\s*(['"])${escaped}\\2\\s*\\)`, "g"), (_match, kind, quote) => `dataiku.${kind}(${quote}${to}${quote})`);
122
+ }
123
+ for (const [from, to,] of Object.entries(payloadTextRewrites)) {
124
+ if (from.length > 0)
125
+ next = next.split(from).join(to);
126
+ }
127
+ return next;
128
+ }
129
+ function cloneRecipeDefinition(recipe, targetName, projectKey, rewrites) {
130
+ const cloned = rewriteRefs(structuredClone(recipe), rewrites);
131
+ delete cloned.versionTag;
132
+ delete cloned.neverBuilt;
133
+ cloned.name = targetName;
134
+ cloned.projectKey = projectKey;
135
+ return cloned;
136
+ }
89
137
  function inferRecipeCodeExtension(recipeType) {
90
138
  const normalized = typeof recipeType === "string" ? recipeType.trim().toLowerCase() : "";
91
139
  if (!normalized)
@@ -562,6 +610,72 @@ export class RecipesResource extends BaseResource {
562
610
  const merged = { ...current, ...data, recipe: mergedRecipe, };
563
611
  await this.client.put(`/public/api/projects/${enc}/recipes/${rnEnc}`, merged);
564
612
  }
613
+ /** Replace a full recipe API document. */
614
+ async replace(recipeName, document, projectKey) {
615
+ const enc = this.enc(projectKey);
616
+ const rnEnc = encodeURIComponent(recipeName);
617
+ await this.client.put(`/public/api/projects/${enc}/recipes/${rnEnc}`, document);
618
+ }
619
+ /** Clone recipe graph/settings and optionally clone a dataset output. */
620
+ async clone(sourceName, opts) {
621
+ const pk = this.resolveProjectKey(opts.projectKey);
622
+ const source = await this.get(sourceName, { includePayload: true, projectKey: pk, });
623
+ const outputRewrites = {};
624
+ if (opts.outputRewrites)
625
+ Object.assign(outputRewrites, opts.outputRewrites);
626
+ if (opts.outputDataset !== undefined) {
627
+ const outputs = recipeOutputItems(source.recipe).filter((item) => item.type !== "MANAGED_FOLDER");
628
+ if (outputs.length !== 1 && Object.keys(outputRewrites).length === 0) {
629
+ throw new Error(`Recipe "${sourceName}" has ${outputs.length} dataset outputs; pass explicit outputRewrites instead of outputDataset.`);
630
+ }
631
+ if (outputs[0])
632
+ outputRewrites[outputs[0].ref] = opts.outputDataset;
633
+ }
634
+ const inputRewrites = {};
635
+ if (opts.inputRewrites)
636
+ Object.assign(inputRewrites, opts.inputRewrites);
637
+ const graphRewrites = { ...inputRewrites, ...outputRewrites, };
638
+ const payloadRewrites = { ...graphRewrites, };
639
+ if (opts.payloadRewrites)
640
+ Object.assign(payloadRewrites, opts.payloadRewrites);
641
+ if (opts.copyOutputSettings === true
642
+ && Object.keys(outputRewrites).length > 1
643
+ && (opts.outputPath !== undefined || opts.metastoreTableName !== undefined)) {
644
+ throw new Error("Cannot reuse --path or --metastore-table for multiple cloned output datasets; pass per-output settings in a separate step.");
645
+ }
646
+ const payloadTextRewrites = {};
647
+ if (opts.payloadTextRewrites)
648
+ Object.assign(payloadTextRewrites, opts.payloadTextRewrites);
649
+ const recipe = cloneRecipeDefinition(source.recipe, opts.name, pk, graphRewrites);
650
+ const payload = rewritePayload(source.payload, payloadRewrites, payloadTextRewrites);
651
+ const copiedOutputDatasets = [];
652
+ if (opts.copyOutputSettings) {
653
+ for (const [from, to,] of Object.entries(outputRewrites)) {
654
+ await this.client.datasets.clone(from, to, {
655
+ projectKey: pk,
656
+ path: opts.outputPath,
657
+ metastoreTableName: opts.metastoreTableName,
658
+ });
659
+ copiedOutputDatasets.push(to);
660
+ }
661
+ }
662
+ const rnEnc = encodeURIComponent(opts.name);
663
+ await this.client.post(`/public/api/projects/${encodeURIComponent(pk)}/recipes/`, {
664
+ recipePrototype: recipe,
665
+ creationSettings: payload !== undefined ? { script: payload, } : {},
666
+ });
667
+ await this.client.put(`/public/api/projects/${encodeURIComponent(pk)}/recipes/${rnEnc}`, { recipe, ...(payload !== undefined ? { payload, } : {}), });
668
+ return {
669
+ sourceRecipeName: sourceName,
670
+ recipeName: opts.name,
671
+ projectKey: pk,
672
+ outputRewrites,
673
+ inputRewrites,
674
+ payloadRewrites,
675
+ payloadTextRewrites,
676
+ copiedOutputDatasets,
677
+ };
678
+ }
565
679
  /**
566
680
  * Download a recipe code payload to a local file.
567
681
 
@@ -1,5 +1,41 @@
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"];
4
+ export interface ScenarioUpdateNormalization {
5
+ from: string;
6
+ to: string;
7
+ action: "promoted" | "ignored";
8
+ message: string;
9
+ }
10
+ export interface ScenarioFieldChange {
11
+ path: string;
12
+ before: unknown;
13
+ after: unknown;
14
+ }
15
+ export interface ScenarioFieldMismatch {
16
+ path: string;
17
+ expected: unknown;
18
+ actual: unknown;
19
+ }
20
+ export interface ScenarioUpdatePreview {
21
+ canonicalEditableFields: typeof SCENARIO_CANONICAL_EDITABLE_FIELDS;
22
+ normalization: ScenarioUpdateNormalization[];
23
+ normalizedData: Record<string, unknown>;
24
+ current: Record<string, unknown>;
25
+ next: Record<string, unknown>;
26
+ changes: ScenarioFieldChange[];
27
+ unchangedPaths: string[];
28
+ }
29
+ export interface ScenarioUpdateResult extends ScenarioUpdatePreview {
30
+ after: Record<string, unknown>;
31
+ verified: true;
32
+ mismatches: [];
33
+ }
34
+ export declare function normalizeScenarioUpdateData(data: Record<string, unknown>): {
35
+ normalizedData: Record<string, unknown>;
36
+ normalization: ScenarioUpdateNormalization[];
37
+ };
38
+ export declare function scenarioUpdatePreview(current: Record<string, unknown>, data: Record<string, unknown>): ScenarioUpdatePreview;
3
39
  export declare class ScenariosResource extends BaseResource {
4
40
  /** List all scenarios in a project. */
5
41
  list(projectKey?: string): Promise<ScenarioSummary[]>;
@@ -19,8 +55,8 @@ export declare class ScenariosResource extends BaseResource {
19
55
  }>;
20
56
  /** Get the light/status view of a scenario. */
21
57
  status(scenarioId: string, projectKey?: string): Promise<ScenarioStatus>;
22
- /** Merge-update a scenario's definition. */
23
- update(scenarioId: string, data: Record<string, unknown>, projectKey?: string): Promise<void>;
58
+ /** Merge-update a scenario's definition, then refetch and verify requested fields persisted. */
59
+ update(scenarioId: string, data: Record<string, unknown>, projectKey?: string): Promise<ScenarioUpdateResult>;
24
60
  /** Delete a scenario. */
25
61
  delete(scenarioId: string, projectKey?: string): Promise<void>;
26
62
  /**