dataiku-sdk 0.10.1 → 0.12.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.
- package/LICENSE +21 -0
- package/dist/packages/types/src/index.d.ts +19 -2
- package/dist/packages/types/src/index.js +13 -1
- package/dist/src/cli/coerce.d.ts +36 -0
- package/dist/src/cli/coerce.js +239 -0
- package/dist/src/cli/commands/analysis.d.ts +2 -0
- package/dist/src/cli/commands/analysis.js +66 -0
- package/dist/src/cli/commands/api-deployer.d.ts +2 -0
- package/dist/src/cli/commands/api-deployer.js +170 -0
- package/dist/src/cli/commands/api-service.d.ts +2 -0
- package/dist/src/cli/commands/api-service.js +121 -0
- package/dist/src/cli/commands/app.d.ts +2 -0
- package/dist/src/cli/commands/app.js +78 -0
- package/dist/src/cli/commands/bundle.d.ts +2 -0
- package/dist/src/cli/commands/bundle.js +107 -0
- package/dist/src/cli/commands/business-app.d.ts +2 -0
- package/dist/src/cli/commands/business-app.js +76 -0
- package/dist/src/cli/commands/code-env.d.ts +2 -0
- package/dist/src/cli/commands/code-env.js +255 -0
- package/dist/src/cli/commands/code.d.ts +2 -0
- package/dist/src/cli/commands/code.js +63 -0
- package/dist/src/cli/commands/connection.d.ts +2 -0
- package/dist/src/cli/commands/connection.js +54 -0
- package/dist/src/cli/commands/continuous-activity.d.ts +2 -0
- package/dist/src/cli/commands/continuous-activity.js +38 -0
- package/dist/src/cli/commands/dashboard.d.ts +2 -0
- package/dist/src/cli/commands/dashboard.js +99 -0
- package/dist/src/cli/commands/data-quality.d.ts +2 -0
- package/dist/src/cli/commands/data-quality.js +235 -0
- package/dist/src/cli/commands/dataset.d.ts +2 -0
- package/dist/src/cli/commands/dataset.js +288 -0
- package/dist/src/cli/commands/discussion.d.ts +2 -0
- package/dist/src/cli/commands/discussion.js +47 -0
- package/dist/src/cli/commands/doctor.d.ts +2 -0
- package/dist/src/cli/commands/doctor.js +9 -0
- package/dist/src/cli/commands/flow-zone.d.ts +2 -0
- package/dist/src/cli/commands/flow-zone.js +282 -0
- package/dist/src/cli/commands/folder.d.ts +2 -0
- package/dist/src/cli/commands/folder.js +202 -0
- package/dist/src/cli/commands/future.d.ts +2 -0
- package/dist/src/cli/commands/future.js +57 -0
- package/dist/src/cli/commands/index.d.ts +2 -0
- package/dist/src/cli/commands/index.js +78 -0
- package/dist/src/cli/commands/insight.d.ts +2 -0
- package/dist/src/cli/commands/insight.js +156 -0
- package/dist/src/cli/commands/job.d.ts +2 -0
- package/dist/src/cli/commands/job.js +406 -0
- package/dist/src/cli/commands/meaning.d.ts +2 -0
- package/dist/src/cli/commands/meaning.js +115 -0
- package/dist/src/cli/commands/metrics.d.ts +2 -0
- package/dist/src/cli/commands/metrics.js +39 -0
- package/dist/src/cli/commands/ml-task.d.ts +2 -0
- package/dist/src/cli/commands/ml-task.js +171 -0
- package/dist/src/cli/commands/model-evaluation-store.d.ts +2 -0
- package/dist/src/cli/commands/model-evaluation-store.js +83 -0
- package/dist/src/cli/commands/notebook.d.ts +2 -0
- package/dist/src/cli/commands/notebook.js +242 -0
- package/dist/src/cli/commands/project-deployer.d.ts +2 -0
- package/dist/src/cli/commands/project-deployer.js +118 -0
- package/dist/src/cli/commands/project-library.d.ts +2 -0
- package/dist/src/cli/commands/project-library.js +197 -0
- package/dist/src/cli/commands/project.d.ts +2 -0
- package/dist/src/cli/commands/project.js +133 -0
- package/dist/src/cli/commands/recipe.d.ts +2 -0
- package/dist/src/cli/commands/recipe.js +659 -0
- package/dist/src/cli/commands/saved-model.d.ts +2 -0
- package/dist/src/cli/commands/saved-model.js +85 -0
- package/dist/src/cli/commands/scenario.d.ts +2 -0
- package/dist/src/cli/commands/scenario.js +190 -0
- package/dist/src/cli/commands/sql.d.ts +2 -0
- package/dist/src/cli/commands/sql.js +109 -0
- package/dist/src/cli/commands/statistics.d.ts +2 -0
- package/dist/src/cli/commands/statistics.js +165 -0
- package/dist/src/cli/commands/streaming-endpoint.d.ts +2 -0
- package/dist/src/cli/commands/streaming-endpoint.js +50 -0
- package/dist/src/cli/commands/variable.d.ts +2 -0
- package/dist/src/cli/commands/variable.js +49 -0
- package/dist/src/cli/commands/webapp.d.ts +2 -0
- package/dist/src/cli/commands/webapp.js +67 -0
- package/dist/src/cli/commands/wiki.d.ts +2 -0
- package/dist/src/cli/commands/wiki.js +123 -0
- package/dist/src/cli/commands/workspace.d.ts +2 -0
- package/dist/src/cli/commands/workspace.js +68 -0
- package/dist/src/cli/contract.d.ts +104 -0
- package/dist/src/cli/contract.js +2041 -0
- package/dist/src/cli/doctor.d.ts +104 -0
- package/dist/src/cli/doctor.js +356 -0
- package/dist/src/cli/env.d.ts +2 -0
- package/dist/src/cli/env.js +37 -0
- package/dist/src/cli/flags.d.ts +16 -0
- package/dist/src/cli/flags.js +272 -0
- package/dist/src/cli/helpers/cleanup.d.ts +3 -0
- package/dist/src/cli/helpers/cleanup.js +204 -0
- package/dist/src/cli/helpers/dataset.d.ts +2 -0
- package/dist/src/cli/helpers/dataset.js +17 -0
- package/dist/src/cli/helpers/flow-zone.d.ts +69 -0
- package/dist/src/cli/helpers/flow-zone.js +420 -0
- package/dist/src/cli/helpers/recipe.d.ts +11 -0
- package/dist/src/cli/helpers/recipe.js +83 -0
- package/dist/src/cli/output.d.ts +38 -0
- package/dist/src/cli/output.js +159 -0
- package/dist/src/cli/runtime.d.ts +15 -0
- package/dist/src/cli/runtime.js +59 -0
- package/dist/src/cli/types.d.ts +25 -0
- package/dist/src/cli/types.js +1 -0
- package/dist/src/cli/usage.d.ts +16 -0
- package/dist/src/cli/usage.js +84 -0
- package/dist/src/cli/version.d.ts +14 -0
- package/dist/src/cli/version.js +63 -0
- package/dist/src/cli.js +671 -8254
- package/dist/src/client.d.ts +13 -1
- package/dist/src/client.js +34 -13
- package/dist/src/errors.d.ts +6 -0
- package/dist/src/errors.js +102 -4
- package/dist/src/index.d.ts +7 -3
- package/dist/src/index.js +6 -2
- package/dist/src/resources/analyses.d.ts +34 -0
- package/dist/src/resources/analyses.js +31 -0
- package/dist/src/resources/api-services.d.ts +2 -0
- package/dist/src/resources/api-services.js +10 -0
- package/dist/src/resources/applications.d.ts +1 -1
- package/dist/src/resources/applications.js +27 -1
- package/dist/src/resources/continuous-activities.js +1 -0
- package/dist/src/resources/data-quality.js +5 -3
- package/dist/src/resources/datasets.d.ts +3 -0
- package/dist/src/resources/datasets.js +10 -5
- package/dist/src/resources/flow-zones.d.ts +2 -2
- package/dist/src/resources/flow-zones.js +4 -1
- package/dist/src/resources/meanings.d.ts +2 -0
- package/dist/src/resources/meanings.js +4 -0
- package/dist/src/resources/ml-tasks.d.ts +91 -0
- package/dist/src/resources/ml-tasks.js +130 -0
- package/dist/src/resources/model-evaluation-stores.d.ts +41 -0
- package/dist/src/resources/model-evaluation-stores.js +36 -0
- package/dist/src/resources/notebooks.d.ts +5 -7
- package/dist/src/resources/notebooks.js +10 -7
- package/dist/src/resources/projects.d.ts +1 -1
- package/dist/src/resources/projects.js +2 -2
- package/dist/src/resources/recipes.d.ts +3 -3
- package/dist/src/resources/recipes.js +198 -38
- package/dist/src/resources/saved-models.d.ts +41 -0
- package/dist/src/resources/saved-models.js +40 -0
- package/dist/src/resources/scenarios.js +67 -26
- package/dist/src/resources/statistics.js +9 -0
- package/dist/src/schemas.d.ts +2 -2
- package/dist/src/schemas.js +1 -1
- package/dist/src/skill.js +2 -2
- package/dist/src/utils/sanitize.js +2 -1
- package/package.json +1 -1
- package/packages/types/dist/index.d.ts +19 -2
- package/packages/types/dist/index.js +13 -1
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
import { readFileSync, } from "node:fs";
|
|
2
|
+
import { mkdir, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { join, } from "node:path";
|
|
4
|
+
import { deepMerge, } from "../../utils/deep-merge.js";
|
|
5
|
+
import { jobLogFilterFromFlag, jsonInput, maxLogLinesFromFlags, normalizeLineEndings, num, recipeInputDatasetsFromFlags, requiredStringFlag, rewritePairsFromFlags, sha256Hex, splitCsvFlag, stableHash, } from "../coerce.js";
|
|
6
|
+
import { moveCreatedItemsToZone, resolveFlowZoneIdFromFlags, } from "../helpers/flow-zone.js";
|
|
7
|
+
import { readRecipeBackup, recipeBackupDocument, recipeBackupPath, recipeCodeEnv, recipeGraph, recipeInputItemRef, recipeRoleInputItems, recipeRunShouldWait, } from "../helpers/recipe.js";
|
|
8
|
+
import { encodedProjectEndpoint, readIfExists, skipResult, } from "../output.js";
|
|
9
|
+
import { requireArgs, UsageError, } from "../usage.js";
|
|
10
|
+
function formatLineDiff(remoteName, localPath, remoteContent, localContent) {
|
|
11
|
+
if (localContent === remoteContent) {
|
|
12
|
+
return "No differences.";
|
|
13
|
+
}
|
|
14
|
+
const localLines = localContent.split("\n");
|
|
15
|
+
const remoteLines = remoteContent.split("\n");
|
|
16
|
+
const lines = [`--- remote:${remoteName}`, `+++ local:${localPath}`, "",];
|
|
17
|
+
const maxLen = Math.max(localLines.length, remoteLines.length);
|
|
18
|
+
for (let i = 0; i < maxLen; i++) {
|
|
19
|
+
const remoteLine = remoteLines[i];
|
|
20
|
+
const localLine = localLines[i];
|
|
21
|
+
if (remoteLine === localLine)
|
|
22
|
+
continue;
|
|
23
|
+
if (remoteLine !== undefined && localLine !== undefined) {
|
|
24
|
+
lines.push(`@@ line ${String(i + 1)} @@`);
|
|
25
|
+
lines.push(`- ${remoteLine}`);
|
|
26
|
+
lines.push(`+ ${localLine}`);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (remoteLine !== undefined) {
|
|
30
|
+
lines.push(`- ${remoteLine}`);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
lines.push(`+ ${localLine}`);
|
|
34
|
+
}
|
|
35
|
+
return lines.join("\n");
|
|
36
|
+
}
|
|
37
|
+
export const recipeCommands = {
|
|
38
|
+
list: {
|
|
39
|
+
handler: (c, _a, f) => c.recipes.list(f["project-key"]),
|
|
40
|
+
usage: "dss recipe list [--project-key KEY]",
|
|
41
|
+
description: "List all recipes in a project.",
|
|
42
|
+
examples: ["dss recipe list",],
|
|
43
|
+
},
|
|
44
|
+
get: {
|
|
45
|
+
handler: (c, a, f) => {
|
|
46
|
+
requireArgs(a, 1, "dss recipe get <name>");
|
|
47
|
+
return c.recipes.get(a[0], {
|
|
48
|
+
includePayload: f["include-payload"] === true && f["no-payload"] !== true,
|
|
49
|
+
projectKey: f["project-key"],
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
usage: "dss recipe get <name> [--include-payload|--no-payload] [--project-key KEY]",
|
|
53
|
+
description: "Get compact recipe settings unless --include-payload is set.",
|
|
54
|
+
examples: [
|
|
55
|
+
"dss recipe get compute_orders",
|
|
56
|
+
"dss recipe get compute_orders --no-payload",
|
|
57
|
+
"dss recipe get compute_orders --include-payload",
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
"validate-graph": {
|
|
61
|
+
handler: (c, a, f) => {
|
|
62
|
+
requireArgs(a, 1, "dss recipe validate-graph <name>");
|
|
63
|
+
return c.recipes.validateGraph(a[0], {
|
|
64
|
+
projectKey: f["project-key"],
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
usage: "dss recipe validate-graph <name> [--project-key KEY]",
|
|
68
|
+
description: "Validate declared recipe input/output graph references before building.",
|
|
69
|
+
examples: ["dss recipe validate-graph compute_orders",],
|
|
70
|
+
},
|
|
71
|
+
run: {
|
|
72
|
+
handler: async (c, a, f) => {
|
|
73
|
+
requireArgs(a, 1, "dss recipe run <name>");
|
|
74
|
+
const pk = f["project-key"];
|
|
75
|
+
const wait = recipeRunShouldWait(f);
|
|
76
|
+
const options = {
|
|
77
|
+
buildMode: f["build-mode"],
|
|
78
|
+
includeLogs: f["include-logs"] === true,
|
|
79
|
+
logFilter: jobLogFilterFromFlag(f["log-filter"]),
|
|
80
|
+
maxLogLines: maxLogLinesFromFlags(f),
|
|
81
|
+
partition: f["partition"],
|
|
82
|
+
pollIntervalMs: num(f["poll-interval"]),
|
|
83
|
+
projectKey: pk,
|
|
84
|
+
timeoutMs: num(f["timeout"]),
|
|
85
|
+
summary: f["summary"] === true,
|
|
86
|
+
wait,
|
|
87
|
+
};
|
|
88
|
+
if (f["dry-run"] === true) {
|
|
89
|
+
const outputs = await c.recipes.resolveRunOutputs(a[0], {
|
|
90
|
+
partition: options.partition,
|
|
91
|
+
projectKey: pk,
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
dryRun: true,
|
|
95
|
+
action: "run",
|
|
96
|
+
resource: "recipe",
|
|
97
|
+
recipe: a[0],
|
|
98
|
+
outputs,
|
|
99
|
+
...options,
|
|
100
|
+
endpoint: encodedProjectEndpoint(c, pk, "/jobs/"),
|
|
101
|
+
method: "POST",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return c.recipes.run(a[0], options);
|
|
105
|
+
},
|
|
106
|
+
usage: "dss recipe run <name> [--wait|--no-wait] [--build-mode MODE] [--include-logs] [--log-filter stdout|stderr|user|errors] [--summary] [--max-log-lines N] [--timeout MS] [--poll-interval MS] [--partition PARTITION] [--dry-run] [--project-key KEY]",
|
|
107
|
+
description: "Run a recipe by resolving its outputs and submitting the correct dataset or managed-folder build job.",
|
|
108
|
+
examples: [
|
|
109
|
+
"dss recipe run compute_orders --wait",
|
|
110
|
+
"dss recipe run compute_exports --include-logs --log-filter stdout --summary --timeout 600000",
|
|
111
|
+
"dss recipe run compute_exports --dry-run",
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
delete: {
|
|
115
|
+
handler: async (c, a, f) => {
|
|
116
|
+
requireArgs(a, 1, "dss recipe delete <name>");
|
|
117
|
+
const pk = f["project-key"];
|
|
118
|
+
if (f["dry-run"] === true || f["if-exists"] === true) {
|
|
119
|
+
const current = await readIfExists(() => c.recipes.get(a[0], { projectKey: pk, includePayload: true, }));
|
|
120
|
+
if (!current)
|
|
121
|
+
return skipResult("recipe", a[0], "missing");
|
|
122
|
+
if (f["dry-run"] === true) {
|
|
123
|
+
return { dryRun: true, action: "delete", resource: "recipe", name: a[0], current, };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
await c.recipes.delete(a[0], pk);
|
|
127
|
+
return { deleted: a[0], resource: "recipe", };
|
|
128
|
+
},
|
|
129
|
+
usage: "dss recipe delete <name> [--if-exists] [--dry-run] [--project-key KEY]",
|
|
130
|
+
description: "Delete a recipe.",
|
|
131
|
+
examples: ["dss recipe delete compute_orders", "dss recipe delete compute_orders --if-exists",],
|
|
132
|
+
},
|
|
133
|
+
download: {
|
|
134
|
+
handler: (c, a, f) => {
|
|
135
|
+
requireArgs(a, 1, "dss recipe download <name>");
|
|
136
|
+
return c.recipes.download(a[0], {
|
|
137
|
+
outputPath: f["output"],
|
|
138
|
+
projectKey: f["project-key"],
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
usage: "dss recipe download <name> [--output PATH] [--project-key KEY]",
|
|
142
|
+
description: "Download recipe definition as JSON.",
|
|
143
|
+
examples: [
|
|
144
|
+
"dss recipe download compute_orders",
|
|
145
|
+
"dss recipe download compute_orders -o recipe.json",
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
"download-code": {
|
|
149
|
+
handler: (c, a, f) => {
|
|
150
|
+
requireArgs(a, 1, "dss recipe download-code <name>");
|
|
151
|
+
return c.recipes.downloadCode(a[0], {
|
|
152
|
+
outputPath: f["output"],
|
|
153
|
+
projectKey: f["project-key"],
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
usage: "dss recipe download-code <name> [--output PATH] [--project-key KEY]",
|
|
157
|
+
description: "Download the code payload of a recipe.",
|
|
158
|
+
examples: [
|
|
159
|
+
"dss recipe download-code compute_orders",
|
|
160
|
+
"dss recipe download-code compute_orders -o code.py",
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
create: {
|
|
164
|
+
handler: async (c, _a, f) => {
|
|
165
|
+
const type = f["type"];
|
|
166
|
+
if (!type) {
|
|
167
|
+
throw new UsageError("--type is required. Usage: dss recipe create --type TYPE --input DS (--output DS | --output-folder FOLDER_ID)");
|
|
168
|
+
}
|
|
169
|
+
const outputDataset = f["output"];
|
|
170
|
+
const outputFolder = f["output-folder"];
|
|
171
|
+
if (outputDataset && outputFolder) {
|
|
172
|
+
throw new UsageError("--output and --output-folder are mutually exclusive.");
|
|
173
|
+
}
|
|
174
|
+
if (!outputDataset && !outputFolder) {
|
|
175
|
+
throw new UsageError("--output or --output-folder is required. Usage: dss recipe create --type TYPE --input DS (--output DS | --output-folder FOLDER_ID)");
|
|
176
|
+
}
|
|
177
|
+
if (outputFolder && !f["output-connection"]) {
|
|
178
|
+
throw new UsageError("--output-connection is required when using --output-folder.");
|
|
179
|
+
}
|
|
180
|
+
const name = f["name"];
|
|
181
|
+
const pk = f["project-key"];
|
|
182
|
+
const inputDatasets = recipeInputDatasetsFromFlags(f);
|
|
183
|
+
const joinColumns = splitCsvFlag(f["join-on"]);
|
|
184
|
+
const fuzzyColumns = splitCsvFlag(f["fuzzy-on"]);
|
|
185
|
+
const rawFuzzyDistance = f["fuzzy-distance"];
|
|
186
|
+
let fuzzyDistance;
|
|
187
|
+
if (typeof rawFuzzyDistance === "string") {
|
|
188
|
+
const normalized = rawFuzzyDistance.trim().toUpperCase();
|
|
189
|
+
if (![
|
|
190
|
+
"DAMERAU_LEVENSHTEIN",
|
|
191
|
+
"HAMMING",
|
|
192
|
+
"JACCARD",
|
|
193
|
+
"COSINE",
|
|
194
|
+
"EUCLIDEAN",
|
|
195
|
+
].includes(normalized)) {
|
|
196
|
+
throw new UsageError("--fuzzy-distance must be one of DAMERAU_LEVENSHTEIN, HAMMING, JACCARD, COSINE, or EUCLIDEAN.", "invalid_enum");
|
|
197
|
+
}
|
|
198
|
+
fuzzyDistance = normalized;
|
|
199
|
+
}
|
|
200
|
+
const fuzzyThreshold = num(f["fuzzy-threshold"]);
|
|
201
|
+
if (typeof f["fuzzy-threshold"] === "string" && fuzzyThreshold === undefined) {
|
|
202
|
+
throw new UsageError("--fuzzy-threshold must be a finite number.", "validation_failed");
|
|
203
|
+
}
|
|
204
|
+
const payload = {
|
|
205
|
+
type,
|
|
206
|
+
name,
|
|
207
|
+
inputDatasets,
|
|
208
|
+
outputDataset,
|
|
209
|
+
outputFolder,
|
|
210
|
+
outputConnection: f["output-connection"],
|
|
211
|
+
...(joinColumns.length > 0 ? { joinOn: joinColumns, } : {}),
|
|
212
|
+
...(typeof f["join-type"] === "string" ? { joinType: f["join-type"], } : {}),
|
|
213
|
+
...(fuzzyColumns.length > 0 ? { fuzzyOn: fuzzyColumns, } : {}),
|
|
214
|
+
...(fuzzyDistance ? { fuzzyDistance, } : {}),
|
|
215
|
+
...(fuzzyThreshold !== undefined ? { fuzzyThreshold, } : {}),
|
|
216
|
+
fuzzyNormalize: f["normalize"] === true,
|
|
217
|
+
projectKey: pk,
|
|
218
|
+
};
|
|
219
|
+
const zoneId = await resolveFlowZoneIdFromFlags(c, f, pk);
|
|
220
|
+
const zoneMove = zoneId && name
|
|
221
|
+
? [{ objectId: name, objectType: "RECIPE", },]
|
|
222
|
+
: undefined;
|
|
223
|
+
if ((f["if-not-exists"] === true || f["dry-run"] === true) && name) {
|
|
224
|
+
const list = await c.recipes.list(pk);
|
|
225
|
+
const existing = list.find((r) => r.name === name);
|
|
226
|
+
if (existing && f["if-not-exists"] === true && f["dry-run"] !== true) {
|
|
227
|
+
return skipResult("recipe", name, "exists", { current: existing, });
|
|
228
|
+
}
|
|
229
|
+
if (f["dry-run"] === true) {
|
|
230
|
+
return {
|
|
231
|
+
dryRun: true,
|
|
232
|
+
action: "create",
|
|
233
|
+
resource: "recipe",
|
|
234
|
+
name,
|
|
235
|
+
payload,
|
|
236
|
+
...(zoneId ? { zoneId, zoneMove, } : {}),
|
|
237
|
+
...(existing ? { current: existing, } : {}),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (f["dry-run"] === true) {
|
|
242
|
+
return {
|
|
243
|
+
dryRun: true,
|
|
244
|
+
action: "create",
|
|
245
|
+
resource: "recipe",
|
|
246
|
+
payload,
|
|
247
|
+
...(zoneId ? { zoneId, zoneMove, } : {}),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const created = await c.recipes.create(payload);
|
|
251
|
+
const createdName = created.recipeName;
|
|
252
|
+
const moved = await moveCreatedItemsToZone(c, f, [{
|
|
253
|
+
objectId: createdName,
|
|
254
|
+
objectType: "RECIPE",
|
|
255
|
+
},], pk);
|
|
256
|
+
return { created: createdName, resource: "recipe", ...created, ...moved, };
|
|
257
|
+
},
|
|
258
|
+
usage: "dss recipe create --type TYPE --input DS[,DS2] (--output DS | --output-folder FOLDER_ID) [--name NAME] [--output-connection CONN] [--zone ZONE|--zone-id ID] [--if-not-exists] [--dry-run] [--join-on COL|LEFT=RIGHT[,...]] [--join-type LEFT|INNER|RIGHT|FULL] [--fuzzy-on COL|LEFT=RIGHT[,...]] [--fuzzy-distance DAMERAU_LEVENSHTEIN|HAMMING|JACCARD|COSINE|EUCLIDEAN] [--fuzzy-threshold N] [--normalize] [--project-key KEY]",
|
|
259
|
+
description: "Create a recipe with one or more inputs and a dataset or managed-folder output.",
|
|
260
|
+
examples: [
|
|
261
|
+
"dss recipe create --type python --input raw_orders,lookup --output orders_clean",
|
|
262
|
+
"dss recipe create --type python --input orders --input customers --output orders_clean --zone Experiments",
|
|
263
|
+
"dss recipe create --type python --input orders --output-folder LT7TUHJ8 --output-connection filesystem --dry-run",
|
|
264
|
+
],
|
|
265
|
+
},
|
|
266
|
+
clone: {
|
|
267
|
+
handler: async (c, a, f) => {
|
|
268
|
+
const usage = "dss recipe clone [source|--from SOURCE] (--name NAME|--to NAME) [--replace-input FROM=TO] [--replace-output FROM=TO] [--replace-payload-text FROM=TO] [--output DATASET] [--copy-output-settings] [--path PATH] [--metastore-table TABLE] [--zone ZONE|--zone-id ID] [--dry-run] [--project-key KEY]";
|
|
269
|
+
const fromFlag = typeof f["from"] === "string" ? f["from"].trim() : "";
|
|
270
|
+
const sourceName = a[0] ?? fromFlag;
|
|
271
|
+
if (!sourceName) {
|
|
272
|
+
throw new UsageError(`Source recipe is required. Usage: ${usage}`, "missing_required_flag");
|
|
273
|
+
}
|
|
274
|
+
if (a[0] && fromFlag && a[0] !== fromFlag) {
|
|
275
|
+
throw new UsageError("Positional source and --from must match when both are provided.", "invalid_enum");
|
|
276
|
+
}
|
|
277
|
+
const pk = f["project-key"];
|
|
278
|
+
const toFlag = typeof f["to"] === "string" ? f["to"].trim() : "";
|
|
279
|
+
const nameFlag = typeof f["name"] === "string" ? f["name"].trim() : "";
|
|
280
|
+
const name = toFlag || nameFlag;
|
|
281
|
+
if (!name) {
|
|
282
|
+
throw new UsageError(`--name or --to is required. Usage: ${usage}`, "missing_required_flag");
|
|
283
|
+
}
|
|
284
|
+
const inputRewrites = rewritePairsFromFlags(f, "replace-input");
|
|
285
|
+
const outputRewrites = rewritePairsFromFlags(f, "replace-output");
|
|
286
|
+
const payloadTextRewrites = rewritePairsFromFlags(f, "replace-payload-text");
|
|
287
|
+
const opts = {
|
|
288
|
+
projectKey: pk,
|
|
289
|
+
name,
|
|
290
|
+
outputDataset: f["output"],
|
|
291
|
+
outputRewrites,
|
|
292
|
+
inputRewrites,
|
|
293
|
+
payloadTextRewrites,
|
|
294
|
+
copyOutputSettings: f["copy-output-settings"] === true,
|
|
295
|
+
outputPath: f["path"],
|
|
296
|
+
metastoreTableName: f["metastore-table"],
|
|
297
|
+
};
|
|
298
|
+
const source = await c.recipes.get(sourceName, { includePayload: true, projectKey: pk, });
|
|
299
|
+
const outputItems = Object.values((source.recipe.outputs ?? {})).flatMap((role) => role.items ?? []).filter((item) => typeof item.ref === "string");
|
|
300
|
+
const plannedOutputRewrites = { ...outputRewrites, };
|
|
301
|
+
if (opts.outputDataset !== undefined && outputItems.length === 1) {
|
|
302
|
+
plannedOutputRewrites[outputItems[0].ref] = opts.outputDataset;
|
|
303
|
+
}
|
|
304
|
+
if (opts.copyOutputSettings === true
|
|
305
|
+
&& Object.keys(plannedOutputRewrites).length > 1
|
|
306
|
+
&& (opts.outputPath !== undefined || opts.metastoreTableName !== undefined)) {
|
|
307
|
+
throw new UsageError("Cannot reuse --path or --metastore-table for multiple cloned output datasets.", "invalid_enum");
|
|
308
|
+
}
|
|
309
|
+
const zoneId = await resolveFlowZoneIdFromFlags(c, f, pk);
|
|
310
|
+
if (f["dry-run"] === true) {
|
|
311
|
+
return {
|
|
312
|
+
dryRun: true,
|
|
313
|
+
action: "clone",
|
|
314
|
+
resource: "recipe",
|
|
315
|
+
source: sourceName,
|
|
316
|
+
target: name,
|
|
317
|
+
inputRewrites,
|
|
318
|
+
outputRewrites: plannedOutputRewrites,
|
|
319
|
+
copyOutputSettings: opts.copyOutputSettings,
|
|
320
|
+
payloadTextRewrites,
|
|
321
|
+
current: source,
|
|
322
|
+
...(zoneId ? { zoneId, zoneMove: [{ objectId: name, objectType: "RECIPE", },], } : {}),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
const cloned = await c.recipes.clone(sourceName, opts).catch((error) => {
|
|
326
|
+
if (!opts.copyOutputSettings
|
|
327
|
+
&& error instanceof Error
|
|
328
|
+
&& /need to create output dataset/i.test(error.message)) {
|
|
329
|
+
throw new UsageError(`Clone output dataset does not exist. Pass --copy-output-settings to clone the source output settings, or create the output dataset first. Usage: ${usage}`, "missing_required_flag");
|
|
330
|
+
}
|
|
331
|
+
throw error;
|
|
332
|
+
});
|
|
333
|
+
const moved = await moveCreatedItemsToZone(c, f, [{ objectId: name, objectType: "RECIPE", },], pk);
|
|
334
|
+
return { ...cloned, resource: "recipe", ...moved, };
|
|
335
|
+
},
|
|
336
|
+
usage: "dss recipe clone [source|--from SOURCE] (--name NAME|--to NAME) [--replace-input FROM=TO] [--replace-output FROM=TO] [--replace-payload-text FROM=TO] [--output DATASET] [--copy-output-settings] [--path PATH] [--metastore-table TABLE] [--zone ZONE|--zone-id ID] [--dry-run] [--project-key KEY]",
|
|
337
|
+
description: "Clone a recipe graph/settings/payload into a separate experiment recipe.",
|
|
338
|
+
examples: [
|
|
339
|
+
"dss recipe clone compute_orders --name compute_orders_opt --output orders_opt --copy-output-settings --dry-run",
|
|
340
|
+
"dss recipe clone compute_orders --name compute_orders_opt --output orders_opt --zone Experiments",
|
|
341
|
+
],
|
|
342
|
+
},
|
|
343
|
+
diff: {
|
|
344
|
+
handler: async (c, a, f) => {
|
|
345
|
+
requireArgs(a, 1, "dss recipe diff <name> --file PATH");
|
|
346
|
+
const filePath = f["file"];
|
|
347
|
+
if (!filePath) {
|
|
348
|
+
throw new UsageError("--file is required. Usage: dss recipe diff <name> --file PATH");
|
|
349
|
+
}
|
|
350
|
+
const result = await c.recipes.get(a[0], {
|
|
351
|
+
includePayload: true,
|
|
352
|
+
projectKey: f["project-key"],
|
|
353
|
+
});
|
|
354
|
+
if (!result.payload) {
|
|
355
|
+
throw new Error(`Recipe "${a[0]}" has no code payload to diff.`);
|
|
356
|
+
}
|
|
357
|
+
const localContent = readFileSync(filePath, "utf-8");
|
|
358
|
+
return formatLineDiff(a[0], filePath, result.payload, localContent);
|
|
359
|
+
},
|
|
360
|
+
usage: "dss recipe diff <name> --file PATH [--project-key KEY]",
|
|
361
|
+
description: "Show differences between local file and remote recipe code.",
|
|
362
|
+
examples: ["dss recipe diff compute_orders --file code.py",],
|
|
363
|
+
},
|
|
364
|
+
update: {
|
|
365
|
+
handler: async (c, a, f) => {
|
|
366
|
+
requireArgs(a, 1, "dss recipe update <name> [--data '{...}' | --data-file PATH | --stdin]");
|
|
367
|
+
const data = jsonInput(f);
|
|
368
|
+
if (!data) {
|
|
369
|
+
throw new UsageError("--data, --data-file, or --stdin is required. Usage: dss recipe update <name> [--data '{...}' | --data-file PATH | --stdin]");
|
|
370
|
+
}
|
|
371
|
+
const pk = f["project-key"];
|
|
372
|
+
if (f["dry-run"] === true) {
|
|
373
|
+
const current = await c.recipes.get(a[0], { projectKey: pk, includePayload: true, });
|
|
374
|
+
const currentRecipe = current.recipe;
|
|
375
|
+
const next = {
|
|
376
|
+
...current,
|
|
377
|
+
...data,
|
|
378
|
+
recipe: deepMerge(currentRecipe, (data.recipe && typeof data.recipe === "object" && !Array.isArray(data.recipe))
|
|
379
|
+
? data.recipe
|
|
380
|
+
: {}),
|
|
381
|
+
};
|
|
382
|
+
return { dryRun: true, action: "update", resource: "recipe", name: a[0], current, next, };
|
|
383
|
+
}
|
|
384
|
+
await c.recipes.update(a[0], data, pk);
|
|
385
|
+
return { updated: a[0], resource: "recipe", };
|
|
386
|
+
},
|
|
387
|
+
usage: "dss recipe update <name> [--data '{...}' | --data-file PATH | --stdin] [--dry-run] [--project-key KEY]",
|
|
388
|
+
description: "Update recipe settings via JSON merge. Recipe definition fields must be nested under a top-level recipe key.",
|
|
389
|
+
examples: [
|
|
390
|
+
"dss recipe update compute_orders --data-file settings.json --dry-run",
|
|
391
|
+
'dss recipe update compute_orders --data \'{"recipe":{"params":{"envSelection":{"envMode":"EXPLICIT_ENV","envName":"python39"}}}}\'',
|
|
392
|
+
"cat settings.json | dss recipe update compute_orders --stdin",
|
|
393
|
+
],
|
|
394
|
+
},
|
|
395
|
+
"add-input": {
|
|
396
|
+
handler: async (c, a, f) => {
|
|
397
|
+
requireArgs(a, 2, "dss recipe add-input <recipe> <dataset> [--role ROLE] [--if-not-exists] [--dry-run] [--project-key KEY]");
|
|
398
|
+
const role = f["role"] ?? "main";
|
|
399
|
+
const pk = f["project-key"];
|
|
400
|
+
const { recipe, } = await c.recipes.get(a[0], { projectKey: pk, });
|
|
401
|
+
const items = recipeRoleInputItems(recipe, role);
|
|
402
|
+
const present = items.some((item) => recipeInputItemRef(item) === a[1]);
|
|
403
|
+
if (present) {
|
|
404
|
+
if (f["if-not-exists"] === true) {
|
|
405
|
+
return skipResult("recipe", a[0], "exists", { dataset: a[1], role, });
|
|
406
|
+
}
|
|
407
|
+
throw new UsageError(`Dataset "${a[1]}" is already a "${role}" input of recipe "${a[0]}".`, "validation_failed");
|
|
408
|
+
}
|
|
409
|
+
const nextItems = [...items, { ref: a[1], deps: [], },];
|
|
410
|
+
const inputs = nextItems.map(recipeInputItemRef).filter((ref) => Boolean(ref));
|
|
411
|
+
if (f["dry-run"] === true) {
|
|
412
|
+
return {
|
|
413
|
+
dryRun: true,
|
|
414
|
+
action: "add-input",
|
|
415
|
+
resource: "recipe",
|
|
416
|
+
recipe: a[0],
|
|
417
|
+
dataset: a[1],
|
|
418
|
+
role,
|
|
419
|
+
inputs,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
await c.recipes.update(a[0], { recipe: { inputs: { [role]: { items: nextItems, }, }, }, }, pk);
|
|
423
|
+
return { updated: a[0], resource: "recipe", action: "add-input", role, dataset: a[1], inputs, };
|
|
424
|
+
},
|
|
425
|
+
usage: "dss recipe add-input <recipe> <dataset> [--role ROLE] [--if-not-exists] [--dry-run] [--project-key KEY]",
|
|
426
|
+
description: "Add a dataset as a recipe input by appending one item to the current inputs (no need to resend the whole list).",
|
|
427
|
+
examples: [
|
|
428
|
+
"dss recipe add-input compute_orders extra_lookup",
|
|
429
|
+
"dss recipe add-input compute_orders extra_lookup --if-not-exists --dry-run",
|
|
430
|
+
],
|
|
431
|
+
},
|
|
432
|
+
"remove-input": {
|
|
433
|
+
handler: async (c, a, f) => {
|
|
434
|
+
requireArgs(a, 2, "dss recipe remove-input <recipe> <dataset> [--role ROLE] [--if-exists] [--dry-run] [--project-key KEY]");
|
|
435
|
+
const role = f["role"] ?? "main";
|
|
436
|
+
const pk = f["project-key"];
|
|
437
|
+
const { recipe, } = await c.recipes.get(a[0], { projectKey: pk, });
|
|
438
|
+
const items = recipeRoleInputItems(recipe, role);
|
|
439
|
+
const present = items.some((item) => recipeInputItemRef(item) === a[1]);
|
|
440
|
+
if (!present) {
|
|
441
|
+
if (f["if-exists"] === true) {
|
|
442
|
+
return skipResult("recipe", a[0], "missing", { dataset: a[1], role, });
|
|
443
|
+
}
|
|
444
|
+
throw new UsageError(`Dataset "${a[1]}" is not a "${role}" input of recipe "${a[0]}".`, "validation_failed");
|
|
445
|
+
}
|
|
446
|
+
const nextItems = items.filter((item) => recipeInputItemRef(item) !== a[1]);
|
|
447
|
+
const inputs = nextItems.map(recipeInputItemRef).filter((ref) => Boolean(ref));
|
|
448
|
+
if (f["dry-run"] === true) {
|
|
449
|
+
return {
|
|
450
|
+
dryRun: true,
|
|
451
|
+
action: "remove-input",
|
|
452
|
+
resource: "recipe",
|
|
453
|
+
recipe: a[0],
|
|
454
|
+
dataset: a[1],
|
|
455
|
+
role,
|
|
456
|
+
inputs,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
await c.recipes.update(a[0], { recipe: { inputs: { [role]: { items: nextItems, }, }, }, }, pk);
|
|
460
|
+
return {
|
|
461
|
+
updated: a[0],
|
|
462
|
+
resource: "recipe",
|
|
463
|
+
action: "remove-input",
|
|
464
|
+
role,
|
|
465
|
+
dataset: a[1],
|
|
466
|
+
inputs,
|
|
467
|
+
};
|
|
468
|
+
},
|
|
469
|
+
usage: "dss recipe remove-input <recipe> <dataset> [--role ROLE] [--if-exists] [--dry-run] [--project-key KEY]",
|
|
470
|
+
description: "Remove a dataset from a recipe's inputs by dropping one item from the current inputs.",
|
|
471
|
+
examples: [
|
|
472
|
+
"dss recipe remove-input compute_orders stale_lookup",
|
|
473
|
+
"dss recipe remove-input compute_orders stale_lookup --if-exists --dry-run",
|
|
474
|
+
],
|
|
475
|
+
},
|
|
476
|
+
"get-payload": {
|
|
477
|
+
handler: async (c, a, f) => {
|
|
478
|
+
requireArgs(a, 1, "dss recipe get-payload <name>");
|
|
479
|
+
const payload = await c.recipes.getPayload(a[0], {
|
|
480
|
+
projectKey: f["project-key"],
|
|
481
|
+
});
|
|
482
|
+
if (typeof f["output"] === "string") {
|
|
483
|
+
await writeFile(f["output"], payload, "utf-8");
|
|
484
|
+
return f["output"];
|
|
485
|
+
}
|
|
486
|
+
return payload;
|
|
487
|
+
},
|
|
488
|
+
usage: "dss recipe get-payload <name> [--raw] [--output PATH] [--project-key KEY]",
|
|
489
|
+
description: "Print the recipe code payload as JSON; use --raw for raw bytes, not JSON.",
|
|
490
|
+
examples: [
|
|
491
|
+
"dss recipe get-payload compute_orders --raw",
|
|
492
|
+
"dss recipe get-payload compute_orders -o code.py",
|
|
493
|
+
],
|
|
494
|
+
},
|
|
495
|
+
cat: {
|
|
496
|
+
handler: (c, a, f) => {
|
|
497
|
+
requireArgs(a, 1, "dss recipe cat <name> [--raw]");
|
|
498
|
+
return c.recipes.getPayload(a[0], {
|
|
499
|
+
projectKey: f["project-key"],
|
|
500
|
+
});
|
|
501
|
+
},
|
|
502
|
+
usage: "dss recipe cat <name> [--raw] [--project-key KEY]",
|
|
503
|
+
description: "Print a recipe code payload as JSON; use --raw for raw bytes, not JSON.",
|
|
504
|
+
examples: ["dss recipe cat compute_orders --raw",],
|
|
505
|
+
},
|
|
506
|
+
"set-payload": {
|
|
507
|
+
handler: async (c, a, f) => {
|
|
508
|
+
requireArgs(a, 1, "dss recipe set-payload <name> --file PATH");
|
|
509
|
+
const filePath = f["file"];
|
|
510
|
+
if (!filePath)
|
|
511
|
+
throw new UsageError("--file is required.");
|
|
512
|
+
const content = readFileSync(filePath, "utf-8");
|
|
513
|
+
const pk = f["project-key"];
|
|
514
|
+
const shouldBackup = f["no-backup"] !== true;
|
|
515
|
+
const backupDir = shouldBackup
|
|
516
|
+
? f["backup-dir"] ?? join(process.cwd(), ".dss-backups", "recipes")
|
|
517
|
+
: undefined;
|
|
518
|
+
const backupPath = backupDir ? recipeBackupPath(a[0], backupDir) : undefined;
|
|
519
|
+
const current = await c.recipes.get(a[0], {
|
|
520
|
+
projectKey: pk,
|
|
521
|
+
includePayload: true,
|
|
522
|
+
});
|
|
523
|
+
if (f["dry-run"] === true) {
|
|
524
|
+
return {
|
|
525
|
+
dryRun: true,
|
|
526
|
+
action: "set-payload",
|
|
527
|
+
resource: "recipe",
|
|
528
|
+
name: a[0],
|
|
529
|
+
file: filePath,
|
|
530
|
+
current,
|
|
531
|
+
next: { ...current, payload: content, },
|
|
532
|
+
...(backupPath ? { backupPath, backup: recipeBackupDocument(a[0], pk, current), } : {}),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
if (backupDir && backupPath) {
|
|
536
|
+
await mkdir(backupDir, { recursive: true, });
|
|
537
|
+
await writeFile(backupPath, `${JSON.stringify(recipeBackupDocument(a[0], pk, current), null, 2)}\n`, "utf-8");
|
|
538
|
+
}
|
|
539
|
+
await c.recipes.replace(a[0], { ...current, payload: content, }, pk);
|
|
540
|
+
return {
|
|
541
|
+
updated: a[0],
|
|
542
|
+
resource: "recipe",
|
|
543
|
+
file: filePath,
|
|
544
|
+
backupCreated: backupPath !== undefined,
|
|
545
|
+
...(backupPath ? { backupPath, } : {}),
|
|
546
|
+
};
|
|
547
|
+
},
|
|
548
|
+
usage: "dss recipe set-payload <name> --file PATH [--backup-dir DIR|--no-backup] [--dry-run] [--project-key KEY]",
|
|
549
|
+
description: "Upload recipe code from a local file, backing up payload, graph, settings, and version metadata by default.",
|
|
550
|
+
examples: [
|
|
551
|
+
"dss recipe set-payload compute_orders --file code.py --dry-run",
|
|
552
|
+
"dss recipe set-payload compute_orders --file code.py --backup-dir ./backups",
|
|
553
|
+
"dss recipe set-payload compute_orders --file code.py --no-backup",
|
|
554
|
+
],
|
|
555
|
+
},
|
|
556
|
+
restore: {
|
|
557
|
+
handler: async (c, a, f) => {
|
|
558
|
+
const usage = "dss recipe restore <name> --backup FILE [--payload-only] [--dry-run] [--project-key KEY]";
|
|
559
|
+
requireArgs(a, 1, usage);
|
|
560
|
+
const backupPath = requiredStringFlag(f, "backup", usage);
|
|
561
|
+
const backup = readRecipeBackup(backupPath);
|
|
562
|
+
const payload = typeof backup.payload === "string" ? backup.payload : "";
|
|
563
|
+
const pk = f["project-key"];
|
|
564
|
+
const current = await c.recipes.get(a[0], { includePayload: true, projectKey: pk, });
|
|
565
|
+
const backupRecipe = backup.recipe && typeof backup.recipe === "object" && !Array.isArray(backup.recipe)
|
|
566
|
+
? backup.recipe
|
|
567
|
+
: undefined;
|
|
568
|
+
const restoredRecipe = backupRecipe
|
|
569
|
+
? { ...backupRecipe, name: a[0], ...(pk ? { projectKey: pk, } : {}), }
|
|
570
|
+
: undefined;
|
|
571
|
+
const next = f["payload-only"] === true || !restoredRecipe
|
|
572
|
+
? { ...current, payload, }
|
|
573
|
+
: { ...current, recipe: restoredRecipe, payload, };
|
|
574
|
+
if (f["dry-run"] === true) {
|
|
575
|
+
return {
|
|
576
|
+
dryRun: true,
|
|
577
|
+
action: "restore",
|
|
578
|
+
resource: "recipe",
|
|
579
|
+
name: a[0],
|
|
580
|
+
backupPath,
|
|
581
|
+
current,
|
|
582
|
+
next,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
await c.recipes.replace(a[0], next, pk);
|
|
586
|
+
return {
|
|
587
|
+
restored: a[0],
|
|
588
|
+
resource: "recipe",
|
|
589
|
+
backupPath,
|
|
590
|
+
payloadOnly: f["payload-only"] === true,
|
|
591
|
+
};
|
|
592
|
+
},
|
|
593
|
+
usage: "dss recipe restore <name> --backup FILE [--payload-only] [--dry-run] [--project-key KEY]",
|
|
594
|
+
description: "Restore a recipe from a set-payload backup.",
|
|
595
|
+
examples: [
|
|
596
|
+
"dss recipe restore compute_orders --backup .dss-backups/recipes/backup.recipe-backup.json --dry-run",
|
|
597
|
+
],
|
|
598
|
+
},
|
|
599
|
+
"assert-unchanged": {
|
|
600
|
+
handler: async (c, a, f) => {
|
|
601
|
+
const usage = "dss recipe assert-unchanged <name> --since BACKUP [--project-key KEY]";
|
|
602
|
+
requireArgs(a, 1, usage);
|
|
603
|
+
const backupPath = requiredStringFlag(f, "since", usage);
|
|
604
|
+
const backup = readRecipeBackup(backupPath);
|
|
605
|
+
const current = await c.recipes.get(a[0], {
|
|
606
|
+
includePayload: true,
|
|
607
|
+
projectKey: f["project-key"],
|
|
608
|
+
});
|
|
609
|
+
const payloadHash = sha256Hex(current.payload ?? "");
|
|
610
|
+
const normalizedPayloadHash = sha256Hex(normalizeLineEndings(current.payload ?? ""));
|
|
611
|
+
const expectedPayloadHash = typeof backup.payloadHash === "string"
|
|
612
|
+
? backup.payloadHash
|
|
613
|
+
: undefined;
|
|
614
|
+
const expectedNormalizedPayloadHash = typeof backup.normalizedPayloadHash === "string"
|
|
615
|
+
? backup.normalizedPayloadHash
|
|
616
|
+
: typeof backup.payload === "string"
|
|
617
|
+
? sha256Hex(normalizeLineEndings(backup.payload))
|
|
618
|
+
: undefined;
|
|
619
|
+
const checks = [
|
|
620
|
+
{
|
|
621
|
+
name: "payload",
|
|
622
|
+
expected: expectedPayloadHash,
|
|
623
|
+
actual: payloadHash,
|
|
624
|
+
unchanged: expectedPayloadHash === payloadHash
|
|
625
|
+
|| (expectedNormalizedPayloadHash !== undefined
|
|
626
|
+
&& expectedNormalizedPayloadHash === normalizedPayloadHash),
|
|
627
|
+
normalizedExpected: expectedNormalizedPayloadHash,
|
|
628
|
+
normalizedActual: normalizedPayloadHash,
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
name: "graph",
|
|
632
|
+
expected: backup.graphHash,
|
|
633
|
+
actual: stableHash(recipeGraph(current.recipe)),
|
|
634
|
+
unchanged: backup.graphHash === stableHash(recipeGraph(current.recipe)),
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
name: "codeEnv",
|
|
638
|
+
expected: backup.codeEnvHash,
|
|
639
|
+
actual: stableHash(recipeCodeEnv(current.recipe)),
|
|
640
|
+
unchanged: backup.codeEnvHash === stableHash(recipeCodeEnv(current.recipe)),
|
|
641
|
+
},
|
|
642
|
+
].filter((check) => typeof check.expected === "string");
|
|
643
|
+
const failures = checks.filter((check) => !check.unchanged);
|
|
644
|
+
return {
|
|
645
|
+
unchanged: failures.length === 0,
|
|
646
|
+
resource: "recipe",
|
|
647
|
+
name: a[0],
|
|
648
|
+
backupPath,
|
|
649
|
+
checks,
|
|
650
|
+
failures,
|
|
651
|
+
};
|
|
652
|
+
},
|
|
653
|
+
usage: "dss recipe assert-unchanged <name> --since BACKUP [--project-key KEY]",
|
|
654
|
+
description: "Compare current recipe payload, graph, and code env against a backup.",
|
|
655
|
+
examples: [
|
|
656
|
+
"dss recipe assert-unchanged compute_orders --since .dss-backups/recipes/backup.recipe-backup.json",
|
|
657
|
+
],
|
|
658
|
+
},
|
|
659
|
+
};
|