dataiku-sdk 1.0.0 → 2.0.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/README.md +64 -10
- package/bin/dss.js +67 -78
- package/dist/build-metadata.json +1 -0
- package/dist/src/cli/commands/api-service.js +5 -4
- package/dist/src/cli/commands/app.js +45 -12
- package/dist/src/cli/commands/bundle.js +5 -4
- package/dist/src/cli/commands/data-quality.js +42 -0
- package/dist/src/cli/commands/dataset.d.ts +23 -0
- package/dist/src/cli/commands/dataset.js +187 -6
- package/dist/src/cli/commands/job.js +15 -2
- package/dist/src/cli/commands/project-library.js +4 -4
- package/dist/src/cli/commands/project.js +116 -12
- package/dist/src/cli/contract.d.ts +1 -0
- package/dist/src/cli/contract.js +194 -15
- package/dist/src/cli/doctor.d.ts +12 -2
- package/dist/src/cli/doctor.js +44 -13
- package/dist/src/cli/flags.js +2 -0
- package/dist/src/cli/helpers/app-successor.d.ts +15 -3
- package/dist/src/cli/helpers/app-successor.js +155 -15
- package/dist/src/cli/helpers/cleanup.js +30 -1
- package/dist/src/cli/output.d.ts +3 -1
- package/dist/src/cli/output.js +11 -6
- package/dist/src/cli/types.d.ts +2 -0
- package/dist/src/cli/version.d.ts +47 -4
- package/dist/src/cli/version.js +176 -9
- package/dist/src/cli.js +55 -14
- package/dist/src/client.d.ts +28 -1
- package/dist/src/client.js +34 -7
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.js +1 -0
- package/dist/src/resources/datasets.d.ts +43 -0
- package/dist/src/resources/datasets.js +199 -28
- package/dist/src/resources/folders.js +6 -8
- package/dist/src/resources/projects.d.ts +36 -3
- package/dist/src/resources/projects.js +122 -20
- package/dist/src/skill.js +1 -18
- package/dist/src/utils/project-archive.d.ts +32 -0
- package/dist/src/utils/project-archive.js +623 -0
- package/dist/src/utils/stable-hash.js +1 -2
- package/package.json +10 -10
- package/plugin.json +1 -1
- package/skills/dataiku-dss/SKILL.md +27 -35
|
@@ -1,10 +1,102 @@
|
|
|
1
1
|
import { buildDatasetCloneSettings, } from "../../resources/datasets.js";
|
|
2
2
|
import { deepMerge, } from "../../utils/deep-merge.js";
|
|
3
|
-
import {
|
|
3
|
+
import { compareStrings, stableHash, } from "../../utils/stable-hash.js";
|
|
4
|
+
import { jsonInput, num, schemaColumnsInput, unknownJsonInput, } from "../coerce.js";
|
|
4
5
|
import { datasetSourceSummary, } from "../helpers/dataset.js";
|
|
5
6
|
import { moveCreatedItemsToZone, resolveFlowZoneIdFromFlags, } from "../helpers/flow-zone.js";
|
|
6
7
|
import { enqueueCliWarning, readIfExists, skipResult, } from "../output.js";
|
|
7
8
|
import { requireArgs, UsageError, } from "../usage.js";
|
|
9
|
+
const MAX_SCHEMA_DIFFERENCES = 50;
|
|
10
|
+
function isPlainObject(value) {
|
|
11
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function appendSchemaPath(parent, key) {
|
|
14
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
|
|
15
|
+
? (parent ? `${parent}.${key}` : key)
|
|
16
|
+
: `${parent}[${JSON.stringify(key)}]`;
|
|
17
|
+
}
|
|
18
|
+
function collectSchemaDifferences(expected, actual, path, acc) {
|
|
19
|
+
// Hash equality is the deterministic equivalence test: structurally equal
|
|
20
|
+
// subtrees (any key order) match exactly and are never descended into.
|
|
21
|
+
if (stableHash(expected) === stableHash(actual))
|
|
22
|
+
return;
|
|
23
|
+
if (isPlainObject(expected) && isPlainObject(actual)) {
|
|
24
|
+
const keys = [...new Set([...Object.keys(expected), ...Object.keys(actual),]),].sort(compareStrings);
|
|
25
|
+
for (const key of keys) {
|
|
26
|
+
const nextPath = appendSchemaPath(path, key);
|
|
27
|
+
const hasExpected = key in expected;
|
|
28
|
+
const hasActual = key in actual;
|
|
29
|
+
if (hasExpected && hasActual) {
|
|
30
|
+
collectSchemaDifferences(expected[key], actual[key], nextPath, acc);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
acc.total += 1;
|
|
34
|
+
if (acc.items.length < MAX_SCHEMA_DIFFERENCES) {
|
|
35
|
+
acc.items.push({
|
|
36
|
+
path: nextPath,
|
|
37
|
+
kind: hasActual ? "added" : "removed",
|
|
38
|
+
...(hasActual
|
|
39
|
+
? { actualHash: stableHash(actual[key]), }
|
|
40
|
+
: { expectedHash: stableHash(expected[key]), }),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (Array.isArray(expected) && Array.isArray(actual)) {
|
|
47
|
+
const shared = Math.min(expected.length, actual.length);
|
|
48
|
+
for (let i = 0; i < shared; i++) {
|
|
49
|
+
collectSchemaDifferences(expected[i], actual[i], `${path}[${i}]`, acc);
|
|
50
|
+
}
|
|
51
|
+
for (let i = shared; i < expected.length; i++) {
|
|
52
|
+
acc.total += 1;
|
|
53
|
+
if (acc.items.length < MAX_SCHEMA_DIFFERENCES) {
|
|
54
|
+
acc.items.push({
|
|
55
|
+
path: `${path}[${i}]`,
|
|
56
|
+
kind: "removed",
|
|
57
|
+
expectedHash: stableHash(expected[i]),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (let i = shared; i < actual.length; i++) {
|
|
62
|
+
acc.total += 1;
|
|
63
|
+
if (acc.items.length < MAX_SCHEMA_DIFFERENCES) {
|
|
64
|
+
acc.items.push({
|
|
65
|
+
path: `${path}[${i}]`,
|
|
66
|
+
kind: "added",
|
|
67
|
+
actualHash: stableHash(actual[i]),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
acc.total += 1;
|
|
74
|
+
if (acc.items.length < MAX_SCHEMA_DIFFERENCES) {
|
|
75
|
+
acc.items.push({
|
|
76
|
+
path: path || "$",
|
|
77
|
+
kind: "changed",
|
|
78
|
+
expectedHash: stableHash(expected),
|
|
79
|
+
actualHash: stableHash(actual),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Compare two schema values deterministically — object key order never matters
|
|
85
|
+
* — and report each structural difference as a concise per-path entry carrying
|
|
86
|
+
* subtree hashes instead of subtree values.
|
|
87
|
+
*/
|
|
88
|
+
export function compareDatasetSchemas(expected, actual) {
|
|
89
|
+
const acc = { items: [], total: 0, };
|
|
90
|
+
collectSchemaDifferences(expected, actual, "", acc);
|
|
91
|
+
return {
|
|
92
|
+
equal: acc.items.length === 0,
|
|
93
|
+
expectedHash: stableHash(expected),
|
|
94
|
+
actualHash: stableHash(actual),
|
|
95
|
+
differences: acc.items,
|
|
96
|
+
totalDifferences: acc.total,
|
|
97
|
+
differencesTruncated: acc.items.length < acc.total,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
8
100
|
export const datasetCommands = {
|
|
9
101
|
list: {
|
|
10
102
|
handler: (c, _a, f) => c.datasets.list(f["project-key"]),
|
|
@@ -69,6 +161,34 @@ export const datasetCommands = {
|
|
|
69
161
|
description: "Show backing connection, catalog/schema/table, path, and format for a dataset.",
|
|
70
162
|
examples: ["dss dataset source orders",],
|
|
71
163
|
},
|
|
164
|
+
files: {
|
|
165
|
+
handler: (c, a, f) => {
|
|
166
|
+
requireArgs(a, 1, "dss dataset files <name> [--project-key KEY]");
|
|
167
|
+
return c.datasets.listUploadedFiles(a[0], f["project-key"]);
|
|
168
|
+
},
|
|
169
|
+
usage: "dss dataset files <name> [--project-key KEY]",
|
|
170
|
+
description: "List files stored by an UploadedFiles dataset.",
|
|
171
|
+
examples: ["dss dataset files uploaded_input",],
|
|
172
|
+
},
|
|
173
|
+
"upload-file": {
|
|
174
|
+
handler: (c, a, f) => {
|
|
175
|
+
const usage = "dss dataset upload-file <name> <localPath> --file-name NAME [--project-key KEY]";
|
|
176
|
+
requireArgs(a, 2, usage);
|
|
177
|
+
const fileName = f["file-name"];
|
|
178
|
+
if (!fileName || fileName.trim() === "") {
|
|
179
|
+
throw new UsageError(`--file-name is required. Usage: ${usage}`);
|
|
180
|
+
}
|
|
181
|
+
return c.datasets.uploadDatasetFile(a[0], a[1], {
|
|
182
|
+
fileName,
|
|
183
|
+
projectKey: f["project-key"],
|
|
184
|
+
});
|
|
185
|
+
},
|
|
186
|
+
usage: "dss dataset upload-file <name> <localPath> --file-name NAME [--project-key KEY]",
|
|
187
|
+
description: "Add one new file to an UploadedFiles dataset and verify the stored byte length.",
|
|
188
|
+
examples: [
|
|
189
|
+
"dss dataset upload-file uploaded_input ./input.csv --file-name input.csv",
|
|
190
|
+
],
|
|
191
|
+
},
|
|
72
192
|
"refresh-schema": {
|
|
73
193
|
handler: async (c, a, f) => {
|
|
74
194
|
const usage = "dss dataset refresh-schema <name> [--data JSON | --data-file PATH | --stdin] [--dry-run] [--project-key KEY]";
|
|
@@ -105,18 +225,79 @@ export const datasetCommands = {
|
|
|
105
225
|
examples: ["dss dataset validate-build orders",],
|
|
106
226
|
},
|
|
107
227
|
preview: {
|
|
108
|
-
handler: (c, a, f) => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
228
|
+
handler: async (c, a, f) => {
|
|
229
|
+
const previewUsage = "dss dataset preview <name> [--max-rows N] [--rows N] [--project-key KEY] [--timeout MS]";
|
|
230
|
+
requireArgs(a, 1, previewUsage);
|
|
231
|
+
const maxRows = num(f["max-rows"], "--max-rows");
|
|
232
|
+
if (maxRows !== undefined && (!Number.isSafeInteger(maxRows) || maxRows <= 0 || maxRows > 500)) {
|
|
233
|
+
throw new UsageError(`--max-rows must be a positive integer no greater than 500 (got "${String(f["max-rows"])}"). `
|
|
234
|
+
+ `Usage: ${previewUsage}`, "validation_failed");
|
|
235
|
+
}
|
|
236
|
+
const result = await c.datasets.preview(a[0], {
|
|
237
|
+
maxRows,
|
|
112
238
|
projectKey: f["project-key"],
|
|
113
239
|
timeoutMs: num(f["timeout"], "--timeout"),
|
|
114
240
|
});
|
|
241
|
+
if (result.truncated) {
|
|
242
|
+
enqueueCliWarning({
|
|
243
|
+
code: "dataset_preview_truncated",
|
|
244
|
+
message: `Preview of '${a[0]}' stopped at the ${result.limit}-row cap; the dataset has more rows. ${result.limit >= 500
|
|
245
|
+
? "The preview cap of 500 rows is the public maximum; use `dss dataset download` for more rows."
|
|
246
|
+
: "Re-run with --max-rows N (up to 500) for more rows."}`,
|
|
247
|
+
dataset: a[0],
|
|
248
|
+
rows: result.rowCount,
|
|
249
|
+
limit: result.limit,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return result;
|
|
115
253
|
},
|
|
116
254
|
usage: "dss dataset preview <name> [--max-rows N] [--rows N] [--project-key KEY] [--timeout MS]",
|
|
117
|
-
description: "Preview dataset rows (--rows is an alias for --max-rows).",
|
|
255
|
+
description: "Preview dataset rows (--rows is an alias for --max-rows). Returns { columns, rows, rowCount, truncated, limit }; when the dataset has more rows than the cap, truncated is true and a dataset_preview_truncated warning is written to stderr.",
|
|
118
256
|
examples: ["dss dataset preview orders", "dss dataset preview orders --rows 5",],
|
|
119
257
|
},
|
|
258
|
+
"assert-count": {
|
|
259
|
+
handler: async (c, a, f) => {
|
|
260
|
+
const assertUsage = "dss dataset assert-count <dataset> --expected N [--project-key KEY]";
|
|
261
|
+
requireArgs(a, 1, assertUsage);
|
|
262
|
+
const expected = num(f["expected"], "--expected");
|
|
263
|
+
if (expected === undefined || !Number.isSafeInteger(expected) || expected < 0) {
|
|
264
|
+
throw new UsageError(`--expected must be a non-negative integer (got "${String(f["expected"])}"). `
|
|
265
|
+
+ `Usage: ${assertUsage}`, "validation_failed");
|
|
266
|
+
}
|
|
267
|
+
const result = await c.datasets.assertRowCount(a[0], expected, f["project-key"]);
|
|
268
|
+
return { ...result, dataset: a[0], };
|
|
269
|
+
},
|
|
270
|
+
usage: "dss dataset assert-count <dataset> --expected N [--project-key KEY]",
|
|
271
|
+
description: "Stream the dataset and assert it holds exactly --expected rows, probing at most N+1 rows. Returns { satisfied, expected, count, exact }; a mismatch exits 4 with assertion_failed.",
|
|
272
|
+
examples: ["dss dataset assert-count orders --expected 1000",],
|
|
273
|
+
},
|
|
274
|
+
"assert-schema": {
|
|
275
|
+
handler: async (c, a, f) => {
|
|
276
|
+
const assertUsage = "dss dataset assert-schema <dataset> (--data JSON | --data-file PATH | --stdin) [--project-key KEY]";
|
|
277
|
+
requireArgs(a, 1, assertUsage);
|
|
278
|
+
const expected = unknownJsonInput(f);
|
|
279
|
+
if (expected === undefined) {
|
|
280
|
+
throw new UsageError(`--data, --data-file, or --stdin is required. Usage: ${assertUsage}`);
|
|
281
|
+
}
|
|
282
|
+
const actual = await c.datasets.getSchemaObject(a[0], f["project-key"]);
|
|
283
|
+
const comparison = compareDatasetSchemas(expected, actual);
|
|
284
|
+
return {
|
|
285
|
+
satisfied: comparison.equal,
|
|
286
|
+
dataset: a[0],
|
|
287
|
+
expectedHash: comparison.expectedHash,
|
|
288
|
+
actualHash: comparison.actualHash,
|
|
289
|
+
totalDifferences: comparison.totalDifferences,
|
|
290
|
+
...(comparison.differencesTruncated ? { differencesTruncated: true, } : {}),
|
|
291
|
+
differences: comparison.differences,
|
|
292
|
+
};
|
|
293
|
+
},
|
|
294
|
+
usage: "dss dataset assert-schema <dataset> (--data JSON | --data-file PATH | --stdin) [--project-key KEY]",
|
|
295
|
+
description: "Assert the dataset's full schema equals the expected schema object; reports concise per-path differences with sha256 subtree hashes. A mismatch exits 4 with assertion_failed.",
|
|
296
|
+
examples: [
|
|
297
|
+
`dss dataset assert-schema orders --data-file expected-schema.json`,
|
|
298
|
+
`dss dataset assert-schema orders --data '{"columns":[]}'`,
|
|
299
|
+
],
|
|
300
|
+
},
|
|
120
301
|
metadata: {
|
|
121
302
|
handler: (c, a, f) => {
|
|
122
303
|
requireArgs(a, 1, "dss dataset metadata <name>");
|
|
@@ -33,6 +33,19 @@ function firstNumberPath(value, paths) {
|
|
|
33
33
|
}
|
|
34
34
|
return undefined;
|
|
35
35
|
}
|
|
36
|
+
function aggregateJobWaitResults(jobs, until) {
|
|
37
|
+
if (jobs.length === 1)
|
|
38
|
+
return jobs[0];
|
|
39
|
+
const failed = jobs.find((job) => !job.success);
|
|
40
|
+
return {
|
|
41
|
+
elapsedMs: jobs.reduce((maximum, job) => Math.max(maximum, job.elapsedMs), 0),
|
|
42
|
+
jobs,
|
|
43
|
+
pollCount: jobs.reduce((total, job) => total + job.pollCount, 0),
|
|
44
|
+
state: failed?.state ?? "DONE",
|
|
45
|
+
success: failed === undefined,
|
|
46
|
+
until,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
36
49
|
function jobSummaryId(job, fallback) {
|
|
37
50
|
return stringPath(job, ["baseStatus", "def", "id",])
|
|
38
51
|
?? stringPath(job, ["def", "id",])
|
|
@@ -357,7 +370,7 @@ export const jobCommands = {
|
|
|
357
370
|
projectKey: f["project-key"],
|
|
358
371
|
};
|
|
359
372
|
const jobs = await Promise.all(a.map((jobId) => c.jobs.wait(jobId, options)));
|
|
360
|
-
return
|
|
373
|
+
return aggregateJobWaitResults(jobs, String(f["until"] ?? "all-done"));
|
|
361
374
|
},
|
|
362
375
|
usage: "dss job monitor <id...> [--summary] [--include-logs] [--log-filter stdout|stderr|user|errors] [--max-log-lines N] [--timeout MS] [--poll-interval MS] [--until all-done] [--project-key KEY]",
|
|
363
376
|
description: "Monitor one or more existing jobs and summarize progress counters from logs.",
|
|
@@ -376,7 +389,7 @@ export const jobCommands = {
|
|
|
376
389
|
projectKey: f["project-key"],
|
|
377
390
|
};
|
|
378
391
|
const jobs = await Promise.all(a.map((jobId) => c.jobs.wait(jobId, options)));
|
|
379
|
-
return
|
|
392
|
+
return aggregateJobWaitResults(jobs, String(f["until"] ?? "all-done"));
|
|
380
393
|
},
|
|
381
394
|
usage: "dss job watch <id...> [--include-logs] [--log-filter stdout|stderr|user|errors] [--max-log-lines N] [--timeout MS] [--poll-interval MS] [--until all-done] [--project-key KEY]",
|
|
382
395
|
description: "Watch one or more existing jobs with progress extraction enabled.",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
2
|
import { readStdinText, } from "../coerce.js";
|
|
3
3
|
import { encodedProjectEndpoint, planResult, } from "../output.js";
|
|
4
4
|
import { requireArgs, UsageError, } from "../usage.js";
|
|
@@ -68,8 +68,8 @@ export const projectLibraryCommands = {
|
|
|
68
68
|
if (!out)
|
|
69
69
|
throw new UsageError("--output PATH is required.", "missing_required_flag");
|
|
70
70
|
const bytes = await c.projectLibrary.getFileBytes(a[0], f["project-key"]);
|
|
71
|
-
|
|
72
|
-
return { path: out, bytes:
|
|
71
|
+
const written = await Bun.write(out, bytes, { createPath: false, });
|
|
72
|
+
return { path: out, bytes: written, };
|
|
73
73
|
},
|
|
74
74
|
usage: "dss project-library get-bytes <path> --output PATH [--project-key KEY]",
|
|
75
75
|
description: "Download a project library file's raw bytes to a local file.",
|
|
@@ -126,7 +126,7 @@ export const projectLibraryCommands = {
|
|
|
126
126
|
const content = typeof f["content"] === "string"
|
|
127
127
|
? f["content"]
|
|
128
128
|
: typeof f["file"] === "string"
|
|
129
|
-
? readFileSync(f["file"], "utf-8")
|
|
129
|
+
? fs.readFileSync(f["file"], "utf-8")
|
|
130
130
|
: await readStdinText();
|
|
131
131
|
await c.projectLibrary.addOrUpdateFile(a[0], content, pk);
|
|
132
132
|
return { updated: a[0], };
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
+
import { ClientValidationError, DataikuError, } from "../../errors.js";
|
|
1
2
|
import { deepMerge, } from "../../utils/deep-merge.js";
|
|
2
|
-
import {
|
|
3
|
+
import { inspectProjectArchive, } from "../../utils/project-archive.js";
|
|
4
|
+
import { projectIncarnationHash, } from "../../utils/project-incarnation.js";
|
|
5
|
+
import { jsonInput, num, requiredJsonInput, requiredStringFlag, } from "../coerce.js";
|
|
6
|
+
import { CommandResultFailure, } from "../output.js";
|
|
3
7
|
import { requireArgs, UsageError, } from "../usage.js";
|
|
8
|
+
const INSPECT_ARCHIVE_USAGE = "dss project inspect-archive <file>";
|
|
9
|
+
/** Local, DSS-free archive inspection shared by the local handler and the command handler. */
|
|
10
|
+
function inspectArchiveCommand(args) {
|
|
11
|
+
requireArgs(args, 1, INSPECT_ARCHIVE_USAGE);
|
|
12
|
+
return inspectProjectArchive(args[0]);
|
|
13
|
+
}
|
|
4
14
|
export const projectCommands = {
|
|
5
15
|
list: {
|
|
6
16
|
handler: (c) => c.projects.list(),
|
|
@@ -56,12 +66,75 @@ export const projectCommands = {
|
|
|
56
66
|
},
|
|
57
67
|
delete: {
|
|
58
68
|
handler: async (c, a, f) => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
const usage = "dss project delete <projectKey> [--drop-data] [--if-exists] [--dry-run] [--expect-project-incarnation HASH]";
|
|
70
|
+
requireArgs(a, 1, usage);
|
|
71
|
+
const projectKey = a[0];
|
|
72
|
+
const expectedIncarnation = f["expect-project-incarnation"] === undefined
|
|
73
|
+
? undefined
|
|
74
|
+
: requiredStringFlag(f, "expect-project-incarnation", usage);
|
|
75
|
+
if (expectedIncarnation !== undefined
|
|
76
|
+
&& !/^[0-9a-f]{64}$/.test(expectedIncarnation)) {
|
|
77
|
+
throw new UsageError("--expect-project-incarnation must be a 64-character lowercase SHA-256 hash.", "validation_failed");
|
|
78
|
+
}
|
|
79
|
+
const guarded = f["if-exists"] === true
|
|
80
|
+
|| f["dry-run"] === true
|
|
81
|
+
|| expectedIncarnation !== undefined;
|
|
82
|
+
if (!guarded) {
|
|
83
|
+
// Purely user-requested delete without any guard flag: keep the
|
|
84
|
+
// historical direct DELETE, with no preflight probe.
|
|
85
|
+
await c.projects.deleteProject(projectKey, f["drop-data"] === true);
|
|
86
|
+
return { deleted: projectKey, };
|
|
87
|
+
}
|
|
88
|
+
// Guarded path: existence and identity must be verified before any
|
|
89
|
+
// DELETE. A missing creationTag or an incarnation mismatch refuses
|
|
90
|
+
// before the DELETE under possible project-key reuse.
|
|
91
|
+
let currentIncarnation;
|
|
92
|
+
try {
|
|
93
|
+
const details = await c.projects.get(projectKey);
|
|
94
|
+
if (expectedIncarnation !== undefined) {
|
|
95
|
+
currentIncarnation = projectIncarnationHash(projectKey, details);
|
|
96
|
+
if (currentIncarnation === undefined) {
|
|
97
|
+
throw new ClientValidationError(`DSS did not return creationTag identity for project ${projectKey}. Refusing to delete without project-incarnation verification.`, "validation_failed", "Project-key reuse cannot be excluded. Refusing the DELETE; re-read the project and retry with a hash captured from that incarnation.", { projectKey, });
|
|
98
|
+
}
|
|
99
|
+
if (currentIncarnation !== expectedIncarnation) {
|
|
100
|
+
throw new ClientValidationError(`Project ${projectKey} is not the same project incarnation recorded for this operation. Refusing to delete after project-key reuse.`, "validation_failed", "Re-read the current project and retry only with an artifact captured from that incarnation.", {
|
|
101
|
+
projectKey,
|
|
102
|
+
expectedProjectIncarnationHash: expectedIncarnation,
|
|
103
|
+
currentProjectIncarnationHash: currentIncarnation,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (error instanceof DataikuError && error.status === 404) {
|
|
110
|
+
// Already absent: converge only when the caller asked for it.
|
|
111
|
+
if (f["if-exists"] !== true)
|
|
112
|
+
throw error;
|
|
113
|
+
return { deleted: false, alreadyAbsent: true, projectKey, };
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
if (f["dry-run"] === true) {
|
|
118
|
+
return {
|
|
119
|
+
deleted: false,
|
|
120
|
+
dryRun: true,
|
|
121
|
+
projectKey,
|
|
122
|
+
...(currentIncarnation !== undefined
|
|
123
|
+
? { projectIncarnationHash: currentIncarnation, }
|
|
124
|
+
: {}),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
await c.projects.deleteProject(projectKey, f["drop-data"] === true);
|
|
128
|
+
return {
|
|
129
|
+
deleted: true,
|
|
130
|
+
projectKey,
|
|
131
|
+
...(currentIncarnation !== undefined
|
|
132
|
+
? { projectIncarnationHash: currentIncarnation, }
|
|
133
|
+
: {}),
|
|
134
|
+
};
|
|
62
135
|
},
|
|
63
|
-
usage: "dss project delete <projectKey> [--drop-data]",
|
|
64
|
-
description: "Delete a project (destructive). --drop-data also clears managed datasets.",
|
|
136
|
+
usage: "dss project delete <projectKey> [--drop-data] [--if-exists] [--dry-run] [--expect-project-incarnation HASH]",
|
|
137
|
+
description: "Delete a project (destructive). --drop-data also clears managed datasets; --if-exists treats an absent project as already deleted; --dry-run verifies without deleting; --expect-project-incarnation HASH refuses deletion unless the project still matches the recorded incarnation.",
|
|
65
138
|
examples: ["dss project delete MY_PROJ",],
|
|
66
139
|
},
|
|
67
140
|
duplicate: {
|
|
@@ -87,13 +160,44 @@ export const projectCommands = {
|
|
|
87
160
|
examples: ["dss project export MY_PROJ --output ./my_proj.zip",],
|
|
88
161
|
},
|
|
89
162
|
import: {
|
|
90
|
-
handler: async (c, a) => {
|
|
91
|
-
|
|
92
|
-
|
|
163
|
+
handler: async (c, a, f) => {
|
|
164
|
+
const usage = "dss project import <filePath> [--target-project-key KEY] [--data JSON|--data-file PATH|--stdin]";
|
|
165
|
+
requireArgs(a, 1, usage);
|
|
166
|
+
const settings = jsonInput(f) ?? {};
|
|
167
|
+
const rawTarget = f["target-project-key"];
|
|
168
|
+
const targetProjectKey = rawTarget?.trim();
|
|
169
|
+
if (rawTarget !== undefined && targetProjectKey === "") {
|
|
170
|
+
throw new UsageError(`--target-project-key must not be empty. Usage: ${usage}`);
|
|
171
|
+
}
|
|
172
|
+
const settingsTarget = settings.targetProjectKey;
|
|
173
|
+
if (settingsTarget !== undefined
|
|
174
|
+
&& (typeof settingsTarget !== "string" || settingsTarget.trim() === "")) {
|
|
175
|
+
throw new UsageError(`targetProjectKey in import settings must be a non-empty string. Usage: ${usage}`);
|
|
176
|
+
}
|
|
177
|
+
if (targetProjectKey !== undefined
|
|
178
|
+
&& settingsTarget !== undefined
|
|
179
|
+
&& targetProjectKey !== settingsTarget.trim()) {
|
|
180
|
+
throw new UsageError(`--target-project-key conflicts with targetProjectKey in import settings. Usage: ${usage}`);
|
|
181
|
+
}
|
|
182
|
+
const result = await c.projects.importProjectFromArchive(a[0], targetProjectKey === undefined ? settings : { ...settings, targetProjectKey, });
|
|
183
|
+
if (result.success !== true) {
|
|
184
|
+
throw new CommandResultFailure({ ...result, resource: "project", action: "import", stage: "process", }, 2);
|
|
185
|
+
}
|
|
186
|
+
return result;
|
|
93
187
|
},
|
|
94
|
-
usage: "dss project import <filePath>",
|
|
95
|
-
description: "Upload a project archive
|
|
96
|
-
examples: [
|
|
188
|
+
usage: "dss project import <filePath> [--target-project-key KEY] [--data JSON|--data-file PATH|--stdin]",
|
|
189
|
+
description: "Upload and process a project archive, optionally overriding its project key. The archive is validated locally before upload; verified success reports the used project key, explicit request/actual remapping, and the landed incarnation hash.",
|
|
190
|
+
examples: [
|
|
191
|
+
"dss project import ./my_proj.zip",
|
|
192
|
+
"dss project import ./my_proj.zip --target-project-key MY_PROJ",
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
"inspect-archive": {
|
|
196
|
+
handler: async (_c, a) => inspectArchiveCommand(a),
|
|
197
|
+
localHandler: inspectArchiveCommand,
|
|
198
|
+
usage: INSPECT_ARCHIVE_USAGE,
|
|
199
|
+
description: "Read-only local inspection of a project export archive: member names, sizes, manifest validity, archive issues, and the source project key. Never contacts DSS.",
|
|
200
|
+
examples: ["dss project inspect-archive ./my_proj.zip",],
|
|
97
201
|
},
|
|
98
202
|
"permissions-get": {
|
|
99
203
|
handler: (c, _a, f) => c.projects.getPermissions(f["project-key"]),
|
|
@@ -101,6 +101,7 @@ export declare function commandPlanShape(resource: string, action: string, args:
|
|
|
101
101
|
payload?: unknown;
|
|
102
102
|
localWrites?: unknown;
|
|
103
103
|
wait?: unknown;
|
|
104
|
+
requests?: unknown;
|
|
104
105
|
};
|
|
105
106
|
export declare function buildMutationPlan(resource: string, action: string, meta: CommandMeta, args: string[], flags: Record<string, string | boolean>): Record<string, unknown>;
|
|
106
107
|
/**
|