dataiku-sdk 0.5.1 → 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.
- package/dist/packages/types/src/index.d.ts +4 -0
- package/dist/packages/types/src/index.js +1 -0
- package/dist/src/cli.js +1353 -128
- package/dist/src/errors.js +12 -0
- package/dist/src/index.d.ts +5 -5
- package/dist/src/index.js +2 -2
- package/dist/src/resources/connections.d.ts +10 -0
- package/dist/src/resources/connections.js +16 -0
- package/dist/src/resources/datasets.d.ts +36 -0
- package/dist/src/resources/datasets.js +80 -0
- package/dist/src/resources/jobs.d.ts +66 -19
- package/dist/src/resources/jobs.js +180 -33
- package/dist/src/resources/recipes.d.ts +80 -1
- package/dist/src/resources/recipes.js +349 -0
- package/dist/src/resources/scenarios.d.ts +38 -2
- package/dist/src/resources/scenarios.js +162 -3
- package/dist/src/resources/sql.js +84 -3
- package/dist/src/skill.d.ts +2 -2
- package/dist/src/skill.js +3 -3
- package/package.json +1 -1
- package/packages/types/dist/index.d.ts +4 -0
- package/packages/types/dist/index.js +1 -0
|
@@ -1,5 +1,69 @@
|
|
|
1
|
-
import type { RecipeCreateOptions, RecipeCreateResult, RecipeSummary } from "../schemas.js";
|
|
1
|
+
import type { BuildMode, JobWaitResult, RecipeCreateOptions, RecipeCreateResult, RecipeSummary } from "../schemas.js";
|
|
2
2
|
import { BaseResource } from "./base.js";
|
|
3
|
+
import type { JobBuildTarget, JobBuildTargetType, JobLogFilter, JobLogSummary } from "./jobs.js";
|
|
4
|
+
export interface RecipeRunOutput extends JobBuildTarget {
|
|
5
|
+
ref: string;
|
|
6
|
+
role: string;
|
|
7
|
+
}
|
|
8
|
+
export interface RecipeGraphReference {
|
|
9
|
+
ref: string;
|
|
10
|
+
role: string;
|
|
11
|
+
type?: JobBuildTargetType;
|
|
12
|
+
exists: boolean;
|
|
13
|
+
id?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface RecipeGraphValidationResult {
|
|
16
|
+
valid: boolean;
|
|
17
|
+
recipeName: string;
|
|
18
|
+
projectKey: string;
|
|
19
|
+
inputs: RecipeGraphReference[];
|
|
20
|
+
outputs: RecipeGraphReference[];
|
|
21
|
+
missingInputs: RecipeGraphReference[];
|
|
22
|
+
missingOutputs: RecipeGraphReference[];
|
|
23
|
+
ambiguousOutputs: string[];
|
|
24
|
+
warnings: string[];
|
|
25
|
+
}
|
|
26
|
+
export interface RecipeRunOptions {
|
|
27
|
+
buildMode?: BuildMode;
|
|
28
|
+
includeLogs?: boolean;
|
|
29
|
+
maxLogLines?: number;
|
|
30
|
+
partition?: string;
|
|
31
|
+
pollIntervalMs?: number;
|
|
32
|
+
projectKey?: string;
|
|
33
|
+
wait?: boolean;
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
logFilter?: JobLogFilter;
|
|
36
|
+
summary?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export type RecipeRunResult = {
|
|
39
|
+
logSummary?: JobLogSummary;
|
|
40
|
+
recipeName: string;
|
|
41
|
+
outputs: RecipeRunOutput[];
|
|
42
|
+
} & ({
|
|
43
|
+
jobId: string;
|
|
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
|
+
}
|
|
3
67
|
export declare class RecipesResource extends BaseResource {
|
|
4
68
|
/** List all recipes in a project. */
|
|
5
69
|
list(projectKey?: string): Promise<RecipeSummary[]>;
|
|
@@ -15,6 +79,17 @@ export declare class RecipesResource extends BaseResource {
|
|
|
15
79
|
recipe: Record<string, unknown>;
|
|
16
80
|
payload?: string;
|
|
17
81
|
}>;
|
|
82
|
+
/** Validate declared recipe graph references before running/building. */
|
|
83
|
+
validateGraph(recipeName: string, opts?: {
|
|
84
|
+
projectKey?: string;
|
|
85
|
+
}): Promise<RecipeGraphValidationResult>;
|
|
86
|
+
/** Resolve recipe outputs to job-build targets. */
|
|
87
|
+
resolveRunOutputs(recipeName: string, opts?: {
|
|
88
|
+
partition?: string;
|
|
89
|
+
projectKey?: string;
|
|
90
|
+
}): Promise<RecipeRunOutput[]>;
|
|
91
|
+
/** Run a recipe by building its resolved outputs. */
|
|
92
|
+
run(recipeName: string, opts?: RecipeRunOptions): Promise<RecipeRunResult>;
|
|
18
93
|
/** Create a recipe, with optional output dataset provisioning and join configuration. */
|
|
19
94
|
create(opts: RecipeCreateOptions): Promise<RecipeCreateResult>;
|
|
20
95
|
/**
|
|
@@ -22,6 +97,10 @@ export declare class RecipesResource extends BaseResource {
|
|
|
22
97
|
* The `recipe` sub-object is deep-merged to preserve nested fields.
|
|
23
98
|
*/
|
|
24
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>;
|
|
25
104
|
/**
|
|
26
105
|
* Download a recipe code payload to a local file.
|
|
27
106
|
|
|
@@ -26,6 +26,114 @@ const RECIPE_DEFINITION_FIELDS = new Set(["params", "inputs", "outputs", "script
|
|
|
26
26
|
function rootRecipeDefinitionFields(data) {
|
|
27
27
|
return Object.keys(data).filter((key) => RECIPE_DEFINITION_FIELDS.has(key));
|
|
28
28
|
}
|
|
29
|
+
function normalizeRecipeOutputType(value) {
|
|
30
|
+
if (typeof value !== "string")
|
|
31
|
+
return undefined;
|
|
32
|
+
const normalized = value.trim().toUpperCase().replace(/-/g, "_");
|
|
33
|
+
if (normalized === "DATASET")
|
|
34
|
+
return "DATASET";
|
|
35
|
+
if (normalized === "MANAGED_FOLDER" || normalized === "FOLDER")
|
|
36
|
+
return "MANAGED_FOLDER";
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
function recipeOutputItems(recipe) {
|
|
40
|
+
const outputs = asRecord(recipe.outputs);
|
|
41
|
+
if (!outputs)
|
|
42
|
+
return [];
|
|
43
|
+
const result = [];
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
for (const [role, roleValue,] of Object.entries(outputs)) {
|
|
46
|
+
const items = asRecord(roleValue)?.items;
|
|
47
|
+
if (!Array.isArray(items))
|
|
48
|
+
continue;
|
|
49
|
+
for (const itemValue of items) {
|
|
50
|
+
const item = asRecord(itemValue);
|
|
51
|
+
const ref = asString(item?.ref);
|
|
52
|
+
if (!ref)
|
|
53
|
+
continue;
|
|
54
|
+
const seenKey = ref;
|
|
55
|
+
if (seen.has(seenKey))
|
|
56
|
+
continue;
|
|
57
|
+
seen.add(seenKey);
|
|
58
|
+
const type = normalizeRecipeOutputType(item?.type ?? item?.targetType ?? item?.objectType);
|
|
59
|
+
result.push({
|
|
60
|
+
ref,
|
|
61
|
+
role,
|
|
62
|
+
...(type ? { type, } : {}),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
function recipeInputItems(recipe) {
|
|
69
|
+
const inputs = asRecord(recipe.inputs);
|
|
70
|
+
if (!inputs)
|
|
71
|
+
return [];
|
|
72
|
+
const result = [];
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
for (const [role, roleValue,] of Object.entries(inputs)) {
|
|
75
|
+
const items = asRecord(roleValue)?.items;
|
|
76
|
+
if (!Array.isArray(items))
|
|
77
|
+
continue;
|
|
78
|
+
for (const itemValue of items) {
|
|
79
|
+
const item = asRecord(itemValue);
|
|
80
|
+
const ref = asString(item?.ref);
|
|
81
|
+
if (!ref || seen.has(ref))
|
|
82
|
+
continue;
|
|
83
|
+
seen.add(ref);
|
|
84
|
+
result.push({ ref, role, });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
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
|
+
}
|
|
29
137
|
function inferRecipeCodeExtension(recipeType) {
|
|
30
138
|
const normalized = typeof recipeType === "string" ? recipeType.trim().toLowerCase() : "";
|
|
31
139
|
if (!normalized)
|
|
@@ -92,6 +200,181 @@ export class RecipesResource extends BaseResource {
|
|
|
92
200
|
}
|
|
93
201
|
return opts?.includePayload ? { ...result, recipe, } : { recipe, };
|
|
94
202
|
}
|
|
203
|
+
/** Validate declared recipe graph references before running/building. */
|
|
204
|
+
async validateGraph(recipeName, opts) {
|
|
205
|
+
const pk = this.resolveProjectKey(opts?.projectKey);
|
|
206
|
+
const { recipe, } = await this.get(recipeName, { projectKey: pk, });
|
|
207
|
+
const inputItems = recipeInputItems(recipe);
|
|
208
|
+
const outputItems = recipeOutputItems(recipe);
|
|
209
|
+
const [datasets, folders,] = await Promise.all([
|
|
210
|
+
this.client.datasets.list(pk),
|
|
211
|
+
this.client.folders.list(pk),
|
|
212
|
+
]);
|
|
213
|
+
const datasetNames = new Set(datasets.map((dataset) => dataset.name));
|
|
214
|
+
const folderIdByRef = new Map();
|
|
215
|
+
for (const folder of folders) {
|
|
216
|
+
folderIdByRef.set(folder.id, folder.id);
|
|
217
|
+
if (folder.name)
|
|
218
|
+
folderIdByRef.set(folder.name, folder.id);
|
|
219
|
+
}
|
|
220
|
+
const resolveReference = (item, requireExplicitOutputType) => {
|
|
221
|
+
const folderId = folderIdByRef.get(item.ref);
|
|
222
|
+
const isDataset = datasetNames.has(item.ref);
|
|
223
|
+
if (item.type === "DATASET") {
|
|
224
|
+
return { ref: item.ref, role: item.role, type: "DATASET", exists: isDataset, id: item.ref, };
|
|
225
|
+
}
|
|
226
|
+
if (item.type === "MANAGED_FOLDER") {
|
|
227
|
+
return {
|
|
228
|
+
ref: item.ref,
|
|
229
|
+
role: item.role,
|
|
230
|
+
type: "MANAGED_FOLDER",
|
|
231
|
+
exists: folderId !== undefined,
|
|
232
|
+
id: folderId ?? item.ref,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (isDataset && (!folderId || !requireExplicitOutputType)) {
|
|
236
|
+
return { ref: item.ref, role: item.role, type: "DATASET", exists: true, id: item.ref, };
|
|
237
|
+
}
|
|
238
|
+
if (folderId && !isDataset) {
|
|
239
|
+
return { ref: item.ref, role: item.role, type: "MANAGED_FOLDER", exists: true, id: folderId, };
|
|
240
|
+
}
|
|
241
|
+
return { ref: item.ref, role: item.role, exists: false, };
|
|
242
|
+
};
|
|
243
|
+
const inputs = inputItems.map((item) => resolveReference(item, false));
|
|
244
|
+
const outputs = outputItems.map((item) => resolveReference(item, true));
|
|
245
|
+
const ambiguousOutputs = outputItems
|
|
246
|
+
.filter((item) => !item.type && datasetNames.has(item.ref) && folderIdByRef.has(item.ref))
|
|
247
|
+
.map((item) => item.ref);
|
|
248
|
+
const missingInputs = inputs.filter((item) => !item.exists);
|
|
249
|
+
const missingOutputs = outputs.filter((item) => !item.exists);
|
|
250
|
+
const warnings = [];
|
|
251
|
+
if (outputItems.length === 0)
|
|
252
|
+
warnings.push("Recipe has no declared outputs to build.");
|
|
253
|
+
for (const ref of ambiguousOutputs) {
|
|
254
|
+
warnings.push(`Output "${ref}" matches both a dataset and a managed folder; declare an explicit output type.`);
|
|
255
|
+
}
|
|
256
|
+
for (const output of outputs) {
|
|
257
|
+
if (!output.exists) {
|
|
258
|
+
warnings.push(`Declared output "${output.ref}" was not found in project "${pk}".`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
for (const input of missingInputs) {
|
|
262
|
+
warnings.push(`Declared input "${input.ref}" was not found in project "${pk}".`);
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
valid: missingInputs.length === 0
|
|
266
|
+
&& missingOutputs.length === 0
|
|
267
|
+
&& ambiguousOutputs.length === 0
|
|
268
|
+
&& outputItems.length > 0,
|
|
269
|
+
recipeName,
|
|
270
|
+
projectKey: pk,
|
|
271
|
+
inputs,
|
|
272
|
+
outputs,
|
|
273
|
+
missingInputs,
|
|
274
|
+
missingOutputs,
|
|
275
|
+
ambiguousOutputs,
|
|
276
|
+
warnings,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/** Resolve recipe outputs to job-build targets. */
|
|
280
|
+
async resolveRunOutputs(recipeName, opts) {
|
|
281
|
+
const pk = this.resolveProjectKey(opts?.projectKey);
|
|
282
|
+
const { recipe, } = await this.get(recipeName, { projectKey: pk, });
|
|
283
|
+
const outputItems = recipeOutputItems(recipe);
|
|
284
|
+
if (outputItems.length === 0) {
|
|
285
|
+
throw new Error(`Recipe "${recipeName}" has no output items to build.`);
|
|
286
|
+
}
|
|
287
|
+
const [datasets, folders,] = await Promise.all([
|
|
288
|
+
this.client.datasets.list(pk),
|
|
289
|
+
this.client.folders.list(pk),
|
|
290
|
+
]);
|
|
291
|
+
const datasetNames = new Set(datasets.map((dataset) => dataset.name));
|
|
292
|
+
const folderIdByRef = new Map();
|
|
293
|
+
for (const folder of folders) {
|
|
294
|
+
folderIdByRef.set(folder.id, folder.id);
|
|
295
|
+
if (folder.name)
|
|
296
|
+
folderIdByRef.set(folder.name, folder.id);
|
|
297
|
+
}
|
|
298
|
+
return outputItems.map((item) => {
|
|
299
|
+
if (item.type === "DATASET") {
|
|
300
|
+
return {
|
|
301
|
+
ref: item.ref,
|
|
302
|
+
role: item.role,
|
|
303
|
+
id: item.ref,
|
|
304
|
+
type: "DATASET",
|
|
305
|
+
projectKey: pk,
|
|
306
|
+
partition: opts?.partition,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const folderId = folderIdByRef.get(item.ref);
|
|
310
|
+
if (item.type === "MANAGED_FOLDER") {
|
|
311
|
+
return {
|
|
312
|
+
ref: item.ref,
|
|
313
|
+
role: item.role,
|
|
314
|
+
id: folderId ?? item.ref,
|
|
315
|
+
type: "MANAGED_FOLDER",
|
|
316
|
+
projectKey: pk,
|
|
317
|
+
partition: opts?.partition,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const isDataset = datasetNames.has(item.ref);
|
|
321
|
+
if (isDataset && folderId) {
|
|
322
|
+
throw new Error(`Recipe "${recipeName}" output "${item.ref}" matches both a dataset and a managed folder. Add an explicit output type to the recipe definition or build the target directly with --target-type.`);
|
|
323
|
+
}
|
|
324
|
+
if (folderId) {
|
|
325
|
+
return {
|
|
326
|
+
ref: item.ref,
|
|
327
|
+
role: item.role,
|
|
328
|
+
id: folderId,
|
|
329
|
+
type: "MANAGED_FOLDER",
|
|
330
|
+
projectKey: pk,
|
|
331
|
+
partition: opts?.partition,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
if (isDataset) {
|
|
335
|
+
return {
|
|
336
|
+
ref: item.ref,
|
|
337
|
+
role: item.role,
|
|
338
|
+
id: item.ref,
|
|
339
|
+
type: "DATASET",
|
|
340
|
+
projectKey: pk,
|
|
341
|
+
partition: opts?.partition,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
throw new Error(`Recipe "${recipeName}" output "${item.ref}" was not found as a dataset or managed folder in project "${pk}".`);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
/** Run a recipe by building its resolved outputs. */
|
|
348
|
+
async run(recipeName, opts) {
|
|
349
|
+
const pk = this.resolveProjectKey(opts?.projectKey);
|
|
350
|
+
const outputs = await this.resolveRunOutputs(recipeName, {
|
|
351
|
+
partition: opts?.partition,
|
|
352
|
+
projectKey: pk,
|
|
353
|
+
});
|
|
354
|
+
const shouldWait = opts?.wait === true
|
|
355
|
+
|| opts?.includeLogs === true
|
|
356
|
+
|| opts?.summary === true
|
|
357
|
+
|| opts?.timeoutMs !== undefined
|
|
358
|
+
|| opts?.pollIntervalMs !== undefined;
|
|
359
|
+
if (shouldWait) {
|
|
360
|
+
const waitResult = await this.client.jobs.buildAndWaitOutputs(outputs, {
|
|
361
|
+
buildMode: opts?.buildMode,
|
|
362
|
+
includeLogs: opts?.includeLogs,
|
|
363
|
+
maxLogLines: opts?.maxLogLines,
|
|
364
|
+
logFilter: opts?.logFilter,
|
|
365
|
+
pollIntervalMs: opts?.pollIntervalMs,
|
|
366
|
+
projectKey: pk,
|
|
367
|
+
timeoutMs: opts?.timeoutMs,
|
|
368
|
+
summary: opts?.summary,
|
|
369
|
+
});
|
|
370
|
+
return { recipeName, outputs, ...waitResult, };
|
|
371
|
+
}
|
|
372
|
+
const started = await this.client.jobs.buildOutputs(outputs, {
|
|
373
|
+
buildMode: opts?.buildMode,
|
|
374
|
+
projectKey: pk,
|
|
375
|
+
});
|
|
376
|
+
return { recipeName, outputs, ...started, };
|
|
377
|
+
}
|
|
95
378
|
/** Create a recipe, with optional output dataset provisioning and join configuration. */
|
|
96
379
|
async create(opts) {
|
|
97
380
|
const pk = this.resolveProjectKey(opts.projectKey);
|
|
@@ -327,6 +610,72 @@ export class RecipesResource extends BaseResource {
|
|
|
327
610
|
const merged = { ...current, ...data, recipe: mergedRecipe, };
|
|
328
611
|
await this.client.put(`/public/api/projects/${enc}/recipes/${rnEnc}`, merged);
|
|
329
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
|
+
}
|
|
330
679
|
/**
|
|
331
680
|
* Download a recipe code payload to a local file.
|
|
332
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<
|
|
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
|
/**
|